1

什么是 MessageChannel

MessageChannel 允许两个不同的脚本运行在同一个文档的不同浏览器上下文(例如两个 iframe,文档主体和一个 iframe,使用 SharedWorker 的两个文档,或两个 worker)来直接通讯,在每端使用一个端口(port)通过双向频道(channel)向彼此传递消息。

MessageChannel 是以DOM Event的形式发送消息,所以它属于异步的宏任务。

基本用法

  1. 使用 MessageChannel() 构造函数来创建通讯信道,获取两个端口 MessagePort 对象 port1 port2
  2. 一个端口使用 postMessage发送消息,另一个端口通过 onmessage 接收消息;
  3. 另一个端口通过 onmessage 接收消息;
  4. 当端口收到无法反序列化的消息时,使用 onmessageerror处理;
  5. 停止发送消息时,调用 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)

时倾
791 声望2.4k 粉丝

把梦想放在心中