ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

React 不让你碰 DOM?3 个实战场景搞懂 useRef + Web Worker,性能直接拉满

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属性,可以指向任何值或对象,修改它不会触发渲染

核心用法就三步:

  1. useRef声明一个可变对象,初始值给null
  2. JSX 的 DOM 节点通过ref属性绑定
  3. 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)创建一个引用对象,初始值是null
  • ref={inputRef}把 ref 绑定到<input>上,挂载后inputRef.current就指向这个真实 DOM 节点
  • useEffect里调用.focus(),组件挂载完成后自动聚焦

为什么用useEffect

因为挂载之前 DOM 还不存在,currentnull,直接调.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

这段代码揭示了useRefuseState的分工:

  • 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 App

worker 线程的代码(worker.js):

console.log("worker online")

逐行拆解:

  • useRef(null)创建引用,用来持有 Worker 实例
  • useEffectnew Worker(...)开启线程,只在挂载时执行一次
  • new URL("./worker.js", import.meta.url)是 Vite 推荐的写法,能正确打包 worker 文件
  • workerRef.current就是这个 Worker 实例,后面可以收发消息

为什么不直接let worker = new Worker(...)

因为每次组件渲染都会重新执行函数体,会创建无数个 Worker 线程。用useRef持有,整个组件生命周期内只存在一个实例

⚠️ 踩坑提醒

这几个坑我都替你踩过了,别再踩:

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

useRefcurrent修改时机有讲究。

useEffect或事件回调里改,不要在组件渲染过程中直接改。否则行为不可预测。

2. ref 不会触发渲染

这是特性,不是 bug。

如果你改了ref.current发现界面没变,不是写错了——它本来就不该触发渲染。需要更新 UI,配合useState手动触发。

3. Worker 不是万能的

  • 开启 Worker 有开销,别为小任务开线程
  • Worker 和主线程通信靠postMessage数据要序列化,传大对象有成本
  • Worker 里拿不到 DOM,它和主线程是隔离的

4. 条件渲染的 DOM,ref 可能是 null

如果 DOM 节点是条件渲染的(比如show && <input ref={inputRef} />),在useEffectinputRef.current可能还是null用之前一定要判空。

🧩 总结

一张表搞定useRef的核心认知:

对比项useStateuseRef
改值是否触发渲染
适合存什么响应式业务数据DOM 节点、Worker 实例、定时器 ID
更新时机任意useEffect或事件回调里
典型场景数据驱动 UI持有可变引用

记住三个关键点:

  1. useRef是持久可变对象current改了不触发渲染
  2. DOM 操作交给useRef,不要绕过 React 直接操作 DOM
  3. 耗时任务交给 Web Worker,用useRef持有实例,主线程保持流畅

React 的设计哲学是数据驱动,但它也给你留了useRef这扇后门——当你真的需要直接操作 DOM、持有可变引用、管理线程实例时,它就是最趁手的工具。

完整源码都在我的ref-demo仓库里,包含ref-focus-demoref-worker-demo两个可运行项目,拉下来npm install && npm run dev就能跑。觉得有帮助的话,点个赞收藏一下,下次用到直接翻出来抄。

返回列表