MessageChannel 详解
一、它是什么?
MessageChannel是 HTML5 规范 中定义的一个 Web API,用来创建一条双向、独立的消息通信管道 。这条管道由两个互相关联的端口(MessagePort) 组成------两端各持一个端口,通过 port.postMessage()发消息,port.onmessage收消息。
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
// A 端监听
port1.onmessage = (e) => {
console.log('port1 收到:', e.data);
};
// B 端发送
port2.postMessage('你好,来自 port2');
本质上它就是一个封装好的异步消息总线 ,比裸
postMessage更结构化,且天然支持端口转移(port transfer)。
二、核心机制拆解
| 概念 | 说明 |
|---|---|
new MessageChannel() |
创建一对绑定的端口:port1和 port2 |
port.postMessage(data[, transfer]) |
发送消息,数据走结构化克隆算法(可传对象、Map、Set、Date 等,不限于 JSON) |
port.onmessage = fn/ addEventListener('message', fn) |
接收消息 |
port.start() |
显式启动端口(用 onmessage赋值时会自动 start,手写 addEventListener时需手动调) |
port.close() |
关闭端口,释放资源 |
transfer参数 |
可把某些对象(如另一个 MessagePort、ArrayBuffer)移交所有权,移交后原端不能再使用 |
与普通 window.postMessage的关键区别
window.postMessage |
MessageChannel |
|
|---|---|---|
| 是否需要"全局目标" | 需要一个具体窗口/iframe 作为 target | 只需两个端口,不依赖 window |
| 事件是否混在一起 | 所有消息都走同一个 message事件,需要自己区分来源 |
独立通道、独立端口,天然隔离 |
| 能否转移端口 | 可以,但用法更绕 | 核心设计就是为端口转移而生 |
| 典型用途 | 跨 frame / 跨 origin 通信 | 任意两端持有端口的地方:frame、worker、甚至同一上下文内做解耦 |
三、典型使用场景
场景 1:父页面 ↔ iframe 安全通信(最经典)
传统 window.postMessage的问题是:所有人都能监听到全局 message 事件 ,你得靠 event.origin、event.source做校验,容易出错。
MessageChannel的做法更干净------建立一条专属通道:
// 父页面
const iframe = document.querySelector('iframe');
const channel = new MessageChannel();
// 把 port2 交给 iframe(通过一次 postMessage 送出)
iframe.contentWindow.postMessage('init-channel', '*', [channel.port2]);
// 父端从 port1 收发
channel.port1.onmessage = (e) => {
console.log('iframe 回:', e.data);
};
channel.port1.postMessage('父页面说:开始');
// ===== iframe 内部 =====
window.addEventListener('message', (e) => {
if (e.data === 'init-channel') {
const port = e.ports[0]; // 拿到 port2
port.onmessage = (ev) => {
console.log('父页面说:', ev.data);
port.postMessage('收到!');
};
}
});
✅ 好处:iframe 拿到的只是这个端口,不需要再去猜"哪个 window 发的",也不会把消息泄漏给别的 listener。
场景 2:与主线程 ↔ Web Worker 通信(不只是 data,还能传"通道")
Worker 默认就用 postMessage通信,但 MessageChannel的价值在于:
-
你可以把 一个 port 传给 Worker,让 Worker 持有它做回调式通信
-
或者建立多条逻辑通道而非把所有消息塞进一条线
// main.js
const worker = new Worker('worker.js');
const channel = new MessageChannel();worker.postMessage({ type: 'bind-channel' }, [channel.port2]);
channel.port1.onmessage = (e) => {
console.log('worker 推送:', e.data);
};
channel.port1.postMessage('ping');// worker.js
self.onmessage = (e) => {
if (e.data?.type === 'bind-channel') {
const port = e.ports[0];
port.onmessage = (ev) => console.log('main 说:', ev.data);
port.postMessage('worker ready');
}
};
场景 3:同一上下文内的"解耦通信"(发布订阅替代方案)
即使不涉及 iframe / worker ,你也可以用 MessageChannel在两个模块之间搭一条异步通道:
// event-bus.js
const { port1, port2 } = new MessageChannel();
// 订阅者端
port1.onmessage = (e) => {
console.log('收到事件:', e.data);
};
// 导出端口给不同模块
export const emitter = {
send: (msg) => port2.postMessage(msg),
};
export const listenerPort = port1;
这在一些库/框架里用来模拟安全的异步消息传递,避免直接共享可变状态。
场景 4:利用它产生一个"可控的宏任务"(偏底层技巧)
port.postMessage(null)+ port.onmessage的执行时机在 浏览器事件循环中的宏任务阶段(类似 setTimeout 但不受最小延迟限制),所以有时会看到它被用于调度:
function nextTickMacro(fn) {
const { port1, port2 } = new MessageChannel();
port1.onmessage = () => fn();
port2.postMessage(null);
}
Vue 2 的早期版本就在某些环境里用过类似思路(
setImmediate不可用时的兜底)。
四、一句话总结
MessageChannel的本质:创建一对互连的MessagePort,用结构化克隆安全地传数据,尤其擅长在 iframe / Worker 等"边界"之间建立隔离、可转移的专属通信通道。
如果你告诉我你当前的实际使用背景(比如是在处理 iframe 通信?Worker?还是在研究微前端/沙箱隔离?),我可以帮你给出更贴合你场景的设计方案和注意事项(端口生命周期、close 时机、多次 init 防重复绑定等)。