最近在 GitHub 上”学习”的时候发现了别人在用 MessageChannel,这个 Api 很少见(没见过)基本不了解如何使用,搜了下 发现 React 和 Vue 都使用过~ 这不学习学习跟同事小装一手?

介绍 Message Channel

Message Channel 允许两个不同的脚本运行在同一个文档的不同浏览器上下文(例如两个 Iframe,文档主体和 Iframe,worker 之间或 window.open 的窗口)来直接通讯,在每一端使用一个端口(port)通过双向频道(Channel)来传递信息。

Message Channel 是以 Dom Event 的形式发送信息,属于异步的宏任务

使用

  • 使用 MessageChannel() 构造函数来创建通讯信息,获取两个端口的 Message Port 对象:Port1、Port2
  • 端口之间可以互相通信
  • 端口接受到无法被序列化的消息时,使用 onmessageerror 处理
  • 使用 close 关闭端口

示例

const { port1, port2 } = new MessageChannel();
port1.onmessage = function (event) {
console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
};
port2.onmessage = function (event) {
console.log('收到来自port1的消息:', event.data); // 收到来自port1的消息: ping
port2.postMessage('pong');
};
port1.postMessage('ping');

use addEventListener

const { port1, port2 } = new MessageChannel();
port1.addEventListener('message', function (event) {
console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
});
port1.start();
port2.addEventListener('message', function (event) {
console.log('收到来自port1的消息:', event.data); // 收到来自port1的消息: ping
port2.postMessage('pong');
});
port2.start();
port1.postMessage('ping');

使用 EventListener 需要手动调用 start() 后才可通信,因为初始化是暂停的。在使用 onmessage 时隐式调用了 start() 方法。

与 Iframe 通信

Main

const channel = new MessageChannel();
const { port1, port2 } = channel;

port1.onmessage = (event) => {
console.log(event.data, '----- port1 get message');
};

port2.onmessage = (event) => {
console.log(event.data, '------ port2 get message');
};

const iframe = document.querySelector('iframe');

iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage('from main -- postMessage', '*', [port1]);
});

Iframe

window.addEventListener('message', (event) => {
const { data, ports } = event;
const [port1] = ports || [];
console.log(data, 'iframe get message', event);
});

互相通信

Iframe

+ let messagePort;
window.addEventListener('message', (event) => {
const { data, ports } = event;
const [port1] = ports || [];
console.log(data, 'iframe get message', event);
+ messagePort = port1;
});

+button.onclick = () => {
// message channel
+ messagePort?.postMessage('message from iframe')
+}

此处的 port1 是来自:iframe.contentWindow.postMessage('xxxx', '*', [port1]);( postMessage 方法能够接受一个由 Transferable Objects 组成的数组作为参数,而 MessageChannel 导出的 MessagePort 刚好是 Transferable Objects )

Window.open

Main

let strWindowFeatures = 'top=10,left=10,width=400,height=200';
const newWindow = window.open(url, 'message', strWindowFeatures);

newWindow.addEventListener('load', () => {
newWindow.postMessage('message from new window', '*', [port1]);
});

new window
与 Iframe 逻辑大体一致

let messagePort;
window.addEventListener('message', (event) => {
const { data, ports } = event;
const [port1] = ports || [];
console.log(data, 'iframe get message', event);
messagePort = port1;
});

button.onclick = () => {
// message channel
messagePort?.postMessage('message from iframe');
};

Event Loop 中执行顺序

const interval = setInterval(() => {
console.log('setInterval');
window.clearInterval(interval);
});

setTimeout(() => {
console.log('timeout');
});

const { port1, port2 } = new MessageChannel();
port1.postMessage('send message');
port2.onmessage = () => {
console.log('get message');
};

requestAnimationFrame(() => console.log('requestAnimationFrame'));

new Promise((resolve) => {
console.log('promise');
resolve();
}).then(() => {
console.log('then');
});

async function A() {
console.log('A run');
await new Promise((resolve) => resolve());
console.log('A end');
}

A();

输出为

promise, A run
then, A end
requestAnimationFrame
timeout(先调先运行), get message, setInterval

至于 requestAnimationFrame 为何在宏任务之前,微任务之后的简单理解(不是宏任务的任务):

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

严格意义上,requestAnimationFrame 并不是一个宏任务:

  • 执行时机与宏任务完全不一致
  • requestAnimationFrame 任务队列被执行的时候,会将此刻队列中所有的任务都执行完

https://segmentfault.com/a/1190000042501046
https://zhuanlan.zhihu.com/p/432726048
https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel/MessageChannel