从零理解 useRef:React 中操作 DOM 与持久化变量的唯一桥梁

今日重点

useRef 是 React 为声明式编程留的唯一后门。当 useState 搞不定(要碰 DOM、要存不渲染的变量、要持有 Worker),就用它。核心就一件事:返回一个 { current: xxx } 对象,改 .current 不触发渲染。


DOM 编程之痛

知识点

React 之前,前端靠 document.querySelector 手写 DOM 操作。JS 在 V8 引擎,DOM 在渲染引擎,每次操作都要跨引擎通信,开销大。React 的解法:你只管改数据(useState),DOM 更新框架全包了。

代码

dart 复制代码
// 被淘汰的方式:命令式 DOM 编程
const h1 = document.querySelector('h1')
h1.innerText = '你好'       // 跨引擎通信
h1.style.color = 'red'      // 又跨一次

// React 的方式:声明式
const [msg, setMsg] = useState('你好')
// <h1>{msg}</h1>
setMsg('world')  // 只改数据,React 算 diff,批量更新 DOM

拆解

  • 命令式:每一步都亲自指挥浏览器,数据散落在 DOM 节点上
  • 声明式:只声明"值变了",React 接管所有 DOM 更新

为什么这样设计

跨引擎通信是前端性能瓶颈。React 把 DOM 操作内聚到框架层,批量 + diff 优化,开发者不再直接碰 DOM。但总有事必须碰 DOM(聚焦、播放、滚动),这时才用 useRef。


useRef + DOM

知识点

useRef 绑定 DOM 节点,三步走:useRef(null) 创建盒子 → ref={} 绑定标签 → useEffect.current 拿 DOM。

代码

ref-focus-demo/src/App.jsx:9,16,22

scss 复制代码
const inputRef = useRef(null)        // ① 创建空盒子

useEffect(() => {
  inputRef.current.focus()           // ③ 挂载后 DOM 就绪,调用聚焦
}, [])

<input ref={inputRef} />             // ② 绑定到标签

拆解

  • useRef(null) 返回 { current: null },初始 null 因为此时 input 还没渲染
  • ref={inputRef} 是 React 特殊语法,渲染完成后自动把真实 DOM 塞进 .current
  • useEffect([], []) 在挂载后执行,此时 .current 已是真实 DOM,调 .focus() 有效

为什么这样设计

React 组件是函数,渲染期间 DOM 还不存在。必须等渲染完、DOM 挂上之后才能操作。useEffect 空依赖正好在这个时间点执行。


存值不渲染

知识点

useRef 不仅能存 DOM,还能存任何值。.current 改了,值确实变了,但 React 不知道,页面不更新。需要借 useState 手动触发渲染。

代码

ref-focus-demo/src/App.jsx:37-43

scss 复制代码
const numRef = useRef(0)                // 初始 0
const [, forceRender] = useState(0)     // 只借 setter,不读值

console.log(numRef.current)             // 每次渲染打印

<div onClick={() => {
  numRef.current += 1                   // 改值,React 不感知
  forceRender()                         // 手动触发渲染
}}>
  {numRef.current}                      {/* 渲染时才读到最新值 */}
</div>

拆解

  • numRef.current += 1:值从 0 变 1、2、3,控制台能看到,但页面不动
  • forceRender():借用 useState 的渲染能力,逼 React 重新执行函数
  • 函数重新执行 → {numRef.current} 重新读 → 显示最新值

为什么这样设计

有些变量不需要显示在页面上,但需要跨渲染保持(定时器 ID、上一次的值、Worker 引用)。用 useState 每次改都渲染一次浪费性能,用 useRef 刚刚好。


不是响应式

知识点

useState 是响应式的(改了自动渲染),useRef 不是(改了没反应)。这是两者最本质的区别。

代码

scss 复制代码
// useState:响应式
const [count, setCount] = useState(0)
// setCount(1) → 自动渲染,页面更新

// useRef:非响应式
const countRef = useRef(0)
// countRef.current = 1 → 值变了,页面不动

拆解

  • useState 的 setter 会通知 React "值变了,重渲染"
  • useRef 的 .current 就是一个普通对象属性,改了 React 完全不知道
  • 存 DOM、存 Worker、存定时器 ID 用 ref,显示在页面上的用 state

