深聊 useRef:比你知道的再多一点
2026/8/8 6:46:36 网站建设 项目流程

用 ref 偷看 DOM 的“底牌”,用 ref 存储“不渲染”的秘密

大家好,在日常开发中,useStateuseEffect几乎是我们的“肌肉记忆”,但useRef这个 Hook 却常常被简化成一个“DOM 获取器”。今天,带你重新认识这位“熟悉的陌生人”。你会发现,useRef不仅仅是document.getElementById的 React 替代品,它更是 React 渲染机制里一个巧妙的“后门”。

1. 从“为什么需要 Ref”说起

在 React 的世界里,我们提倡“数据驱动视图”,意思是你只管改数据,UI 会自己变。这让我们告别了过去“命令式”的 DOM 操作(比如document.getElementById('input').focus()),极大地提升了开发效率和性能。

但“完全禁止操作 DOM”是不现实的。React 官方也清楚这一点,于是提供了useRef这个 Hook,让我们在必要时,可以“抓住”真实的 DOM 节点。

1.1 回顾一下 DOM 操作的代价

在 React 和 Vue 出现之前,我们直接用 JS 操作 DOM。JS 引擎(V8)和渲染引擎是分离的,每次 JS 访问 DOM 都是一次“跨界”。这种跨引擎的通信是有性能开销的。所以 React 的“虚拟 DOM”和“批量更新”本质上就是为了减少这种跨界的频率。

说白了就是:React 推荐你用state控制 UI,而不是自己手动改 DOM。但如果非要去改,请走useRef这个“官方通道”。

2.useRef的两大核心场景

我把useRef的用法抽象为两类:绑定 DOM 节点存储可变数据。虽然它们底层原理一样,但使用场景截然不同。

2.1 场景一:绑定 DOM 节点(“连接”真实世界)

这是useRef最基础的应用。比如,我们想让输入框在页面加载后自动获得焦点。

代码示例:自动聚焦

