大家好,我是雷布斯。
如果大家有阅读过 Vue 或者 React 的源码,会发现它们都用到过 MessageChannel
。
MessageChannel 究竟是什么,有什么用?我们今天一块来进行学习。
先把 MessageChannel
需要重点关注信息给大家列出来:
MessageChannel
可以帮助我们创建一个消息通道,并通过它的两个 MessagePort
属性发送数据。这个管道有两个端口,每个端口都可以通过postMessage
发送数据,其中一个端口只要绑定了onmessage
回调方法,就可以接收从另一个端口传过来的数据React
源码或其他一些前端库中,MessageChannel
通常被用于实现一些并发模式、多线程或在不同上下文中的通信。例如,React
的 Concurrent Mode
使用 MessageChannel
实现了一种任务切片和协同工作的机制,以提高应用的响应性。MessageChannel
是一个宏任务MessageChannel
API 提供了一个双向通信的通道,使得两个上下文能够通过发送和接收消息进行交流。
在使用 MessageChannel
时,你可以创建一个 MessageChannel
对象,该对象包含两个端口,分别是 port1
和 port2
。
你可以将其中一个端口发送到另一个上下文,从而建立通信通道。
通过端口,你可以发送消息,并监听接收到的消息。这在多窗口、多框架或 Web Worker 中进行数据传递时非常有用。
// 创建 MessageChannel
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
port1.onmessage = function(event) {
console.log("port1收到来自port2的数据:" + event.data);
}
port2.onmessage = function(event) {
console.log("port2收到来自port1的数据:" + event.data);
}
port1.postMessage("发送给port2");
port2.postMessage("发送给port1");
我们接着来看下 MessageChannel
的使用场景。
大家知道最简单的深拷贝是用 JSON.parse(JSON.stringify(object))
实现,但这种方法有一些局限性:
undefined和函数会被忽略,而尝试拷贝循环引用的对象则会报错:
MessageChannel
的postMessage
传递的数据也是深拷贝的,这和web worker
的postMessage
一样,而且还可以拷贝undefined
和循环引用的对象。
// 有undefined + 循环引用
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
f: undefined
}
obj.c = obj.b;
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
function deepCopy(obj) {
return new Promise((resolve) => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
deepCopy(obj).then((copy) => { // 请记住`MessageChannel`是异步的这个前提!
let copyObj = copy;
console.log(copyObj, obj)
console.log(copyObj == obj)
});
但拷贝有函数、Symbol
等不可序列化的值时,还是会报无法克隆的错误:
window与单个iframe或者多个iframe之间的通信可以使用 MessageChannel
,通过只暴露有限的能力从而保证安全性。
//主页面
<iframe id="iframe1" src="./iframe1.html"></iframe>
<iframe id="iframe2" src="./iframe2.html"></iframe>
<script>
window.onload = function(){
var {port1,port2} = new MessageChannel();
var iframe1 = document.getElementById('iframe1');
iframe1.contentWindow.postMessage('main','*',[port1]);
var iframe2 = document.getElementById('iframe2');
iframe2.contentWindow.postMessage('main','*',[port2]);
}
</script>
//iframe1
<div id="message"></div>
<script>
window.addEventListener('message',function(event){
console.log(event);
let messageDom = document.getElementById('message');
messageDom.innerHTML = "收到" + event.origin + "消息:" + event.data;
let port = event.ports[0];
port.onmessage = function(e){
messageDom.innerHTML += '<br/>收到' + e.origin + '消息: ' + e.data;
}
port.postMessage('from iframe1');
}, false);
</script>
//iframe2
<div id="message"></div>
<script>
window.addEventListener('message',function(event){
console.log(event);
let messageDom = document.getElementById('message');
messageDom.innerHTML = "收到" + event.origin + "消息:" + event.data;
let port = event.ports[0];
port.onmessage = function(e){
messageDom.innerHTML += '<br/>收到' + e.origin + '消息: ' + e.data;
}
port.postMessage('from iframe2');
}, false);
</script>
const worker1 = new Worker('./worker1.js');
const worker2 = new Worker('./worker2.js');
const ms = new MessageChannel();
// 把 port1 分配给 worker1
worker1.postMessage('main', [ms.port1]);
// 把 port2 分配给 worker2
worker2.postMessage('main', [ms.port2]);
//worker1.js
self.onmessage = function(e) {
console.log('worker1', e.ports);
if (e.data === 'main') {
const port = e.ports[0];
port.postMessage(`worker1: Hi! I'm worker1`);
port.onmessage = function(ev){
console.log('reveice: ',ev.data,ev.origin);
}
}
}
//worker2.js
self.onmessage = function(e) {
if (e.data === 'main') {
const port = e.ports[0];
port.onmessage = function(e) {
console.log('receive: ', e.data);
port.postMessage('worker2: ' + e.data);
}
}
}
除了以上几种场景,MessageChannel还是在事件循环的应用上出现得比较多。
下面的例子,打印顺序会是怎样?
setTimeout(() => {
console.log('setTimeout')
}, 0)
const { port1, port2 } = new MessageChannel()
port2.onmessage = function () {
console.log('MessageChannel')
}
port1.postMessage('ping')
Promise.resolve().then(() => {
console.log('Promise')
})
新版本的 Chrome 中,输出是
Promise
setTimeout
MessageChannel
MessageChannel
与 setTimeout
一样,是一个宏任务。
Vue的nextTick
的实现经过了多次的调整。在 Vue2.5 以前,nextTick优先使用微任务Promise来实现。到了2.5版本,Vue引入MessageChannel,nextTick的实现优先使用setImmediate
,平台不支持则使用MessageChannel
,再不支持才使用Promise
,最后用setTimeout
兜底。
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
不过到了2.6版本以后,nextTick又改回原来的Promise实现。虽然MessageChannel只存在了一个minor版本,但是我们从Vue的使用上知道它可以用来控制异步任务的执行时机。
众所周知,React为了保证一帧内有足够的时间渲染ui,使用了requestIdleCallback
这个API。但实际上,由于requestIdleCallback工作帧率低,只有20FPS
,还有兼容问题,React并没有使用它,而是用requestAnimationFrame
和MessageChannel
进行polyfill。
// SchedulerHostConfig.default.js
...
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
...
主流浏览器都对MessageChannel支持良好。
还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打无广告和更新快哦~。
老规矩,也给我们团队的辅导服务打个广告。