什么是MessageChannel

什么是MessageChannel

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


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

核心:

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

核心概念与代码说明

1. MessageChannel 本质

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

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

2. 关键 API 解析

API 作用
new MessageChannel() 创建通信通道,生成 port1 和 port2
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. port1 和 port2 是 一一对应 的,不能混用其他端口
  2. 必须调用 port.start() 启用通信(Worker 环境中可省略,因为 onmessage 会自动启用)
  3. 发送的数据会被结构化克隆算法序列化,支持大部分数据类型(如对象、数组、日期等),但不支持函数、DOM 元素等

在 Web Worker 通信中使用 MessageChannel

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

核心原理

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

步骤示例

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

javascript 复制代码
// 创建消息通道
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)

javascript 复制代码
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)

xml 复制代码
<!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') 方式示例

html 复制代码
<!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>
相关推荐
deli00739 分钟前
手机拍灯为什么有一条条黑纹?PWM 240Hz 实测
前端
wangruofeng5 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng6 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈9 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技9 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569159 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马11 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng12 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding13 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天13 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron