MessageChannel

什么是MessageChannel

MessageChannel允许我们在不同的浏览上下文,比如window.open()打开的窗口或者iframe等之间建立通信管道,并通过两端的端口(port1和port2)发送消息。MessageChannel以DOM Event的形式发送消息,所以它属于异步的宏任务。


以下是 MessageChannel(端口通信) 的完整示例代码,清晰展示 port1port2 的双向通信特性,可直接复制到在线 HTML 编辑器(如 CodePen、JSFiddle)中执行:

核心:

  • port1.start(); port2.start();
  • port1.postMessage(...)port2.addEventListener('message', (e) => {...});
  • port2.postMessage(...)port1.addEventListener('message', (e) => {...});

核心概念与代码说明

1. MessageChannel 本质

MessageChannel 是浏览器提供的 双向通信机制,创建后会自动生成两个关联的端口:

  • port1port2成对存在 的,相互绑定
  • 数据通过 postMessage 发送,通过 message 事件接收
  • 通信是 双向的:port1 可给 port2 发,port2 也可给 port1 发

2. 关键 API 解析

API 作用
new MessageChannel() 创建通信通道,生成 port1port2
port.postMessage(data) 发送数据(data 可是字符串、对象等)
port.addEventListener('message', e => {}) 监听接收消息(e.data 是收到的数据)
port.start() 启用端口(必须调用,否则无法接收消息)

3. 运行效果

  • 在左侧 Port1 输入框输入内容,点击“发送到 port2”,右侧 Port2 会收到消息
  • 在右侧 Port2 输入框输入内容,点击“发送到 port1”,左侧 Port1 会收到消息
  • 日志区域会实时显示发送/接收记录,包含时间戳

4. 实际应用场景

  • iframe 跨域通信:父页面和子 iframe 可通过 port 传递数据(无需处理跨域限制)
  • Web Worker 通信:主线程和 Worker 线程的双向数据交互(比 postMessage 原生用法更清晰)
  • 组件间通信:复杂应用中,无直接关联的组件可通过 MessageChannel 解耦通信

注意事项

  1. port1port2一一对应 的,不能混用其他端口
  2. 必须调用 port.start() 启用通信(Worker 环境中可省略,因为 onmessage 会自动启用)
  3. 发送的数据会被结构化克隆算法序列化,支持大部分数据类型(如对象、数组、日期等),但不支持函数、DOM 元素等

在 Web Worker 通信中使用 MessageChannel

可以实现更灵活的双向通信,尤其适合需要在多个上下文(主线程与 Worker 或多个 Worker 之间)建立独立通信通道的场景。以下是具体使用方法:

核心原理

MessageChannel 会创建两个相互关联的 MessagePort(端口):port1port2。发送到 port1 的消息会被 port2 接收,反之亦然。通过将其中一个端口传递给 Worker,即可建立主线程与 Worker 之间的专属通信通道。

步骤示例

1. 主线程代码(main.js)

// 创建消息通道
const channel = new MessageChannel();
const { port1, port2 } = channel;

// 创建 Worker
const worker = new Worker('./worker.js', [port2]);

// 初始化消息发送, 向 Worker 发送 port2(需通过 postMessage 传递,且标记为可转移)
worker.postMessage({ type: 'init' }, [port2]);
// worker.onmessage = (msg) => { console.log('[worker -> msg: ', msg); };
// worker.onerror = (error) => { console.error('[worker -> error:', error); };

// 端口消息处理, 监听 port1 接收的消息(来自 Worker 的 port2)
port1.onmessage = (msg) => {
  console.log('[主线程收到: port2 + worker] -> port1 信息: ', msg.data);
  if (msg && msg?.data !== '') {
    // 可通过 port1 向 Worker 发送消息
    port1.postMessage({ type: 'info', data: 'hello friend!' });
  }
};

port1.onmessageerror = (error) => {
  console.error(error);
};

// 关键:页面卸载清理:在 window.onbeforeunload 中触发资源清理,确保页面关闭时释放资源
function cleanupResources() {
  console.log('------ cleanupResources -----')
  
  // 1. 先向 Worker 发送终止指令,触发其内部的 terminate 处理
  port1.postMessage({ type: 'terminate' });

  // 2. 关闭端口,移除事件监听器
  port1.close(); // 端口关闭:通过 port.close() 释放 MessageChannel 端口,避免事件监听器残留
  port2.close();

  // // 事件解绑:显式将 onmessage、onerror 设为 null,移除引用
  port1.onmessage = null;
  port1.onmessageerror = null;

  port2.onmessage = null;
  port2.onmessageerror = null;

  // 3. 终止 Worker 线程
  if (worker) {
    // Worker 终止:使用 worker.terminate()(主线程)或 self.close()(Worker 内部)终止线程
    // 终止 Worker 线程(浏览器环境中 terminate 是同步方法,无返回值)
    if (worker) {
      worker.terminate(); // 直接调用,无需 catch
    }
  }
}

// 页面卸载前清理
window.onbeforeunload = () => {
  cleanupResources();
};

// 如需主动终止(例如按钮点击),可调用 cleanupResources()

2. Worker 线程代码(worker.js)

let workerPort;

self.onmessage = function(event) {
  const { data, ports } = event;
  if (data.type === 'init') {
    workerPort = ports[0];
    workerPort.onmessage = handlePortMessage;
    workerPort.onerror = (error) => {
      console.error('worker port2: ', error);
    };

    workerPort.postMessage('ok port2 与 worker 握手了🤝');
  }
};

