React 不让你碰 DOM?3 个实战场景搞懂 useRef + Web Worker,性能直接拉满
「React 里我想操作个 DOM,结果发现框架根本不让我直接上手?」
很多刚接触 React 的同学都有这个困惑。
习惯了原生 JS 的document.getElementById,到了 React 却发现全套数据驱动,直接操作 DOM 反而成了一种"禁忌"。
但现实是,有些场景你绕不开 DOM:
- 挂载后自动聚焦输入框
- 引用一个不触发渲染的可变值
- 持有一个 Web Worker 线程实例
这些事useState干不了,只能靠useRef。
这篇文章带你用3 个实战场景,彻底搞懂useRef的核心用法,顺带把Web Worker也一起拿下。看完直接能用到项目里。
🤔 React 为什么不让你直接操作 DOM?
先说清楚一个底层逻辑。
JS 运行在 V8 引擎,DOM 运行在渲染引擎。
JS 里做 DOM 编程,跨引擎通信,非常耗性能。
React、Vue 之前的原生 JS 开发,就是纯 DOM 编程,性能拉胯。
React、Vue 出现后,直接规避 DOM 编程:
useState做数据绑定 + 响应式- 框架帮你高效更新 DOM
- 前端开发方式被彻底改变
数据变了,UI 自动变。这才是 React 想让你做的事。
🎯 那万一非得操作 DOM 呢?
不是说不能操作 DOM,而是交给 React 来管。
useRef就是 React 提供的持久可变对象 Hook,经常用来引用 DOM 节点对象。
它有一个current属性,可以指向任何值或对象,修改它不会触发渲染。
核心用法就三步:
useRef声明一个可变对象,初始值给null- JSX 的 DOM 节点通过
ref属性绑定 current就指向了这个 DOM 节点对象
下面直接上代码。
📌 场景一:挂载后自动聚焦输入框
这是个经典需求:页面打开,输入框自动获得焦点,用户不用再点一下。
原生思路是input.focus(),但在 React 里,你要用useRef持有这个 DOM 节点。
import { useRef, useEffect, useState } from 'react' function App() { const [count, setCount] = useState(0) // ref 对象引用,初始为 null const inputRef = useRef(null) useEffect(() => { // current 指向 input DOM 节点 inputRef.current.focus() }, []) return ( <> {/* 把用户当小白,挂载后直接 focus input */} <input type="text" autoFocus placeholder="请输入用户名" ref={inputRef} /> {count} <button onClick={() => setCount(count + 1)}>增加</button> </> ) } export default App重点讲一下这段代码在干什么:
useRef(null)创建一个引用对象,初始值是nullref={inputRef}把 ref 绑定到<input>上,挂载后inputRef.current就指向这个真实 DOM 节点useEffect里调用.focus(),组件挂载完成后自动聚焦
为什么用useEffect?
因为挂载之前 DOM 还不存在,current是null,直接调.focus()会报错。useEffect的回调在 DOM 渲染完成后执行,这时节点已经就绪。
这里其实用了
autoFocus属性也能实现聚焦,但useRef的意义在于:你拿到了 DOM 节点的完整控制权,后面想干嘛都行。
🔥 场景二:引用一个不触发渲染的可变值
useRef不只能引用 DOM,还能引用任何值。
关键区别在于:
useState改了值 → 触发重渲染useRef改了current→不会触发渲染
来看一个计数器的例子:
import { useRef, useState } from 'react' const App = () => { const numRef = useRef(0) // 引用一个值,改它不会渲染 const [, forceRender] = useState(0) // 响应式,用来手动触发渲染 return ( <> <div onClick={() => { numRef.current++ forceRender(numRef.current) }}> {numRef.current} </div> </> ) } export default App这段代码揭示了useRef和useState的分工:
numRef负责存数据,改它不会让组件重新渲染forceRender负责触发渲染,让界面更新
两者配合,数据存一份、渲染触发一次,职责分离,干净利落。
什么时候用这个模式?
- 存一个定时器 ID,不需要它触发渲染
- 存一个上一次的值,做前后对比
- 存任何"只需要记住、不需要驱动 UI"的数据
记住一句话:useState管响应式数据,useRef管可变引用。
⚙️ 场景三:useRef + Web Worker,耗时计算不卡主线程
这个场景稍微硬核一点,但非常实用。
先问个问题:JS 为什么是单线程?
因为前端做的事很简单——显示页面、响应用户交互。单线程能保证一致性,不会出冲突。
但页面复杂起来,遇到耗时任务怎么办?
- LLM 推理、游戏逻辑、大数据计算……
- Event Loop 的异步机制搞不定真正吃 CPU 的任务
- 主线程一旦被卡住,用户点击、滚动全部失灵
这时候就该 Web Worker 上场了。
Web Worker 是浏览器提供的独立线程,专门干耗时的活:
- 开辟一块独立内存
- 复杂计算在 worker 线程跑
- 完成后通过消息机制通知主线程
- 主线程全程不阻塞
而 Worker 实例本身,用useRef来持有最合适——它不需要触发渲染,只需要被持久引用。
import { useRef, useEffect, useState } from 'react' const App = () => { const workerRef = useRef(null) useEffect(() => { // 开启一个 worker 线程,开销比较大,只开一次 workerRef.current = new Worker( new URL("./worker.js", import.meta.url) ) }, []) return ( <> </> ) } export default Appworker 线程的代码(worker.js):
console.log("worker online")逐行拆解:
useRef(null)创建引用,用来持有 Worker 实例useEffect里new Worker(...)开启线程,只在挂载时执行一次new URL("./worker.js", import.meta.url)是 Vite 推荐的写法,能正确打包 worker 文件workerRef.current就是这个 Worker 实例,后面可以收发消息
为什么不直接let worker = new Worker(...)?
因为每次组件渲染都会重新执行函数体,会创建无数个 Worker 线程。用useRef持有,整个组件生命周期内只存在一个实例。
⚠️ 踩坑提醒
这几个坑我都替你踩过了,别再踩:
1. 不要在渲染过程中读写 ref
useRef的current修改时机有讲究。
在useEffect或事件回调里改,不要在组件渲染过程中直接改。否则行为不可预测。
2. ref 不会触发渲染
这是特性,不是 bug。
如果你改了ref.current发现界面没变,不是写错了——它本来就不该触发渲染。需要更新 UI,配合useState手动触发。
3. Worker 不是万能的
- 开启 Worker 有开销,别为小任务开线程
- Worker 和主线程通信靠
postMessage,数据要序列化,传大对象有成本 - Worker 里拿不到 DOM,它和主线程是隔离的
4. 条件渲染的 DOM,ref 可能是 null
如果 DOM 节点是条件渲染的(比如show && <input ref={inputRef} />),在useEffect里inputRef.current可能还是null。用之前一定要判空。
🧩 总结
一张表搞定useRef的核心认知:
| 对比项 | useState | useRef |
|---|---|---|
| 改值是否触发渲染 | 是 | 否 |
| 适合存什么 | 响应式业务数据 | DOM 节点、Worker 实例、定时器 ID |
| 更新时机 | 任意 | useEffect或事件回调里 |
| 典型场景 | 数据驱动 UI | 持有可变引用 |
记住三个关键点:
useRef是持久可变对象,current改了不触发渲染- DOM 操作交给
useRef,不要绕过 React 直接操作 DOM - 耗时任务交给 Web Worker,用
useRef持有实例,主线程保持流畅
React 的设计哲学是数据驱动,但它也给你留了useRef这扇后门——当你真的需要直接操作 DOM、持有可变引用、管理线程实例时,它就是最趁手的工具。
完整源码都在我的
ref-demo仓库里,包含ref-focus-demo和ref-worker-demo两个可运行项目,拉下来npm install && npm run dev就能跑。觉得有帮助的话,点个赞收藏一下,下次用到直接翻出来抄。