本文通过一个"繁重计算不卡页面"的 Demo,讲解三个核心 Hook 如何配合 Web Worker 工作。
问题背景
JavaScript 在浏览器中是单线程 的。如果有一段耗时 5 秒的 for 循环,页面就会冻结 5 秒------按钮点不动,滚动也卡死。
浏览器提供了 Web Worker API ,可以在后台线程中运行脚本,和主线程通过消息机制 通信。而要在 React 里管理好 Worker 的生命周期,需要 useRef + useEffect + useState 三者协作。
效果:点击按钮 → 按钮禁用显示"正在后台计算..." → 50 万次循环在 Worker 中执行 → 主线程依然流畅 → 计算完成,结果显示在页面上。
完整代码
主组件
jsx
import { useRef, useState, useEffect } from 'react';
function App() {
const workerRef = useRef(null);
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const worker = new Worker(
new URL("./work.js", import.meta.url)
);
workerRef.current = worker;
worker.onmessage = (e) => {
const { result } = e.data;
setResult(result);
setLoading(false);
};
return () => {
worker.terminate();
workerRef.current = null;
};
}, []);
const startHeavyCalc = () => {
setLoading(true);
workerRef.current.postMessage({ num: 88 });
};
return (
<div style={{ padding: "30px" }}>
<h2>useRef + WebWorker 耗时运算</h2>
<p>开启 Web Worker 线程执行 50 万次循环,同时主线程不阻塞</p>
<button onClick={startHeavyCalc} disabled={loading}>
{loading ? "正在后台计算...." : "启动繁重计算任务"}
</button>
{result && <h3>计算结果: {result}</h3>}
</div>
);
}
export default App;
Worker 脚本
js
self.onmessage = (e) => {
const { num } = e.data;
let sum = 0;
for (let i = 0; i < 500000; i++) {
sum += i * num;
}
self.postMessage({ result: sum });
};
Hook 拆解
useState --- 驱动 UI
jsx
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
| 状态 | 作用 | 变化时机 |
|---|---|---|
loading |
按钮禁用 + 文案切换 | 点击 → true;收到结果 → false |
result |
展示计算结果 | Worker 回传时 |
loading 变化 → 按钮灰掉,result 变化 → 数字显示。useState 让数据变化时 React 自动重新渲染。
useEffect --- 副作用与清理
jsx
useEffect(() => {
const worker = new Worker(new URL("./work.js", import.meta.url));
workerRef.current = worker;
worker.onmessage = (e) => { /* 更新 state */ };
return () => {
worker.terminate(); // 终止 Worker 线程
workerRef.current = null; // 释放引用
};
}, []);
- 为什么放 useEffect 里 :
new Worker(...)是副作用------它操作的是浏览器环境,不是计算 UI。渲染函数应该是纯的,副作用必须放 useEffect。 - 为什么 return 清理函数 :组件卸载时 Worker 不会自动销毁,不
terminate()就会内存泄漏。 []空依赖 :只在挂载时执行一次,卸载时清理一次。类比componentDidMount+componentWillUnmount。
useRef --- 持久引用
jsx
const workerRef = useRef(null);
// useEffect 中赋值
workerRef.current = worker;
// 事件处理中使用
workerRef.current.postMessage({ num: 88 });
为什么不用普通变量?
jsx
let worker = null; // ❌ 每次重新渲染,App() 重新执行,worker 又变成 null
为什么不用 useState?
jsx
const [worker, setWorker] = useState(null); // ❌ Worker 实例不需要触发渲染
useRef 返回一个 { current: ... } 对象,在组件整个生命周期中都是同一个对象:
sql
第一次渲染 → { current: null }
↓ useEffect 赋值
第二次渲染 → { current: Worker实例 } (还是同一个对象!)
第三次渲染 → { current: Worker实例 } (依然同一个)
两大特性:
- 持久化 --- 值在多次渲染间保持
- 可变不触发渲染 --- 改
.current不会导致组件更新
三者协作
| Hook | 角色 | 职责 |
|---|---|---|
useRef |
持久引用 | 持有 Worker 实例,跨渲染不丢失 |
useState |
响应式状态 | result/loading 变化 → UI 更新 |
useEffect |
副作用管理 | 创建/销毁 Worker |
- 没有 useRef → Worker 实例重新渲染时丢失
- 没有 useState → Worker 返回了结果但 UI 不动
- 没有 useEffect → Worker 创建时机不可控,清理无从下手
Web Worker 通信模型
scss
主线程 (Main Thread) Worker 线程
───────────────── ────────────
postMessage({num:88}) ──────────► onmessage 收到
for循环计算中...
(主线程可自由处理UI)
onmessage 收到结果 ◄────────── postMessage({result:...})
setResult() → 重新渲染
关键点:
- Worker 和主线程真正并发 --- 跑在独立的 OS 线程上
- 通信靠消息传递,不是共享内存
- Worker 中不能操作 DOM ,没有
window/document,全局对象叫self import.meta.url配合new URL()确保 Vite 正确处理 Worker 文件的打包路径
数据流时间线
scss
点击按钮
→ setLoading(true) 按钮变灰
→ postMessage({num:88}) 向 Worker 发送任务
Worker 开始 50万次循环 【主线程自由,用户正常操作】
Worker 计算完成
← onmessage 触发
→ setResult(result) 显示结果
→ setLoading(false) 按钮恢复
没有 Worker,循环就卡在主线程------期间用户什么也做不了。
总结
| 概念 | 一句话 |
|---|---|
useState |
数据变了通知 React 重新渲染 |
useEffect |
渲染后做副作用,并负责清理 |
useRef |
跨渲染持久引用,改了不触发渲染 |
| Web Worker | 浏览器的真·多线程,用消息通信 |
postMessage/onmessage |
主线程和 Worker 之间的"对讲机" |
标准范式 :useRef 持有引用 → useEffect 管理创建/销毁 → useState 驱动 UI。