写在前面:今天学了一个让我"性能觉醒"的知识------useRef + Web Worker 。以前写代码遇到耗时计算(比如 5 亿次循环),页面直接卡死,点啥都没反应------就像你让厨师一边切菜一边收银,结果客人点单都顾不上。老师说:"JS 是单线程的,但浏览器可以给你开一个'独立后厨'------Web Worker 线程。" 把 5 亿次循环丢给 Worker,主线程继续"接待客人",计算完再通过消息机制把结果送回来。页面再也不会卡死了!
一、先认识 useRef:持久可变对象
1.1 useRef 是什么?
老师说:
"useRef 是 React 的一个提供持久可变对象的 Hook 函数,经常用来引用 DOM 节点对象。它有一个 current 属性,可以指向任何值或对象,不会触发渲染。"
javascript
import { useRef } from 'react';
const numRef = useRef(0); // 创建一个可变对象,初始 current = 0
useRef 返回一个对象:{ current: 初始值 }。
numRef.current:可以读。numRef.current += 1:可以改。- 改它不会触发渲染(和 useState 最大的区别)。
1.2 useState vs useRef
看 App.jsx(第一个版本)的对比代码:
jsx
const App = () => {
const numRef = useRef(0); // 引用一个值(非响应式)
const [, forceRender] = useState(0); // 响应式
return (
<div onClick={() => {
numRef.current += 1; // 改 ref 不触发渲染
forceRender(0); // 手动触发渲染
}}>
{numRef.current}
</div>
);
};
| 对比 | useState | useRef |
|---|---|---|
| 改变值 | setCount() |
ref.current = xxx |
| 触发渲染 | ✅ 会 | ❌ 不会 |
| 适用场景 | 数据状态、UI 相关 | DOM 引用、可变对象 |
老师说:
"
useState聚焦数据状态业务;useRefDOM 对象引用等。useState响应式的,useRef非响应式的。"
一句话总结:想让数据驱动 UI → useState;想持有一个不会引起渲染的可变对象 → useRef。
二、useRef 的第一个场景:绑定 DOM
2.1 React 不做 DOM 编程
老师说:
"React/Vue 之前,原生 JS 做 DOM 编程,非常耗性能。React/Vue 新框架直接规避 DOM 编程,
useState数据绑定 + 响应式编程,前端开发方式直接改变。"
原生 JS 时代:
javascript
document.getElementById('input').focus(); // 手动操作 DOM
React 时代:
jsx
<input ref={inputRef} />
// 通过 ref 拿到 DOM 节点,交给 React 管理
2.2 useRef 绑定 DOM
jsx
const App = () => {
const inputRef = useRef(null); // 初始 null,未来指向 DOM 节点
useEffect(() => {
inputRef.current.focus(); // 挂载后自动聚焦输入框
}, []);
return (
<input
type="text"
placeholder="请输入用户名"
ref={inputRef} // ref 属性绑定
/>
);
};
流程:
ini
<input ref={inputRef} />
↓ React 渲染时
inputRef.current = 那个 input 的 DOM 节点
↓ useEffect(挂载后)
inputRef.current.focus() → 输入框自动聚焦
用户体验:页面打开,光标直接落在输入框里,用户不用点一下就能打字。 老师说:"把用户当小白,前端的职责就是打造良好的用户体验。"
三、问题:JS 单线程,耗时任务卡死页面
3.1 JS 为什么是单线程?
老师说:
"JS 做一些前端交互、脚本工作、显示和操作的页面,一致性不能出问题。JS 如果是多线程,可能就会有冲突。"
如果两个线程同时改同一个 DOM,页面就乱了。 所以 JS 设计成单线程------所有任务排队执行(Event Loop)。
3.2 单线程的痛点
一个 5 亿次的循环:
javascript
for (let i = 0; i < 5000000000; i++) {
sum += num * i;
}
这段代码在主线程跑的时候:
主线程:执行 5 亿次循环(卡住!)
↓
用户:滚动屏幕?没反应
用户:点击按钮?没反应
↓
页面就像死了一样
老师说:
"JS 主线程负责脚本执行、DOM 渲染、用户交互,忙得飞起。繁重的 CPU 计算会阻塞主线程,造成页面卡顿。"
这就好比收银台只有一个窗口------你让收银员去后厨炒菜,其他客人全等着。
四、解决方案:Web Worker 独立线程
4.1 什么是 Web Worker?
老师说:
"浏览器提供给 JS 可以调用的耗时性计算或 LLM、游戏等复杂任务的 Worker 线程。开启一个新的线程,和主线程隔离开,两个线程互不打扰,并行执行。"
Web Worker 是浏览器开辟的"独立后厨":
- 主线程:负责 UI、交互(前台接待)。
- Worker 线程:负责耗时计算(后厨炒菜)。
- 两个线程并行执行,互不阻塞。
4.2 创建 Worker
javascript
const worker = new Worker(
new URL("./worker.js", import.meta.url)
);
new Worker() 就开启了一个新线程,加载 worker.js 里的代码。
4.3 Worker 里能做什么?
看 worker.js:
javascript
// web worker 独立子线程计算
// 不可以做 DOM api,有自己的 api
console.log('worker online');
// self 关键字:worker 线程自己的全局对象
self.onmessage = (e) => {
const { num } = e.data; // 接收主线程发来的任务
console.log('Worker收到主线程任务', e.data);
let sum = 0;
for (let i = 0; i < 5000000000; i++) { // 5 亿次循环!
sum += num * i;
}
self.postMessage({ // 计算完成,把结果发回主线程
result: sum
});
};
Worker 的关键点:
| 概念 | 说明 |
|---|---|
| self | Worker 线程的全局对象(类似主线程的 window) |
| self.onmessage | 接收主线程消息的监听器 |
| self.postMessage() | 向主线程发送消息 |
| 不能访问 DOM | Worker 没有 document,不能操作页面 |
4.4 消息机制(通信)
主线程和 Worker 之间的通信靠"消息":
php
主线程:worker.postMessage({ num: 88 }) → 发任务
↓
Worker:self.onmessage 收到任务,开始计算
↓
Worker:self.postMessage({ result: sum }) → 发结果
↓
主线程:worker.onmessage 收到结果
这种"消息机制"就像两个隔开房间的人用对讲机通话------不共享内存,只交换消息。
五、完整实战:useRef 管理 Worker 生命周期
5.1 完整代码
看 App.jsx(第二个版本):
jsx
import { useRef, useEffect, useState } from 'react';
function App() {
console.log('main thread');
const workerRef = useRef(null); // 持久保存 worker 实例
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 组件挂载后,开启一个 worker 线程
workerRef.current = new Worker(
new URL("./worker.js", import.meta.url)
);
// 监听 worker 有没有消息到达
workerRef.current.onmessage = (e) => {
const { result } = e.data;
setResult(result);
setLoading(false);
};
// 组件卸载时,销毁 worker 线程
return () => {
workerRef.current.terminate();
workerRef.current = null;
};
}, []);
const startHeavyCalc = () => {
setLoading(true);
// 给 worker 发一条工作指令,带上参数
workerRef.current.postMessage({
num: 88
});
};
return (
<div style={{ padding: '30px' }}>
<h2>useRef + Web Worker 耗时运算</h2>
<button
onClick={startHeavyCalc}
disabled={loading}
>
{loading ? '正在后台计算...' : '启动繁重计算任务'}
</button>
{result && <h3>计算结果:{result}</h3>}
</div>
);
}
export default App;
5.2 为什么用 useRef 存 Worker?
老师说:
"useRef 用来持久存放 Web Worker 实例,组件每次渲染不会重置该线程对象。方便监听、发送数据,以及组件卸载时销毁线程。"
如果不用 useRef,用普通变量存 Worker:
- 组件每次渲染,普通变量会重新创建。
- Worker 实例可能会丢失或重复创建。
用 useRef 存 Worker:
workerRef.current在组件整个生命周期内持久存在。- 不会因为渲染而重置。
5.3 完整生命周期
| 阶段 | 代码 | 做了什么 |
|---|---|---|
| 挂载后 | useEffect 中 new Worker() |
创建 Worker 线程 |
| 计算时 | workerRef.current.postMessage() |
发送计算任务 |
| 完成时 | workerRef.current.onmessage |
接收结果,setState 更新 |
| 卸载时 | workerRef.current.terminate() |
销毁线程,回收资源 |
useRef + useEffect 的组合:useEffect 负责生命周期(创建/销毁),useRef 负责持久引用。
六、Web Worker 适合什么场景?
老师说:
"耗时性复杂专项任务:游戏引擎计算、LLM、加密等密集计算。"
| 场景 | 为什么用 Worker |
|---|---|
| 游戏引擎 | 物理计算、寻路算法很耗 CPU |
| LLM 推理 | 端侧模型计算量巨大 |
| 加密解密 | 密集的数学运算 |
| 大数据处理 | 百万级数据过滤、排序 |
不适合用 Worker 的: 操作 DOM、需要频繁与主线程交互的任务(消息通信有开销)。
七、总结:useRef + Web Worker
| 概念 | 说明 |
|---|---|
| useRef | 持久可变对象,{ current },不触发渲染 |
| useRef 绑 DOM | ref={inputRef},inputRef.current.focus() |
| JS 单线程 | Event Loop 排队执行,主线程负责 UI |
| 主线程阻塞 | 耗时计算导致页面卡顿 |
| Web Worker | 浏览器开辟的独立后台线程 |
| 消息机制 | postMessage 发送,onmessage 接收 |
| worker 不能碰 DOM | 只有自己的 API,用 self |
| useRef 存 Worker | 持久保存实例,生命周期由 useEffect 管理 |
Web Worker 是浏览器给 JS 的"外挂线程"------把耗时的纯计算任务丢进去,主线程保持流畅。JS 本身仍然是单线程语言,Worker 只是浏览器提供的辅助线程。
写在最后
今天最大的收获,是亲手体验了"页面卡死 → Worker 救场"的全过程。5 亿次循环,以前在主线程跑,页面直接冻住;扔进 Web Worker 后,页面照常响应,计算完结果自己送回来。那种"后台默默干活,前台丝滑如初"的感觉,太爽了。
而且 useRef 这个 Hook 也让我很惊喜------之前只以为它能绑 DOM,现在知道它还能持久保存任何对象(包括 Worker 实例),配合 useEffect 管理生命周期,完美。
下次面试官问你:"useRef 和 Web Worker 怎么配合使用?"
你可以淡定地说:
"useRef 创建持久可变对象({ current }),改变它不会触发渲染,常用于引用 DOM 节点或保存不会因渲染而重置的对象。Web Worker 是浏览器提供的独立后台线程,用来执行耗时计算(如 5 亿次循环、LLM 推理),避免阻塞主线程导致页面卡顿。两者的配合是:在 useEffect 中创建 Worker 并保存到 workerRef.current,组件卸载时 terminate() 销毁;通过 postMessage 发送任务、onmessage 接收结果,用 useState 把结果更新到界面。Worker 线程不能访问 DOM,只能通过消息机制和主线程通信。JS 本身仍是单线程语言,Worker 只是浏览器的辅助线程。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。