// 处理端口消息
function handlePortMessage(event) {
  const { type, data } = event.data;
  console.log('type = ', type, ', workerPort = ' ,workerPort);
  switch (type) {
    case 'info':
      if (data) {
        console.log('port1.postMessage -> [port2 in worker] 消息: ', data);
      }
      break;

    // 可添加终止指令处理
    case 'terminate':
      if (workerPort) {
        workerPort.close(); // 关闭端口
        workerPort.onmessage = null;
        workerPort.onerror = null;
      }

      self.close(); // 关闭 Worker 自身
      break;
  }
}

// 监听 Worker 错误
self.onerror = (err) => {
  console.error('Worker 错误:', err);
};

3. 测试页面代码 (test.html)

<!DOCTYPE html>
<html>
  <head>
    <title>Test MessageChannel + Worker + html</title>
    <script type="text/javascript" src="main.js"></script>
    <script type="text/javascript">
      window.onload = function(){
        console.log(typeof cleanupResources, '~~~~');
      }
    </script>
  </head>
  <body>
    <button onclick="cleanupResources()">CLEAR</button>
  </body>
</html>

关键说明

  1. 端口传递
    必须通过 postMessage 的第二个参数(transferList)传递 MessagePort,且传递后原上下文将失去该端口的控制权(端口被转移)。

  2. 通信方向

    • 主线程通过 port1 发送消息,Worker 通过 port2 接收;
    • Worker 通过 port2 发送消息,主线程通过 port1 接收。
  3. 多通道支持
    可创建多个 MessageChannel 实现并行通信(如不同功能模块使用独立通道)。

  4. 关闭通道
    通信结束后可调用 port1.close()port2.close() 释放资源。

优势

相比 Worker 自带的 postMessage 通信,MessageChannel 更适合:

  • 分离不同类型的通信(如业务数据、控制指令);
  • 实现多对多通信(多个 Worker 之间通过端口转发消息);
  • 避免消息混杂导致的逻辑混乱。

通过上述方式,即可利用 MessageChannel 在 Web Worker 中实现高效、隔离的双向通信。

addEventListener('message') 方式示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>MessageChannel port1 & port2 示例</title>
  <style>
    .container {
      display: flex;
      gap: 20px;
      margin: 20px;
    }
    .panel {
      flex: 1;
      padding: 20px;
      border: 1px solid #ddd;
      border-radius: 8px;
    }
    h3 {
      margin-top: 0;
      color: #333;
    }
    button {
      padding: 8px 16px;
      margin-top: 10px;
      cursor: pointer;
    }
    .log {
      margin-top: 15px;
      padding: 10px;
      background: #f5f5f5;
      border-radius: 4px;
      height: 150px;
      overflow-y: auto;
      font-size: 14px;
    }
  </style>
</head>
<body>
  <h2>MessageChannel 双向通信演示</h2>
  <div class="container">
    <!-- Port1 通信面板 -->
    <div class="panel">
      <h3>Port1 发送区</h3>
      <input type="text" id="port1Input" placeholder="输入要发送给 port2 的内容">
      <button id="port1SendBtn">发送到 port2</button>
      <div class="log" id="port1Log">
        <p>📥 Port1 接收日志:</p>
      </div>
    </div>

    <!-- Port2 通信面板 -->
    <div class="panel">
      <h3>Port2 发送区</h3>
      <input type="text" id="port2Input" placeholder="输入要发送给 port1 的内容">
      <button id="port2SendBtn">发送到 port1</button>
      <div class="log" id="port2Log">
        <p>📥 Port2 接收日志:</p>
      </div>
    </div>
  </div>

  <script>
    // 1. 创建 MessageChannel 实例(自动生成 port1 和 port2 两个端口)
    const channel = new MessageChannel();
    const { port1, port2 } = channel; // 解构出两个端口

    // 2. 工具函数:添加日志到指定面板
    function addLog(logElement, content) {
      const p = document.createElement('p');
      p.textContent = `[${new Date().toLocaleTimeString()}] ${content}`;
      logElement.appendChild(p);
      logElement.scrollTop = logElement.scrollHeight; // 自动滚动到底部
    }

    // 3. Port1 接收消息(监听 port1 的 message 事件)
    port1.addEventListener('message', (e) => {
      addLog(document.getElementById('port1Log'), `收到 port2 的消息:${e.data}`);
    });

    // 4. Port2 接收消息(监听 port2 的 message 事件)
    port2.addEventListener('message', (e) => {
      addLog(document.getElementById('port2Log'), `收到 port1 的消息:${e.data}`);
    });

    // 5. 关键:启用端口通信(必须调用 start(),否则无法接收消息)
    port1.start();
    port2.start();

    // 6. Port1 发送消息按钮事件
    document.getElementById('port1SendBtn').addEventListener('click', () => {
      const input = document.getElementById('port1Input');
      const message = input.value.trim();
      if (message) {
        port1.postMessage(message); // 通过 port1 发送消息(port2 接收)
        addLog(document.getElementById('port1Log'), `发送到 port2:${message}`);
        input.value = ''; // 清空输入框
      }
    });

    // 7. Port2 发送消息按钮事件
    document.getElementById('port2SendBtn').addEventListener('click', () => {
      const input = document.getElementById('port2Input');
      const message = input.value.trim();
      if (message) {
        port2.postMessage(message); // 通过 port2 发送消息(port1 接收)
        addLog(document.getElementById('port2Log'), `发送到 port1:${message}`);
        input.value = ''; // 清空输入框
      }
    });

    // 可选:支持按 Enter 键发送
    document.getElementById('port1Input').addEventListener('keydown', (e) => {
      if (e.key === 'Enter') document.getElementById('port1SendBtn').click();
    });
    document.getElementById('port2Input').addEventListener('keydown', (e) => {
      if (e.key === 'Enter') document.getElementById('port2SendBtn').click();
    });
  </script>
</body>
</html>
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容