什么是 MessageChannel
MessageChannel 允许两个不同的脚本运行在同一个文档的不同浏览器上下文(例如两个 iframe,文档主体和一个 iframe,使用 SharedWorker
的两个文档,或两个 worker)来直接通讯,在每端使用一个端口(port)通过双向频道(channel)向彼此传递消息。
MessageChannel 是以DOM Event
的形式发送消息,所以它属于异步的宏任务。
基本用法
- 使用
MessageChannel()
构造函数来创建通讯信道,获取两个端口 MessagePort 对象port1
port2
; - 一个端口使用
postMessage
发送消息,另一个端口通过onmessage
接收消息; - 另一个端口通过
onmessage
接收消息; - 当端口收到无法反序列化的消息时,使用
onmessageerror
处理; - 停止发送消息时,调用
close
关闭端口;
方式一
const { port1, port2 } = new MessageChannel();
port1.onmessage = (event) => {
console.log('收到来自port2的消息:', event.data);
};
port1.onmessageerror = (event) => {};
port2.onmessage = function (event) {
console.log('收到来自port1的消息:', event.data);
port2.postMessage('我是port2');
};
port2.onmessageerror = (event) => {};
port1.postMessage('我是port1');
方式二
const { port1, port2 } = new MessageChannel();
port1.addEventListener('message', event => {
console.log('收到来自port2的消息:', event.data);
});
port1.addEventListener('messageerror', (event) => { });
port1.start();
port2.addEventListener('message', event => {
console.log('收到来自port1的消息:', event.data);
port2.postMessage('我是port2');
});
port2.addEventListener('messageerror', (event) => { });
port2.start();
port1.postMessage('我是port1');
以上两种方式,输出均为:
收到来自port1的消息: 我是port1
收到来自port2的消息: 我是port2
- 使用
addEventListener
方式,需要手动调用start()
方法消息才能流动,因为初始化的时候是暂停的。onmessage
已经隐式调用了start()
方法。
Event Loop 中的执行顺序
同步任务 > 微任务 > requestAnimationFrame > DOM渲染 > 宏任务
setTimeout(() => {
console.log('setTimeout')
}, 0)
const { port1, port2 } = new MessageChannel()
port2.onmessage = e => {
console.log(e.data)
}
port1.postMessage('MessageChannel')
requestAnimationFrame(() => {
console.log('requestAnimationFrame')
})
Promise.resolve().then(() => {
console.log('Promise1')
})
输出为:
Promise // 微任务先执行
requestAnimationFrame
setTimeout // 宏任务,先定义先执行
MessageChannel // 宏任务,后定义后执行
requestAnimationFrame - 不是宏任务的任务
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行 --- MDN
严格意义上来说,raf 并不是一个宏任务,因为
- 执行时机和宏任务完全不一致;
- raf 任务队列被执行的时候,会将其此刻队列中所有的任务都执行完;
使用场景
一:同一个document 的上下文通信
var channel = new MessageChannel();
var para = document.querySelector('p');
var ifr = document.querySelector('iframe');
var otherWindow = ifr.contentWindow;
ifr.addEventListener("load", iframeLoaded, false);
function iframeLoaded() {
otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
}
channel.port1.onmessage = handleMessage;
function handleMessage(e) {
para.innerHTML = e.data;
}
二:结合 Web Worker 实现多线程通信
三:深拷贝
大部分需要深拷贝的场景,都使用JSON.parse(JSON.stringify(object))
。但这种办法会忽略 undefined、function、symbol 和循环引用的对象。
// 深拷贝函数
function deepClone(val) {
return new Promise(resolve => {
const { port1, port2 } = new MessageChannel()
port2.onmessage = e => resolve(e.data)
port1.postMessage(val)
})
}
使用 MessageChannel 实现的深拷贝只能解决 undefined 和 循环引用对象的问题,对于 Symbol 和 function 依然束手无策。
实践
问题描述:
有两个由第三方调用,顺序不定的方法,等这两个方法都调用后,在做进一步处理。
解决方案
使用 setTimeout 模拟方法的调用顺序,代码如下:
const { port1, port2 } = new MessageChannel()
let data
const handleUser = newData => {
if (data) {
const result = { ...data, ...newData }
console.log('获取到全部数据', result)
port1.close()
port2.close()
} else {
data = newData
}
}
const getName = () => {
const params = { name: '123' }
port1.postMessage(params)
port1.onmessage = e => {
handleUser(e.data)
}
}
const getAge = () => {
const params = { age: 88 }
port2.postMessage(params)
port2.onmessage = e => {
handleUser(e.data)
}
}
setTimeout(() => {
getName()
}, 0)
setTimeout(() => {
getAge()
}, 10)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。