5月27日 14:02

Web Worker 和主线程怎么通信?

两种通信方式:拷贝和共享

Worker 和主线程之间不共享内存(SharedArrayBuffer 除外),数据必须"过桥"。过桥有两种方式:

结构化克隆(默认):数据完整拷贝一份,双方各持一份,互不影响。类似你复印一份文件给同事。

Transferable 转移:数据所有权直接移交,发送方丧失访问权。类似你把原件直接递给同事,自己手里没了。

javascript
// 结构化克隆(默认)—— 数据拷贝 worker.postMessage({ data: largeArray }); // 主线程和 Worker 各有一份,largeArray 仍在 // Transferable 转移 —— 所有权移交 const buffer = new ArrayBuffer(1024 * 1024); // 1MB worker.postMessage({ buffer }, [buffer]); // buffer.byteLength === 0,主线程不能再用了

选哪种?小数据无所谓,大数据(超过 100KB 的 ArrayBuffer、Blob)用 Transferable,否则拷贝开销能吃掉你 Worker 带来的全部性能收益。

结构化克隆支持什么

postMessage 不是 JSON.stringify,它用的是浏览器内置的结构化克隆算法,能处理的东西比 JSON 多:

能传的:对象、数组、字符串、数字、布尔值、Date、RegExp、Blob、File、ArrayBuffer、TypedArray、Map、Set、ImageData、Error

不能传的:函数、DOM 节点、Symbol、有循环引用的对象(部分情况)

一个容易踩的坑:对象的方法和原型链不会被克隆。你传一个 class 实例过去,对面收到的是一个纯数据对象,方法全丢了。如果 Worker 需要调用方法,要么传纯数据重新构造,要么用 RPC 模式。

双向通信的实战写法

简单的 echo 通信谁都会写,但生产环境里你需要的是"请求-响应"模式——主线程发任务,Worker 算完回结果,最好还能 Promise 化。

javascript
// 主线程:封装 RPC 风格的 Worker 通信 class WorkerRPC { constructor(url) { this.worker = new Worker(url); this.id = 0; this.pending = new Map(); this.worker.onmessage = (e) => { const { id, result, error } = e.data; const { resolve, reject } = this.pending.get(id); this.pending.delete(id); error ? reject(new Error(error)) : resolve(result); }; } call(method, params) { return new Promise((resolve, reject) => { const id = ++this.id; this.pending.set(id, { resolve, reject }); this.worker.postMessage({ id, method, params }); }); } } // 使用 const rpc = new WorkerRPC('worker.js'); const sorted = await rpc.call('sort', { data: largeArray });
javascript
// worker.js:处理 RPC 调用 const handlers = { sort: ({ data }) => data.sort((a, b) => a - b), filter: ({ data, condition }) => data.filter(condition), }; self.onmessage = async (e) => { const { id, method, params } = e.data; try { const result = await handlers[method](params); self.postMessage({ id, result }); } catch (err) { self.postMessage({ id, error: err.message }); } };

这样主线程就可以 await rpc.call('sort', data) 了,比裸写 postMessage + onmessage 干净很多。

SharedArrayBuffer:真正的共享内存

结构化克隆和 Transferable 本质上还是"传数据",有拷贝或转移开销。如果你要的是两个线程同时读写同一块内存,用 SharedArrayBuffer。

javascript
// 主线程:创建共享内存 const shared = new SharedArrayBuffer(1024); const view = new Int32Array(shared); worker.postMessage({ shared }); // Worker:直接读写同一块内存 self.onmessage = (e) => { const view = new Int32Array(e.data.shared); // 用 Atomics 做原子操作,避免竞态 Atomics.add(view, 0, 1); Atomics.store(view, 1, 42); };

关键点:共享内存没有自动同步机制,必须用 Atomics API 做原子操作,否则两个线程同时写一个位置,数据就乱了。Atomics 提供了 addsubcompareExchangewait/notify 等操作,基本够用。

注意:SharedArrayBuffer 有安全限制,服务端必须返回 Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp 两个响应头,否则浏览器直接拒绝。很多开发者在本地调试时发现能用,部署到生产环境就不行,就是这个头没配。

其他通信通道

除了 postMessage,还有几个不太常见但特定场景好用的通信方式:

MessageChannel:创建一对互相连接的端口,可以传给 Worker 作为私有通道。适合多个 Worker 之间直接通信,不经过主线程中转。

javascript
const channel = new MessageChannel(); worker1.postMessage({ port: channel.port1 }, [channel.port1]); worker2.postMessage({ port: channel.port2 }, [channel.port2]); // 两个 Worker 现在可以直接通信了

BroadcastChannel:同源下所有标签页和 Worker 都能收发的广播通道。适合跨标签页同步状态。

javascript
const bc = new BroadcastChannel('app-sync'); bc.postMessage({ type: 'data-updated', payload: newData }); bc.onmessage = (e) => { /* 收到其他页面的广播 */ };

通信性能的实际影响

很多人以为 Worker 通信开销可以忽略,实际上结构化克隆的耗时跟数据量正相关。实测数据:

数据量结构化克隆耗时Transferable 耗时
10KB~0.1ms~0.05ms
1MB~5ms~0.1ms
10MB~50ms~0.2ms
100MB~500ms~0.5ms

数据量越大,结构化克隆越慢,Transferable 优势越明显。10MB 以上的数据,不用 Transferable 基本等于白用 Worker——拷贝时间比计算时间还长。

实践建议:如果 Worker 间通信频率高(每秒几十次以上),即使单次数据量小,也要考虑 SharedArrayBuffer + Atomics,省掉反复序列化的开销。

错误处理别忘了

Worker 内部抛出的异常不会冒泡到主线程,必须显式监听:

javascript
worker.onerror = (e) => { console.error('Worker 出错了:', e.message); console.error('文件:', e.filename, '行号:', e.lineno); // 可以选择重新创建 Worker }; // Worker 内部也要处理异常 self.onmessage = (e) => { try { const result = riskyOperation(e.data); self.postMessage({ id: e.data.id, result }); } catch (err) { self.postMessage({ id: e.data.id, error: err.message }); } };

生产环境里 Worker 挂了不重启,等于你的后台任务全停了。建议封装一个自动重启的 Worker 管理器:onerror 触发后 terminate 旧 Worker,new 一个新的,再把未完成的任务重放一遍。

标签:Web Worker