React useRef、useEffect、useState 与 Web Worker 多线程实践

本文通过一个"繁重计算不卡页面"的 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实例 }  (依然同一个)

两大特性:

  1. 持久化 --- 值在多次渲染间保持
  2. 可变不触发渲染 --- 改 .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。

相关推荐
玉宇夕落1 小时前
useRef + Web Worker —— 从零到一理解React多线程协作原理
前端
烬羽1 小时前
useContext 用是用了,但你真的用对了吗?——把 Context 封装进自定义 Hook
前端·react.js·全栈
做前端的娜娜子1 小时前
JavaScript 闭包
前端·javascript·掘金·金石计划
xiaominlaopodaren1 小时前
three.js地图数学基础(一)
前端·three.js
何时梦醒1 小时前
🎯 从零彻底搞懂 React Context API —— 一篇带你穿越"组件树"的状态共享方案
前端·javascript·react.js
渣波1 小时前
拒绝页面假死!React 并发编程实战:Web Worker + useRef 深度解析与高性能计算架构
前端·javascript
用户2930750976691 小时前
Web Worker:让 JavaScript 拥有"多线程"能力
前端
渣波1 小时前
赋予 AI “灵魂”:LangChain.js 中临时与长期记忆的终极实战指南
前端·javascript
逍遥德2 小时前
ECMAScript 各个版本的语法列表
前端·javascript·ecmascript·es6