Web Worker 不负责渲染:React 中的线程分工与消息闭环

Web Worker 不负责渲染:React 中的线程分工与消息闭环

摘要:这个 React Demo 的关键不是"开启了多线程"这么简单,而是把职责拆开:主线程负责页面,Worker 负责计算,postMessage 负责传输,useRef 负责保存实例,useEffect 负责生命周期。文章基于本地源码整理,运行未验证。

当一个 React 组件执行很大的循环时,真正被占用的是页面主线程。Event Loop 能安排异步任务,却不会让当前的大循环自动并行。此时更合适的思路是使用 Web Worker,并把 Worker 当作一个需要管理的外部资源。

这个 Demo 的核心判断

不要把 Worker 写在组件函数体里,否则每次重新渲染都有重复创建的风险。应当:

text 复制代码
useEffect 创建
useRef 保存
postMessage 发送任务
onmessage 接收结果
cleanup terminate

创建线程和保存实例是两件事

jsx 复制代码
const workerRef = useRef(null)

useEffect(() => {
  workerRef.current = new Worker(
    new URL('../worker.js', import.meta.url)
  )
}, [])

这里最容易混淆:useRef 并没有创建线程,真正创建线程的是 new Worker()。ref 只是提供了一个稳定的 { current } 容器。

Worker 实例不需要参与页面渲染,所以不适合用 state 保存。按钮点击时可以拿出同一个实例:

jsx 复制代码
workerRef.current.postMessage({ num: 88 })

消息协议:两次 postMessage,方向相反

主线程发送:

jsx 复制代码
workerRef.current.postMessage({
  num: 88,
})

Worker 接收并计算:

js 复制代码
self.onmessage = (e) => {
  const { num } = e.data
  let sum = 0

  for (let i = 0; i < 5000000000; i++) {
    sum += num * i
  }

  self.postMessage({ result: sum })
}

Worker 返回的数据由主线程接收:

jsx 复制代码
workerRef.current.onmessage = (e) => {
  setResult(e.data.result)
  setLoading(false)
}

因此要记住:

代码 方向
worker.postMessage(data) 主线程到 Worker
self.onmessage Worker 接收主线程消息
self.postMessage(data) Worker 到主线程
worker.onmessage 主线程接收 Worker 消息

为什么 Worker 不能直接改页面?

Worker 与页面主线程相互独立,不能直接访问 React 组件或 DOM。它只返回普通消息:

js 复制代码
self.postMessage({ result: sum })

主线程收到后再执行:

jsx 复制代码
setResult(e.data.result)

页面更新仍属于 React 主线程的职责。也就是说,Worker 的价值是"分担计算",不是"替代渲染"。

生命周期清理是必要步骤

Worker 创建后必须考虑关闭:

jsx 复制代码
useEffect(() => {
  const worker = new Worker(
    new URL('../worker.js', import.meta.url)
  )

  return () => {
    worker.terminate()
  }
}, [])

组件卸载时调用 terminate(),可以停止后台线程。真实代码还会把 workerRef.current 置为 null,表示当前没有可用实例。

这个示例还应该检查什么?

静态代码中存在几个需要继续验证的边界:

  1. 页面提示"五亿次",实际循环上限是 5000000000,即 50 亿次。
  2. 计算结果可能超出 JavaScript 安全整数范围,普通 Number 不能保证精确整数。
  3. 当前未见 onerroronmessageerror,Worker 失败后 loading 的状态需要运行验证。
  4. 如果结果用 result && 条件渲染,结果为 0 时不会显示。

这些是源码检查结论,不代表已经在浏览器中复现;本文代码运行未验证。

复用这套设计时的检查顺序

text 复制代码
是否是 CPU 密集任务?
  ↓ 是
是否能拆成纯计算、无需 DOM 的函数?
  ↓ 是
useEffect 创建 Worker
  ↓
useRef 保存 Worker
  ↓
约定输入和输出消息结构
  ↓
onmessage 更新 state
  ↓
cleanup terminate

结尾

React 中使用 Web Worker 的重点不是把所有异步工作都放到新线程,而是识别真正会阻塞主线程的 CPU 计算,并建立清晰的消息协议。useRef 解决实例保存,useEffect 解决资源生命周期,useState 解决界面状态,Worker 解决计算隔离。下一步可以给 Demo 补充错误消息和取消任务能力,再验证不同输入规模下的行为。

相关推荐
默_笙3 小时前
☕ 我给 TS 类型做了台"瘦身手术",还顺手用 Docker 起了个 MySQL
前端·javascript
ji_shuke4 小时前
Vue 3 使用 History 哨兵和 popstate 拦截浏览器返回
前端·javascript·vue.js·history·哨兵·popstate
名字还没想好☜5 小时前
Prometheus 告警实战:写 alerting rules、用 Alertmanager 做路由分组与抑制
运维·前端·javascript·docker·kubernetes·prometheus
mONESY5 小时前
手把手从零复刻「英文网页 AI 翻译」Chrome 插件(新手向)
javascript
iFlyCai6 小时前
深入理解Flutter:StatefulWidget生命周期全解析(四)
前端·javascript·flutter
夜雨声烦丿7 小时前
从需求到页面:日期计算器应用的 ArkTS 原生实现
开发语言·javascript·华为·harmonyos
宇智波亚索8 小时前
TypeScript 实际应用
前端·javascript·typescript
:-)9 小时前
idea中的vue文件没有高亮显示
前端·javascript·vue.js·ecmascript·intellij-idea
全栈项目管理程序猿9 小时前
ArcGIS JS 基础教程(14):Map、View 与 Layer 关系
javascript
Damai9 小时前
Next.js 切换动态路由后,为什么页面还保留着上一个分类的数据?
前端·react.js