摘要:JS 是单线程语言,但浏览器提供了 Web Worker 开辟独立线程跑纯计算任务。本文用 useRef 持久化 Worker 实例避免渲染重置,useEffect 在挂载时初始化并在卸载时 terminate,postMessage 发任务给子线程,五千万次循环不卡主界面。消息机制串联双线程通信,loading 状态承接用户等待。
目录
- [Web Worker 解决什么?](#Web Worker 解决什么? "#%E4%B8%80web-worker-%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88")
- [useRef:在 React 中持久化 Worker 实例](#useRef:在 React 中持久化 Worker 实例 "#%E4%BA%8Cuseref%E5%9C%A8-react-%E4%B8%AD%E6%8C%81%E4%B9%85%E5%8C%96-worker-%E5%AE%9E%E4%BE%8B")
- [消息机制:主线程与 Worker 的双向通信](#消息机制:主线程与 Worker 的双向通信 "#%E4%B8%89%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6%E4%B8%BB%E7%BA%BF%E7%A8%8B%E4%B8%8E-worker-%E7%9A%84%E5%8F%8C%E5%90%91%E9%80%9A%E4%BF%A1")
- 完整生命周期:初始化、通信、销毁
- 总结
一、Web Worker 解决什么?
JS 主线程调用栈里,一段 for (let i = 0; i < 50000000; i++) 的循环会阻塞所有 UI 交互------用户的点击、输入、滚动全部卡死,直到循环结束。这不是 JS 写得不好,而是 JS 单线程 Event Loop 的物理限制:主线程既要管 DOM 渲染,又要管用户交互,现在还要管五千万次计算------它忙不过来。
Web Worker 是浏览器提供的方案:在主线程之外开辟一个独立的后台线程,专门跑纯计算任务。

Web Worker 有严格的限制:不能访问 DOM 、不能操作 window 对象、只能通过消息机制和主线程通信。它本质上是浏览器(C++ 多线程)提供给 JS 的辅助线程------JS 本身的单线程机制并未改变,页面渲染和组件更新仍然只能在主线程上运行。
二、useRef:在 React 中持久化 Worker 实例
Worker 的创建成本很高------每次 new Worker() 都会启动一个独立线程。在 React 组件中,如果直接 useState 存放 Worker 实例,组件每次重渲染都会丢失引用并重新创建。useRef 是正确选择:
jsx
const workerRef = useRef(null); // 组件重渲染不会重置这个值
| Hook | 重渲染时 | 适合存放 |
|---|---|---|
useState |
保持不变(但修改会触发重渲染) | 驱动 UI 的状态 |
useRef |
current 属性值保持不变 |
不需要触发渲染的持久引用(计时器、Worker、DOM 节点) |
useRef 返回一个 { current: null } 对象,这个对象在整个组件生命周期中只有一份,修改 current 不会触发重渲染------完美适合存放 Worker 实例。
三、消息机制:主线程与 Worker 的双向通信
主线程和 Worker 线程是隔离的------不能共享变量,不能互相调用函数。通信只能靠消息机制。
主线程发、Worker 收
jsx
// 主线程:发任务
workerRef.current.postMessage({ num: 88 });
javascript
// Worker 线程:收任务
self.onmessage = (e) => {
const { num } = e.data;
let sum = 0;
for (let i = 0; i < 50000000; i++) {
sum += num * i;
}
// 算完发回去
self.postMessage({ result: sum });
};
Worker 发、主线程收
jsx
// 主线程:收结果
workerRef.current.onmessage = (e) => {
const { result } = e.data;
setResult(result); // 更新 UI
setLoading(false); // 关闭 loading
};

整个计算过程中,主线程的 DOM 渲染、用户交互完全不受影响------五千万次循环在 Worker 线程里跑,主线程只负责"发任务"和"收结果"两件事。
四、完整生命周期:初始化、通信、销毁
jsx
function App() {
const workerRef = useRef(null);
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) => {
setResult(e.data.result);
setLoading(false);
};
// 卸载:销毁 Worker
return () => {
workerRef.current.terminate();
workerRef.current = null;
};
}, []); // 只在挂载时执行一次
const startHeavyCalc = () => {
setLoading(true);
workerRef.current.postMessage({ num: 88 });
};
return (
<div style={{ padding: '30px' }}>
<h2>useRef + WebWorker 耗时运算</h2>
<button
onClick={startHeavyCalc}
disabled={loading}
>
{loading ? "正在后台计算..." : "启动繁重计算任务"}
</button>
{result && <h3>计算结果:{result}</h3>}
</div>
);
}
生命周期四个阶段
| 阶段 | 代码 | 做了什么 |
|---|---|---|
| 挂载 | useEffect(() => {...}, []) |
创建 Worker,绑定 onmessage |
| 通信 | postMessage + onmessage |
主线程发任务,Worker 算完发回结果 |
| 更新 | setResult + setLoading |
收到结果后更新 UI 状态 |
| 卸载 | return () => { terminate() } |
销毁 Worker 线程,回收内存 |
useEffect 的 return 是清理函数------组件卸载时自动调用。workerRef.current.terminate() 关闭 Worker 线程,workerRef.current = null 手动回收内存引用。如果不做清理,组件卸载后 Worker 线程仍然在后台运行,造成资源泄漏。
状态流转
disabled={loading} 在计算期间禁用按钮防止重复提交。"正在后台计算..." 文案让用户知道任务在跑,不是页面卡死了。
五、总结
- Web Worker 是浏览器的多线程能力:主线程跑 UI,Worker 线程跑计算。Worker 不能访问 DOM,只能纯计算,通过消息机制通信。JS 仍然是单线程语言。
- useRef 持久化 Worker :
workerRef.current在组件重渲染时不会被重置,适合存放非响应式的持久引用。修改current不触发重渲染。 - 消息机制是唯一的通信方式 :主线程
postMessage发任务 → Workerself.onmessage收 → 计算 →self.postMessage返回结果 → 主线程onmessage收。 - useEffect 管生命周期 :挂载时
new Worker(),卸载时terminate()+ 手动置空。不做清理会造成资源泄漏。 - loading 状态防重复提交 :
disabled={loading}禁按钮,文案切换提示后台计算中。
------ 主线程是前台,Worker 是后厨。你点菜就走,做好了铃响再取。