useRef + Web Worker:React 中的多线程计算

摘要:JS 是单线程语言,但浏览器提供了 Web Worker 开辟独立线程跑纯计算任务。本文用 useRef 持久化 Worker 实例避免渲染重置,useEffect 在挂载时初始化并在卸载时 terminate,postMessage 发任务给子线程,五千万次循环不卡主界面。消息机制串联双线程通信,loading 状态承接用户等待。


目录

  1. [Web Worker 解决什么?](#Web Worker 解决什么? "#%E4%B8%80web-worker-%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88")
  2. [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")
  3. [消息机制:主线程与 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")
  4. 完整生命周期:初始化、通信、销毁
  5. 总结

一、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 线程,回收内存

useEffectreturn 是清理函数------组件卸载时自动调用。workerRef.current.terminate() 关闭 Worker 线程,workerRef.current = null 手动回收内存引用。如果不做清理,组件卸载后 Worker 线程仍然在后台运行,造成资源泄漏。

状态流转

flowchart LR A[初始 state<br/>loading false<br/>result null] --> B[点击按钮<br/>setLoading true] B --> C[postMessage 发任务] C --> D[Worker 计算中<br/>主线程不阻塞] D --> E[Worker 返回结果<br/>onmessage 触发] E --> F[setResult 显示结果<br/>setLoading false<br/>按钮恢复可点击]

disabled={loading} 在计算期间禁用按钮防止重复提交。"正在后台计算..." 文案让用户知道任务在跑,不是页面卡死了。


五、总结

  1. Web Worker 是浏览器的多线程能力:主线程跑 UI,Worker 线程跑计算。Worker 不能访问 DOM,只能纯计算,通过消息机制通信。JS 仍然是单线程语言。
  2. useRef 持久化 WorkerworkerRef.current 在组件重渲染时不会被重置,适合存放非响应式的持久引用。修改 current 不触发重渲染。
  3. 消息机制是唯一的通信方式 :主线程 postMessage 发任务 → Worker self.onmessage 收 → 计算 → self.postMessage 返回结果 → 主线程 onmessage 收。
  4. useEffect 管生命周期 :挂载时 new Worker(),卸载时 terminate() + 手动置空。不做清理会造成资源泄漏。
  5. loading 状态防重复提交disabled={loading} 禁按钮,文案切换提示后台计算中。

------ 主线程是前台,Worker 是后厨。你点菜就走,做好了铃响再取。

相关推荐
Sterting1 小时前
第 10 节:异步请求与 Axios —— 前端与后端
前端
Coder_Ke1 小时前
源码没错,API 却“失踪”了:微信开发者工具的一次作用域背刺
前端
渣波1 小时前
从“迷路”到“瞬移”:彻底搞懂 React Router 的 `useLocation` 与 `useNavigate`
前端·javascript
申君健24864182772021 小时前
WebGPU 开发入门:从设备初始化到渲染一帧
前端
橘子星1 小时前
React Context + 自定义 Hooks:告别 Props 地狱,一行代码跨层级传数据
前端·javascript
moMo1 小时前
前端路由,其实就是三个对象在演戏
前端·react.js
用户2930750976691 小时前
React 自定义 Hooks 实战:用 useMouse 理解响应式封装
前端
触底反弹1 小时前
🔥 别再只用 useRef 拿 DOM 了!这 5 个实战场景让你彻底理解它(附源码解析 + 面试题)
前端·javascript·react.js
玉宇夕落1 小时前
useRef + Web Worker —— 从零到一理解React多线程协作原理
前端