什么是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 解耦通信
注意事项
-
port1和port2是 一一对应 的,不能混用其他端口 - 必须调用
port.start()启用通信(Worker 环境中可省略,因为onmessage会自动启用) - 发送的数据会被结构化克隆算法序列化,支持大部分数据类型(如对象、数组、日期等),但不支持函数、DOM 元素等
在 Web Worker 通信中使用 MessageChannel
可以实现更灵活的双向通信,尤其适合需要在多个上下文(主线程与 Worker 或多个 Worker 之间)建立独立通信通道的场景。以下是具体使用方法:
核心原理
MessageChannel 会创建两个相互关联的 MessagePort(端口):port1 和 port2。发送到 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>
关键说明
端口传递:
必须通过postMessage的第二个参数(transferList)传递MessagePort,且传递后原上下文将失去该端口的控制权(端口被转移)。-
通信方向:
- 主线程通过
port1发送消息,Worker 通过port2接收; - Worker 通过
port2发送消息,主线程通过port1接收。
- 主线程通过
多通道支持:
可创建多个MessageChannel实现并行通信(如不同功能模块使用独立通道)。关闭通道:
通信结束后可调用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>