为什么这样设计

两个 Hook 各司其职:state 管前台(用户看得见的),ref 管后台(纯存储)。如果 ref 也响应式,就没法存"不想渲染的变量"了。


Worker ref

知识点

JS 是单线程的。复杂计算(大量循环、LLM、游戏逻辑)会卡住主线程,页面冻结。Web Worker 另开一条线程,重活扔过去,主线程继续响应用户交互。useRef 用来持有 Worker 实例。

代码

ref-worker-demo/src/App.jsx:20-27

javascript 复制代码
const workerRef = useRef(null)

useEffect(() => {
  workerRef.current = new Worker(
    new URL('./worker.js', import.meta.url)
  )
}, [])

ref-worker-demo/src/worker.js:1

arduino 复制代码
console.log('worker线程启动我来了')

拆解

  • workerRef.current = new Worker(...):手动把 Worker 实例存入 ref
  • new URL('./worker.js', import.meta.url):Vite 的模块化 Worker 加载方式
  • 放 useEffect 里:不在渲染期间创建(让路),空依赖确保只创建一次

为什么这样设计

  • Worker 对象不需要显示,用 ref 不用 state
  • 不在函数体顶层 new Worker():会每次渲染都创建新实例,内存泄漏
  • 放 useEffect 空依赖:挂载后创建一次,卸载时 terminate() 销毁

.current 设计

知识点

为什么 useRef 不直接返回值,非要用 .current 包一层?因为 JS 里只有对象是引用类型,普通值在函数重新执行时会重置。

代码

csharp 复制代码
// 如果不用对象包一层:
function App() {
  let num = 0    // 每次渲染重新声明,永远是 0
  num += 1       // 从 0 变 1,下次渲染又重置为 0
}

// useRef 的做法:
const numRef = useRef(0)  // { current: 0 }
// 对象地址不变 → 改 .current 不会丢 → 持久化

拆解

  • React 组件每次渲染函数都重新执行,普通变量全部重置
  • 对象是引用类型,只要地址不变,改属性是持久的
  • { current: 0 } 的盒子始终是同一个,只是里面的值在变

为什么这样设计

不是 React 故意搞复杂,是 JS 语言本身的限制。返回对象是唯一让值跨渲染持久化的方式。


自查清单

  1. React 为什么要消灭 DOM 编程? --- 见「DOM 编程之痛」
  2. useRef 绑定 DOM 的三步是什么?为什么初始值是 null? --- 见「useRef + DOM」
  3. ref 存值改了为什么页面不更新?怎么让它更新? --- 见「存值不渲染」
  4. useRef 和 useState 的本质区别? --- 见「不是响应式」
  5. 为什么 new Worker 要放 useEffect 里而不是函数顶层? --- 见「Worker ref」
  6. 为什么 useRef 要用 .current 包一层? --- 见「.current 设计
相关推荐
名字还没想好☜1 小时前
React 用 Portal + Context 实现全局 Toast:一次调用、自动消失与队列管理
前端·javascript·react.js·react·next.js
嘟嘟07171 小时前
摘要 手把手实现路由鉴权守卫,逐行拆解 ProtectRoute、localStorage、Navigate state、useLocation、FormDat
javascript
肉肉不吃 肉1 小时前
无渲染组件和组合式函数
前端·javascript·vue.js
其美杰布-富贵-李1 小时前
01 从项目结构开始认识 Vue 3
javascript·vue.js·ecmascript
sugar__salt1 小时前
前端路由技术详解:从传统多页到 React SPA 的完整进化之路
前端·javascript·react.js·前端框架
用户84298142418102 小时前
JS代码压缩实测:可减小体积、提高执行效率!
前端·javascript·后端
Synmbrf2 小时前
ECharts 饼图鼠标悬浮后标签 title 消失的问题
前端·javascript
breeze jiang3 小时前
React useRef 实战:从 input 聚焦到 Web Worker 引用
前端·javascript·react.js
苏灿烤鱼4 小时前
GitHub #2 拆解|把工程经验装进 Agent,为什么仍会“静默失效”?
javascript·人工智能·agent