MessageChannel 与 MessagePort:私有双向管道
基于 WHATWG HTML(跨文档消息 / 通道消息 / 广播频道)现行标准与各浏览器 Baseline 状态 · 核于 2026-07
速查
- 定位:
MessageChannel建一条私有双向管道,两端是一对MessagePort;把一端转移给<iframe>/ Worker,两个上下文即可点对点直连,不再挤在同一个window.onmessage。 - 创建:
const { port1, port2 } = new MessageChannel();——两个 port 由管道连通(entangled),一端发、另一端收。 - 收发:
port.postMessage(data, transfer?)发;port.onmessage = (e) => e.data或port.addEventListener("message", ...)收。 port.start()坑(本页核心):MDN 原文「only needed when usingEventTarget.addEventListener; it is implied when usingonmessage」——用addEventListener("message")必须显式port.start(),只有onmessage =赋值才隐式启动。- 为什么:端口的消息队列初始是禁用(disabled)的;
start()或首次设onmessage才启用,之后消息才被派发。漏掉start()= 一条都收不到、且不报错。 - 转移 port:
目标.postMessage(初始数据, targetOrigin/transfer, [port2])——第三参数(窗口)或第二参数(Worker)的 transfer 列表放port2。 - 接收 port:对端在
message事件里从event.ports[0]取出转移来的 port,再挂onmessage(或addEventListener+start())。 - 端口独属一个上下文:
MessagePort是 Transferable,转移后原上下文里那个 port 立即失效(neutered),只能在新宿主用——一个 port 同一时刻只属于一个上下文。 window.postMessagevsport.postMessage:前者带targetOrigin且撞在全局window.onmessage;后者只在这对 port 间流动、无targetOrigin参数(管道已私有)。close():port.close()断开端口、停止收发、利于回收;一端关另一端发消息不再送达。messageerror事件:收到无法反序列化(结构化克隆失败)的消息时触发,与message分开处理。- 典型用途:给每个 Worker 任务开独立通道多路复用、让两个
<iframe>/ 两个 Worker 不经主线程中转直连、RPC 库(如 Comlink)的底座。 - 结构化克隆 + transfer:
port.postMessage同样走结构化克隆,也支持第二参数 transfer 零拷贝移交(含再转移别的 port)。 - 多次转移:port 可被再转移(A→B→C),每次转移后前一持有者失效;但同一时刻只能有一个活跃持有者。
- Baseline:
MessageChannel/MessagePort自 2015-09 起 Baseline Widely available,Worker 内也可用。
一、为什么要 MessageChannel:从「公共广场」到「专线」
window.postMessage 的所有消息都撞在接收窗口同一个 window.onmessage 上——多个来源、多种消息混在一起,得靠 origin + type 分拣,像在公共广场喊话。
MessageChannel 给你一条专线:new MessageChannel() 一次造出一对用管道连通的 MessagePort(port1 / port2)。留一端自用、把另一端转移给对端,之后两端各用自己的 port 收发,消息只在这对 port 之间流动,互不干扰。好处:
- 多路复用:给每个子任务 / 每个 Worker 请求开一条独立通道,天然隔离。
- 直连:把两端分别交给两个
<iframe>(或两个 Worker),它们可不经主线程中转直接通信。 - RPC 底座:请求 / 响应式封装(Comlink 等)就建在成对 port 上。
// 一次造出一对连通的端口
const channel = new MessageChannel();
channel.port1; // 我方留用
channel.port2; // 待转移给对端二、收发:postMessage 与 onmessage
同一个页面内先感受收发(两端都在本上下文,仅演示 API):
const { port1, port2 } = new MessageChannel();
// port2 端收
port2.onmessage = (event) => {
console.log("port2 收到:", event.data);
port2.postMessage("port2 回复:已收到");
};
// port1 端收
port1.onmessage = (event) => {
console.log("port1 收到:", event.data);
};
// port1 端发——消息只会到 port2,不会撞到别处
port1.postMessage("port1 发:你好");与 window.postMessage 的关键差别:
port.postMessage(data)没有targetOrigin参数——管道已经是私有专线,不需要再限定收方源。- 消息只在这一对 port 间流动,不会广播到
window.onmessage。
三、port.start():本页头号坑
MessagePort 的消息队列初始是禁用的(WHATWG 原文:「A port message queue can be enabled or disabled, and is initially disabled」)。队列没启用,消息进不来。启用它有两种方式,行为不同:
// 方式 A:赋值 onmessage —— 隐式启用队列(等同自动调用了 start())
port.onmessage = (e) => console.log(e.data); // ✅ 立刻开始收
// 方式 B:addEventListener —— 必须显式 start(),否则永远收不到
port.addEventListener("message", (e) => console.log(e.data));
port.start(); // ⭐ 少了这一行,回调一次都不触发,且不报错MDN 对 start() 的原话(直译):「Starts the sending of messages queued on the port(only needed when using EventTarget.addEventListener; it is implied when using onmessage)」。WHATWG 补充:「The first time a MessagePort object's onmessage IDL attribute is set, the port's port message queue must be enabled, as if the start() method had been called」。
一句话记牢:
port.onmessage = fn→ 自动start(),直接能收。port.addEventListener("message", fn)→ 必须手动port.start()。
这是「代码看着没错、一条消息都收不到、又不报任何错」的经典排查点——用 addEventListener 风格时,检查是否漏了 start()。
四、把 port 转移出去:建立真正的跨上下文直连
单页内的一对 port 意义不大;MessageChannel 的威力在于把一端转移给另一个上下文。转移用 postMessage 的 transfer 列表(Transferable 机制):
4.1 主页面 ↔ <iframe> 直连
// 父页面:造通道,留 port1,把 port2 转移给 <iframe>
const iframe = document.querySelector("iframe");
const channel = new MessageChannel();
// 我方用 port1 收发(onmessage 隐式 start)
channel.port1.onmessage = (event) => {
console.log("父页面经专线收到:", event.data);
};
iframe.addEventListener("load", () => {
// 用一次 window.postMessage 把 port2 转移过去:
// 第二参数是 targetOrigin,第三参数 transfer 列表放 port2
iframe.contentWindow.postMessage("端口交付", "https://widget.example", [channel.port2]);
});
// 之后就用 port1 发,走专线,不再撞全局 window.onmessage
channel.port1.postMessage({ type: "ping" });// <iframe> 侧:从 event.ports[0] 取出转移来的 port2
window.addEventListener("message", (event) => {
if (event.origin !== "https://host.example") return; // 转移这一步仍要校验来源
const port = event.ports[0]; // 拿到 port2
port.onmessage = (e) => {
// onmessage 赋值 → 隐式 start,直接能收
console.log("<iframe> 经专线收到:", e.data);
port.postMessage({ type: "pong" });
};
});要点:
- 转移那一次
window.postMessage仍要遵守 postMessage 安全——targetOrigin写明、接收端校验event.origin(见 postMessage 页)。握手完成后,专线上的后续消息才不必再带targetOrigin。 - 对端从
event.ports[0]取 port(ports是数组,转移几个取几个)。
4.2 主页面 ↔ Worker(及 Worker ↔ Worker 直连)
Worker 的 postMessage 签名是 postMessage(message, transfer?)(无 targetOrigin),transfer 是第二参数:
// 主线程:把 port2 交给 worker
const worker = new Worker("worker.js");
const channel = new MessageChannel();
channel.port1.onmessage = (e) => console.log("主线程收到:", e.data);
worker.postMessage({ type: "port" }, [channel.port2]); // 第二参数 transfer
channel.port1.postMessage("主线程经专线问候");// worker.js
self.onmessage = (event) => {
const port = event.ports[0];
port.onmessage = (e) => {
console.log("worker 经专线收到:", e.data);
port.postMessage("worker 已回复");
};
};把两个 port 分别交给两个不同的 Worker,它们就能绕过主线程直接通信——主线程只负责牵线、不做转发中转,这是 MessageChannel 相对纯 postMessage 的独特能力。
五、端口只属一个上下文:转移即失效
MessagePort 是 Transferable 对象。转移的本质是移交所有权——WHATWG / MDN 原义:「A port, after it is sent, can no longer be used by the original context」(端口一经发送,原上下文即不可再用它)。
const channel = new MessageChannel();
iframe.contentWindow.postMessage("交付", "*", [channel.port2]);
// ❌ port2 已转移,本上下文里它已 neutered(失效)
channel.port2.postMessage("still here?"); // 抛错 / 无效推论:
- 一个 port 同一时刻只属于一个上下文——转移给谁,就只有谁能用。
- port 可以多次接力转移(A→B→C),但每转移一次,前一持有者立即失效;任一时刻只有一个活跃持有者。
- 这套语义和
postMessage转移ArrayBuffer(转移后byteLength变 0)完全一致——都是零拷贝移交所有权。
六、close 与 messageerror
port.close():断开这一端,停止收发,利于垃圾回收。一端close后,另一端再postMessage不会送达(也不报错)。通道用完(一次性 RPC 完成、组件卸载)应主动close,避免端口悬挂。messageerror事件:当收到一条无法反序列化(结构化克隆失败)的消息时,在 port(或window)上触发,与正常message分开:
port.addEventListener("message", (e) => handle(e.data));
port.addEventListener("messageerror", (e) => {
console.error("收到无法反序列化的消息", e); // 单独处理坏消息
});
port.start(); // addEventListener 风格:别忘了 start()七、易错点
addEventListener("message")后漏port.start():一条都收不到且不报错——要么改用port.onmessage =(隐式 start),要么显式补port.start()。- 转移后还用原 port:port 转移即 neutered——转移前把要发的先发,转移后只用新宿主里的 port。
- 对端忘了从
event.ports[0]取 port:转移来的 port 不在event.data里,在event.ports——按数组下标取。 - 给
port.postMessage传targetOrigin:port 版没有这个参数(第二参数是 transfer 列表)——targetOrigin只属于window.postMessage。 - 转移 port 的那次 window.postMessage 用
*/ 不校验来源:握手这一步仍是普通 postMessage,安全规则照旧(见 postMessage 页)。 - 通道用完不
close:端口悬挂、内存不回收——一次性通道用完主动close。 - 误以为
<iframe>一 append 就能收 port:仍要等其脚本就绪——转移前等load或做ready握手。 - 把请求 / 响应当同步:port 通信是异步消息,没有内建 RPC——自己配 id 匹配 + 超时,或上 Comlink(见 Web Workers 叶的 Comlink 模式)。
下一页:从「点对点专线」转到「同源一对多广播」——BroadcastChannel。