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。

相关推荐
AlienZHOU8 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc11 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师12 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen13 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒13 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow13 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81813 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007813 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明13 小时前
第 6 章 组件进阶
前端·vue.js
江华森13 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端