window.MessageChannel也是浏览器提供的一个异步操作的API,属于宏任务
应用场景
- 不同浏览器上下文通信。比如window.open()打开的窗口或者iframe等之间建立通信管道,并通过两端的端口(port1和port2)发送消息。因此实现专用信道通信,而不必对非指定窗口进行过滤。
- worker跨线程通信
- 对象深拷贝,利用消息在发送和接收的过程需要序列化和反序列化。
这个api会创建一个管道,管道的两端分别代表一个messagePort,都能够通过portMessage向对方发送数据,通过onmessage来接受对方发送过来的数据
基本使用
const ch = new MessageChannel()
const port1 = ch.port1
const port2 = ch.port2
port1.onmessage = function(d) {
console.log(`port1接收的消息是:${d.data}`)
}
port2.onmessage = function(d) {
console.log(`port2接收的消息是:${d.data}`)
}
//addEventListener写法也可以,但必须手动调用start,onmessage写法隐式调用了start
port2.addEventListener('message', function (event) {
...
});
//addEventListener写法
port1.start();
//addEventListener写法
port2.start();
// 发送消息
port1.portMessage('port1发送的消息')
port2.portMessage('port2发送的消息')
port1.close() //关闭连接
port1.onmessageerror=fn; //消息不能反序列化时,会出现错误,这时可以用onmessageerror方法捕获
应用场景
让两个web worker可以通过MessageChannel通信,即实现多线程通信
- 注意,此时port在主线程不可用了,因为postMessage第二个参数是transferable的原因,让线程中的变量内存被转移到了另一个线程
// index.html
<script>
var w1 = new Worker("worker1.js");
var w2 = new Worker("worker2.js");
var ch = new MessageChannel();
w1.postMessage("port1", [ch.port1]); //第二个参数不会被编码转换导致失效
w2.postMessage("port2", [ch.port2]);
w2.onmessage = function(e) {
console.log(e.data);
}
</script>
====worker1.js
// worker1.js
self.onmessage = function(e) {
const port = e.ports[0];
port.postMessage("this is from worker1") //woker1中通过port1发送给port2
}
====worker2.js
// worker2.js
self.onmessage = function(e) {
const port = e.ports[0];
port.onmessage = function(e) {
postMessage(e.data) //woker2中监听woker1中port1传递的数据,然后触发自身woker的监听函数
}
}
对象深拷贝
- 当消息包含函数、Symbol等不可序列化的值时,就会报无法克隆的DOM异常
function deepClone(obj) {
return new Promise((resolve, reject) => {
try {
const { port1, port2 } = new MessageChannel();
port2.onmessage = function (e) {
resolve(e.data);
};
port1.postMessage(obj);
} catch (e) {
reject(e);
}
});
}
const oldObj = { a: { b: 1 } };
deepClone(oldObj).then((newObj) => {
console.log(oldObj === newObj); // false
newObj.a.b = 2;
console.log(oldObj.a.b); // 1
});
和iframe之间通信
<script>
var channel = new MessageChannel();
var output = document.querySelector('.output');
var iframe = document.querySelector('iframe');
// Wait for the iframe to load
iframe.addEventListener("load", onLoad);
function onLoad() {
channel.port2.onmessage = onMessage;
iframe.contentWindow.postMessage('I am from main page!', '*', [channel.port1]);
}
// Handle messages received on port1
function onMessage(e) {
console.log(e, 'main');
output.innerHTML = e.data + '---main--';
}
</script>
<script>
var output = document.querySelector('.output');
window.addEventListener('message', onMessage);
function onMessage(e) {
output.innerHTML = e.data;
e.ports[0].postMessage('I am from iframe page');
}
</script>
在vue中的应用
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)
}
}
在React中的应用
- 由于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;
...