Worker 通信(postMessage)
Web Worker 通过 postMessage 方法实现主线程与 Worker 线程之间的双向通信。postMessage 支持传递多种数据类型,包括字符串、数字、对象、数组和某些可转移对象(Transferable Objects)。本节深入讲解消息传递的各种方式、结构化克隆算法和高效数据传输技巧。
前置知识
阅读本节前,建议先了解:Worker 基础
基础概念
消息传递模型
Worker 通信采用异步消息传递模型,数据通过序列化复制在两个线程之间传递,而非共享引用。
主线程 Worker 线程
│ │
├── postMessage(data) ─────────→│ onmessage 接收
│ │
│←───────── postMessage(result) ─┤
│ │
onmessage 接收 │支持的数据类型
| 类型 | 支持情况 | 说明 |
|---|---|---|
| 基本类型 | 支持 | 字符串、数字、布尔、null、undefined |
| 对象 | 支持 | 普通对象、数组 |
| Date | 支持 | 日期对象 |
| RegExp | 支持 | 正则表达式 |
| Blob | 支持 | 二进制大对象 |
| File | 支持 | 文件对象 |
| FileList | 支持 | 文件列表 |
| ArrayBuffer | 支持(可转移) | 可零拷贝传输 |
| ImageData | 支持 | Canvas 像素数据 |
| Map/Set | 支持 | 集合类型 |
| 函数 | 不支持 | 不可序列化 |
| DOM 节点 | 不支持 | 不可序列化 |
| Symbol | 不支持 | 不可序列化 |
语法与 API
主线程发送消息
javascript
const worker = new Worker('worker.js');
// 发送字符串
worker.postMessage('Hello Worker');
// 发送数字
worker.postMessage(42);
// 发送对象
worker.postMessage({
type: 'calculate',
data: [1, 2, 3, 4, 5],
options: { method: 'sum' }
});
// 发送数组
worker.postMessage([10, 20, 30, 40, 50]);
// 发送带有多种类型的数据
worker.postMessage({
action: 'process',
payload: {
text: 'Hello',
numbers: [1, 2, 3],
date: new Date(),
buffer: new ArrayBuffer(1024)
}
});Worker 线程发送消息
javascript
// worker.js
self.onmessage = function(event) {
const data = event.data;
console.log('收到:', data);
// 处理数据
const result = processData(data);
// 发送结果回主线程
self.postMessage({
status: 'success',
result: result
});
};
function processData(data) {
if (typeof data === 'number') {
return data * 2;
}
return 'processed: ' + JSON.stringify(data);
}MessageEvent 对象
javascript
// 主线程和 Worker 线程都接收 MessageEvent
worker.onmessage = function(event) {
console.log(event.data); // 消息数据
console.log(event.origin); // 消息来源(Worker 中为空字符串)
console.log(event.ports); // 消息端口(MessagePort 数组)
console.log(event.source); // 消息源(Worker 的引用)
};| 属性 | 类型 | 说明 |
|---|---|---|
data | any | 消息内容 |
origin | string | 来源域名 |
lastEventId | string | SSE 事件 ID |
source | object | 消息来源 |
ports | MessagePort[] | 消息端口 |
详细说明
双向通信模式
javascript
// 主线程
const worker = new Worker('worker.js');
// 发送请求并等待响应
function sendToWorker(data) {
return new Promise((resolve) => {
worker.onmessage = (event) => {
resolve(event.data);
};
worker.postMessage(data);
});
}
// 使用
sendToWorker({ action: 'calculate', numbers: [1, 2, 3] })
.then(result => console.log('Worker 返回:', result));javascript
// worker.js - 支持多种操作
self.onmessage = function(event) {
const { action, payload } = event.data;
switch (action) {
case 'add':
self.postMessage({ action, result: payload.a + payload.b });
break;
case 'sort':
self.postMessage({ action, result: [...payload].sort((a, b) => a - b) });
break;
case 'encrypt':
self.postMessage({ action, result: btoa(JSON.stringify(payload)) });
break;
default:
self.postMessage({ action: 'unknown', error: '未知操作' });
}
};请求-响应模式封装
javascript
// 主线程 - 请求响应封装
class WorkerClient {
constructor(workerUrl) {
this.worker = new Worker(workerUrl);
this.callbacks = new Map();
this.nextId = 0;
this.worker.onmessage = (event) => {
const { id, result, error } = event.data;
const callback = this.callbacks.get(id);
if (callback) {
if (error) {
callback.reject(new Error(error));
} else {
callback.resolve(result);
}
this.callbacks.delete(id);
}
};
}
send(action, payload) {
const id = this.nextId++;
return new Promise((resolve, reject) => {
this.callbacks.set(id, { resolve, reject });
this.worker.postMessage({ id, action, payload });
});
}
terminate() {
this.worker.terminate();
this.callbacks.clear();
}
}
// 使用
const client = new WorkerClient('worker.js');
client.send('add', { a: 10, b: 20 }).then(console.log); // { result: 30 }
client.send('sort', [5, 3, 1, 4, 2]).then(console.log); // { result: [1,2,3,4,5] }Transferable Objects(零拷贝传输)
某些对象可以通过"转移"方式在线程间传递,实现零拷贝传输,避免大数据的序列化开销。
javascript
// 转移 ArrayBuffer(零拷贝)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
// 普通传递(复制数据,较慢)
worker.postMessage({ buffer: buffer }); // 复制 1MB
// 转移传递(零拷贝,极快)
worker.postMessage(buffer, [buffer]); // 转移后主线程的 buffer 变为空javascript
// 完整的 Transferable 示例
// 主线程
const largeBuffer = new ArrayBuffer(10 * 1024 * 1024); // 10MB
const view = new Int32Array(largeBuffer);
for (let i = 0; i < view.length; i++) {
view[i] = i * 2;
}
console.log('转移前 buffer 字节长度:', largeBuffer.byteLength); // 10485760
// 转移 ArrayBuffer
const worker = new Worker('transfer-worker.js');
worker.postMessage(
{ type: 'process', buffer: largeBuffer },
[largeBuffer] // 第二个参数指定要转移的对象
);
console.log('转移后 buffer 字节长度:', largeBuffer.byteLength); // 0(已被转移)javascript
// transfer-worker.js
self.onmessage = function(event) {
const { buffer } = event.data;
const view = new Int32Array(buffer);
console.log('收到的 buffer 字节长度:', buffer.byteLength); // 10485760
console.log('前 5 个元素:', view.slice(0, 5)); // [0, 2, 4, 6, 8]
// 处理完成后可以转移回去
self.postMessage({ result: view.reduce((a, b) => a + b, 0) });
};可转移对象列表
| 对象 | 可转移 | 说明 |
|---|---|---|
ArrayBuffer | 是 | 最常用 |
MessagePort | 是 | 用于通道通信 |
ReadableStream | 是 | 可读流 |
WritableStream | 是 | 可写流 |
TransformStream | 是 | 转换流 |
OffscreenCanvas | 是 | 离屏 Canvas |
结构化克隆算法
postMessage 使用结构化克隆算法(Structured Clone)序列化数据:
javascript
// 结构化克隆支持的类型
const complexData = {
// 基本类型
string: 'Hello',
number: 42,
boolean: true,
null: null,
undefined: undefined,
// 复杂类型
nested: { a: 1, b: { c: 2 } },
array: [1, 2, 3],
date: new Date(),
regex: /test/gi,
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
// 二进制类型
typedArray: new Int32Array([1, 2, 3]),
blob: new Blob(['hello'], { type: 'text/plain' }),
dataView: new DataView(new ArrayBuffer(16))
};
worker.postMessage(complexData);实战示例
图片处理 Worker
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Worker 图片处理</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; }
.card {
max-width: 600px; margin: 0 auto; padding: 24px;
background: white; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 { text-align: center; }
.controls { display: flex; gap: 10px; margin: 16px 0; flex-wrap: wrap; }
.btn {
padding: 10px 20px; border: none; border-radius: 8px;
cursor: pointer; font-size: 13px; font-weight: 600;
color: white; background: #1a73e8;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.preview {
display: flex; gap: 16px; margin-top: 16px; flex-wrap: wrap;
}
.preview img {
max-width: 250px; border-radius: 8px;
border: 2px solid #e0e0e0;
}
.status { margin-top: 12px; padding: 8px; background: #f0f0f0; border-radius: 6px; font-size: 13px; }
</style>
</head>
<body>
<div class="card">
<h2>Worker 图片灰度处理</h2>
<input type="file" id="fileInput" accept="image/*">
<div class="controls">
<button class="btn" id="grayBtn" disabled onclick="processImage('gray')">灰度化</button>
<button class="btn" id="invertBtn" disabled onclick="processImage('invert')">反色</button>
<button class="btn" id="sepiaBtn" disabled onclick="processImage('sepia')">棕褐色</button>
</div>
<div class="status" id="status">请选择一张图片</div>
<div class="preview">
<div><p>原图</p><canvas id="srcCanvas" style="display:none;"></canvas><img id="srcImg"></div>
<div><p>处理结果</p><canvas id="dstCanvas" style="display:none;"></canvas><img id="dstImg"></div>
</div>
</div>
<script>
let imageData = null;
let worker = null;
// 创建图片处理 Worker
const workerCode = `
self.onmessage = function(e) {
const { type, data, width, height } = e.data;
const pixels = new Uint8ClampedArray(data);
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i], g = pixels[i+1], b = pixels[i+2];
if (type === 'gray') {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
pixels[i] = pixels[i+1] = pixels[i+2] = gray;
} else if (type === 'invert') {
pixels[i] = 255 - r;
pixels[i+1] = 255 - g;
pixels[i+2] = 255 - b;
} else if (type === 'sepia') {
pixels[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
pixels[i+1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
pixels[i+2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
}
}
// 转移处理后的 buffer 回去
const buffer = pixels.buffer;
self.postMessage({ buffer }, [buffer]);
};
`;
// 文件选择
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
const canvas = document.getElementById('srcCanvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
imageData = ctx.getImageData(0, 0, img.width, img.height);
document.getElementById('srcImg').src = canvas.toDataURL();
document.querySelectorAll('.btn').forEach(b => b.disabled = false);
document.getElementById('status').textContent = `图片已加载: ${img.width}x${img.height}`;
};
img.src = URL.createObjectURL(file);
});
// 处理图片
function processImage(type) {
if (!worker) {
worker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' })));
}
document.getElementById('status').textContent = '正在处理...';
worker.onmessage = (e) => {
const { buffer } = e.data;
const pixels = new Uint8ClampedArray(buffer);
const canvas = document.getElementById('dstCanvas');
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext('2d');
const newImageData = new ImageData(pixels, imageData.width, imageData.height);
ctx.putImageData(newImageData, 0, 0);
document.getElementById('dstImg').src = canvas.toDataURL();
document.getElementById('status').textContent = `${type} 处理完成`;
};
// 复制数据并转移
const bufferCopy = imageData.data.buffer.slice(0);
worker.postMessage({
type,
data: bufferCopy,
width: imageData.width,
height: imageData.height
}, [bufferCopy]);
}
</script>
</body>
</html>注意事项
- 数据是复制的:默认情况下
postMessage复制数据,发送方和接收方各自持有独立副本 - 大数据使用 Transferable:大块
ArrayBuffer应使用转移方式 - 避免频繁小消息:消息传递有开销,应尽量批量发送
- 结构化克隆有局限:函数、DOM、Symbol 等无法传递
最佳实践
- 使用请求-响应封装简化双向通信
- 大数据传输优先使用 Transferable Objects
- 为消息添加
type或action字段区分不同操作 - 使用消息 ID 匹配请求和响应
下一节
继续学习:SharedWorker