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。

相关推荐
IT_陈寒33 分钟前
SpringBoot自动配置失效?你可能漏了这个小开关
前端·人工智能·后端
kyriewen35 分钟前
我写了1个复盘Skill,和AI协作踩过的坑第二天自动变成护栏
前端·程序员·ai编程
风骏时光牛马36 分钟前
AI提示词异常故障复盘分析
前端
SoaringHeart1 小时前
Flutter 进阶:NCanvasImageLoader 让 Canvas 也能画网络图
前端·flutter
计算机魔术师1 小时前
英伟达两个月叫停360亿美元生意:芯片巨头怕了反垄断?
前端
创新技术阁2 小时前
FastapiAdmin 前后端启动全流程详解
前端·后端·fastapi
七牛开发者2 小时前
实测推荐 3 个 Skill,轻松上手 Codex 网页设计与交付流程
前端·javascript·后端
七牛开发者2 小时前
拆解 dsh 系列:从源码和版本变化看 DeepSeek Harness 的设计取舍
前端·javascript·后端
谭光志2 小时前
如何从网站提取设计风格:DOM、计算样式与 DESIGN.md
前端·javascript·agent
深念Y2 小时前
登录日志与管理员审计日志存储决策
前端·arm开发·后端·微服务·云原生·架构