Skip to content

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 的引用)
};
属性类型说明
dataany消息内容
originstring来源域名
lastEventIdstringSSE 事件 ID
sourceobject消息来源
portsMessagePort[]消息端口

详细说明

双向通信模式

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>

注意事项

  1. 数据是复制的:默认情况下 postMessage 复制数据,发送方和接收方各自持有独立副本
  2. 大数据使用 Transferable:大块 ArrayBuffer 应使用转移方式
  3. 避免频繁小消息:消息传递有开销,应尽量批量发送
  4. 结构化克隆有局限:函数、DOM、Symbol 等无法传递

最佳实践

  1. 使用请求-响应封装简化双向通信
  2. 大数据传输优先使用 Transferable Objects
  3. 为消息添加 typeaction 字段区分不同操作
  4. 使用消息 ID 匹配请求和响应

下一节

继续学习:SharedWorker

参考链接