import React, { useRef, useEffect } from 'react'; const LoginInput = () => { // 1. 声明一个 ref 对象,初始值为 null const inputRef = useRef(null); // 2. 在组件挂载完成后,手动聚焦 useEffect(() => { // 此时 inputRef.current 就是真实的 <input> DOM 元素 if (inputRef.current) { inputRef.current.focus(); } }, []); // 空依赖数组,确保只执行一次 return ( // 3. 通过 ref 属性将 DOM 节点挂载到 inputRef.current 上 <input ref={inputRef} type="text" placeholder="用户名" /> ); };

深度解析:

  • useRef(null)创建了一个对象{ current: null }
  • 当 React 将<input>渲染到真实 DOM 后,会在“提交阶段”(commit phase)更新inputRef.current的指向。
  • useEffect在 DOM 绘制完成后执行,所以我们能在其中安全地操作inputRef.current

2.2 场景二:存储可变数据(“跨渲染”的容器)

这是useRef最容易被忽略但极其强大的功能。它像是一个组件实例上的“全局变量”,可以用来存储任何可变值,并且改变它不会触发组件重新渲染

代码示例:记录点击次数

import React, { useRef, useState } from 'react'; const ClickCounter = () => { // 定义一个类似“全局变量”的计数器 const countRef = useRef(0); // 这个 state 只用来“强制”触发渲染,它的值不重要 const [, setDummy] = useState(0); const handleClick = () => { // 修改 ref.current 不会触发渲染 countRef.current += 1; console.log('点击次数:', countRef.current); // 手动触发一下渲染,以便看到 UI 上的变化(虽然我们用 ref 存值) setDummy(prev => prev + 1); }; return ( <div> {/* 你会发现,即使 setDummy 触发了渲染,countRef.current 的值依然保持不变 */} <p>点击次数(用 ref 存储):{countRef.current}</p> <button onClick={handleClick}>增加</button> </div> ); };

useState是给 React看的,变了它就得干活(渲染);useRef是给你自己用的,怎么变都行,别打扰 React。

2.3useRefvsuseState核心差异

为了让你更清晰,我整理了一个对比表格:

特性useStateuseRef
更新方式setXxx(newValue)ref.current = newValue
触发渲染(更新后重绘)不会(静默修改)
存储内容用于渲染的数据(UI 状态)不用于渲染的数据(副作用、实例变量)
读取时机渲染期间读取任何时候(事件、Effect、定时器)
典型用途表单输入、控制显示隐藏DOM 引用、定时器 ID、跨渲染变量

3. 进阶:useRef的“不变”原理(底层触发机制深度解析)

你可能会好奇,为什么useRef修改值不会触发渲染,而setState一改就立刻重绘?而且为什么useRef能在多次渲染之间保持同一个引用?

答案就在它的“出生地”和 React 的任务调度机制里。

useRef实际上是在 React 的“内部 Fiber 节点”上挂载了一个属性。每次组件函数执行(渲染)时,useRef返回的都是同一个内存地址的对象。React 只会在首次渲染时初始化current属性,后续渲染直接复用。

但更深层次的区别,在于 React 的“更新队列”和“调度中心”是如何看待这两个 Hook 的:

  • useState的“强制唤醒”机制:当你调用setCount时,React 会做两件非常重的事情。首先,它会在该 Fiber 节点的更新队列(updateQueue)中创建一个更新对象(Update Object);其次,它会向 React 的调度器(Scheduler)发起一个“优先级任务”请求。这个任务会强制组件重新执行(Re-render),哪怕你的新值和旧值一模一样(除非使用了updater函数做惰性判断)。重新执行后,React 会走完完整的“调度 -> 协调(Diffing)-> 提交(Commit)”流水线。
  • useRef的“静默修改”机制:当你修改ref.current时,你仅仅是在直接修改 Fiber 节点memoizedState中的一个普通 JavaScript 对象属性。关键在于,这个突变(Mutation)完全绕过了 React 的状态更新机制。因为没有新的 Update 对象被创建,调度器的任务队列空空如也,React 根本感知不到这个变化。在 React 的理念里,“没有 Update 就意味着没有状态变化,既然状态没变,那就没有重新渲染的必要”。

换个更形象的比喻:

useStatesetter像是你在公司群里@了全体成员(React),并大喊“出大事了,快开会(重新渲染)!”;而修改useRef.current就像是你默默在自己的工位笔记本上涂改了一笔,老板(React)根本不知道,自然也不会组织全员大会。

这种机制带来的最大好处就是极致的性能优化。如果你的组件需要频繁记录一些“与 UI 无关”的数据(比如滚动事件的节流标记、动画的帧率计数器、定时器的 ID),使用useRef可以确保这些高频操作完全不影响 React 的主渲染线程,从而避免掉帧和无意义的计算。


4. 常见踩坑与最佳实践

4.1 不要在渲染过程中读写ref.current

// ❌ 错误示例 const App = () => { const ref = useRef(0); // 在渲染函数中修改 ref,会导致不可预测的结果,甚至破坏并发渲染 ref.current = ref.current + 1; return <div>{ref.current}</div>; };

最佳实践:只在事件处理函数、useEffectsetTimeout等“副作用”中操作ref.current

4.2 用ref来“缓存”函数或定时器

有时候,我们需要在组件卸载时清除定时器。用useRef来存储定时器 ID 是最优雅的方式。

const Timer = () => { const timerIdRef = useRef(null); const startTimer = () => { timerIdRef.current = setInterval(() => { console.log('tick'); }, 1000); }; const stopTimer = () => { clearInterval(timerIdRef.current); }; // 在组件卸载时清理 useEffect(() => { return () => { if (timerIdRef.current) { clearInterval(timerIdRef.current); } }; }, []); // ... return JSX with buttons };

4.3 别把所有东西都往ref里放

useRef是用来存放“副作用”的,不是用来存业务数据的。如果你发现某个值需要被 UI 展示,那它必须是 State;如果某个值的变化需要 UI 响应,那它也必须是 State。

一句话总结:UI 要用的,用useState;UI 不关心的,用useRef


5. 总结

useRef是 React 设计中非常精妙的一环。它既是我们与命令式 DOM 操作之间的桥梁,也是我们绕过 React 响应式系统的一个“安全通道”。

通过今天的文章,希望你能记住三点:

  1. ref抓 DOM:替代document.querySelector,在useEffect中操作。
  2. ref存变量:存储那些“变了也不需要重新渲染”的数据。
  3. 牢记不变性:useRef在整个生命周期中保持引用不变,这是其所有特性的基石。

下次当你再次写下useRef时,别忘了你正在使用 React 赋予你的“超能力”,去窥探和影响那个响应式世界之外的物理空间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询