前言

哈喽大家好,我是 SuperYing。最近看 React 源码的时候,发现了一个我不太常用的玩意儿 - MessageChannel,这是什么东西呢?到底是用来做什么的呢?如我一般好奇的小伙伴们请跟着我一起往下看…

阅读本文您将获得:

了解 MessageChannel 及其常用应用场景。 了解 MessageChannel 在 React 源码中的应用,及对应的兼容方案。 技能点 +1。

本文示例代码已上传至 GitHub:https://github.com/ying2gege/message-channel-study

什么是 MessageChannel

MessageChannel 允许我们建立一个消息通道,并通过两端的端口发送消息实现通信。 其以 DOM Event 的形式发送消息,属于浏览器 宏任务(macro task)的一种。

MessageChannel 实例有两个只读属性:

port1: 消息通道的第一个端口,连接源上下文通道。port2: 消息通道的第二个端口,连接目标上下文通道。

以下 port1 和 port2 统称为 MessagePort。

MessageChannel 可以通过调用 MessagePort 的 postMessage 函数相互发送消息,并通过监听 MessagePort 的 message 事件获取对方端口发送的消息内容。

示例代码:

const mc = new MessageChannel()

const port1 = mc.port1

const port2 = mc.port2

port1.onmessage = (e) => {

console.log(`port1 接收来自 port2 的消息:${e.data}`)

port1.postMessage('你好 port2')

}

// port1.addEventListener('message', (e) => {

// console.log(`port1 接收来自 port2 的消息:${e.data}`)

// port1.postMessage('你好 port2')

// })

port2.onmessage = (e) => {

console.log(`port2 接收来自 port1 的消息:${e.data}`)

}

// port2.addEventListener('message', (e) => {

// console.log(`port2 接收来自 port1 的消息:${e.data}`)

// })

port2.postMessage('你好 port1')

执行结果如下:

建立消息后,可以通过调用 MessagePort 的 close 函数断开链接,断开连接后,消息通道两端将无法继续通信。

实例代码

const mc = new MessageChannel()

const port1 = mc.port1

const port2 = mc.port2

port1.onmessage = (e) => {

console.log(`port1 接收来自 port2 的消息:${e.data}`)

port1.postMessage('你好 port2')

// 断开连接

port1.close()

}

// port1.addEventListener('message', (e) => {

// console.log(`port1 接收来自 port2 的消息:${e.data}`)

// port1.postMessage('你好 port2')

// })

port2.onmessage = (e) => {

console.log(`port2 接收来自 port1 的消息:${e.data}`)

port2.postMessage('你在写啥呢?')

}

// port2.addEventListener('message', (e) => {

// console.log(`port2 接收来自 port1 的消息:${e.data}`)

// })

port2.postMessage('你好 port1')

上面代码在 port1.onmessage 绑定函数中调用 port1.close() 断开连接,因此无法接收 port2.onmessage 绑定函数中发送的「‘你在写啥呢?’」。

执行结果如下:

MessageChannel 应用场景

脚本通信

iframe

MessageChannel 可用于 iframe 通信。

大家应该知道,页面与 iframe 通信,需要通过 postMessage api 及 message 事件监听实现。若使用 MessageChannel 则可以将通信工作完全有 MessageChannel 接管,仅使用两端 port 进行通信即可。

具体步骤如下:

parent 页面监听 iframe 加载事件,初次使用 postMessage 向 iframe 发送一条消息,并将 port2 转移到 iframe。iframe 监听 message 事件,接受 parent 页面转移过来的 port2。到此 parent 页面管理 port1,iframe 接管 port2,后续便可通过 MessagePort 进行通信。

实例代码:

iframe

parent page

parent p

iframe

iframe

iframe p

执行效果如下:

Web Worker

MessageChannel 通信也可以应用到 Web Worker,过程与 iframe 类似。只不过将 MessageChannel 的两个 port 通过 Worker.postMessage 分别移交给需要通信的两个 Worker。

实例代码:

Worker

// worker1.js

onmessage = (e) => {

e.ports[0].onmessage = (e) => {

console.log(`worker1.js 接收:${e.data}`)

}

}

// worker2.js

onmessage = (e) => {

e.ports[0].postMessage("message from the worker2 port!");

}

执行结果:

注:

由于浏览器安全策略,不允许通过本地文件访问 Worker,直接在浏览器打开 index.html 可能会报错,建议将以上测试代码部署到 Nginx 或 Tomcat 上运行。

深拷贝

由于 MessageChannel 通信过程中会对发送的数据进行序列化,因此也可以通过 MessageChannel 实现深拷贝。

代码示例:

const obj = {

a: 1,

b: {

c: 3,

d: 4

},

e: undefined,

f: new Date(),

// 拷贝函数和 Symbol 会报错

// g: function() {

// console.log('f')

// },

// h: Symbol()

}

const deepClone = (obj) => {

return new Promise((resolve, reject) => {

const mc = new MessageChannel()

const port1 = mc.port1

const port2 = mc.port2

port2.onmessage = (e) => {

resolve(e.data)

}

port2.onmessageerror = (e) => {

reject(e)

}

port1.postMessage(obj)

})

}

deepClone(obj).then((res) => {

console.log(res)

console.log(res === obj)

})

执行结果如下:

注:

MessageChannel 实现深拷贝存在限制,函数 和 Symbol 等特殊值无法拷贝,执行过程中会报错MessageChannel 通信异步的,因此其实现的深拷贝也是异步函数,效率不如同步处理。

React 源码中 MessageChannel 做了什么

了解过源码的同学可能会发现,在 React 和 Vue 的源码中都有 MessageChannel 的身影,我们来看下 React 中的应用。

源码位置:packages/scheduler/src/forks/Scheduler.js

大家应该听说过,React 16+ 版本实现了异步渲染,其应用了时间切片的策略,那么 React 是如何实现异步渲染的呢?答案是调度器 Scheduler。而调度器中则用到了 MessageChannel,以实现任务(如更新组件)的优先级调度和高效执行。

我们在看下简化后的代码:

let schedulePerformWorkUntilDeadline;

if (typeof localSetImmediate === 'function') {

schedulePerformWorkUntilDeadline = () => {

localSetImmediate(performWorkUntilDeadline);

};

} else if (typeof MessageChannel !== 'undefined') {

const channel = new MessageChannel();

const port = channel.port2;

channel.port1.onmessage = performWorkUntilDeadline;

schedulePerformWorkUntilDeadline = () => {

port.postMessage(null);

};

} else {

schedulePerformWorkUntilDeadline = () => {] nullable value

localSetTimeout(performWorkUntilDeadline, 0);

};

}

通过以上代码可以看到,React 实现任务调度的方法顺序为:setImmediate -> MessageChannel -> setTimeout,这几种方法都属于宏任务,决定该顺序的主要原因在于其运行时间的先后。

结语

好啦,MessageChannel 的介绍就到这里啦。感谢小伙伴们的阅读,相信通读的小伙伴已经对 MessageChannel 的基础用法和应用场景有了大致的了解,爱动手的亲们快去行动起来吧。

若本文内容有错误或遗漏的地方,欢迎评论指出。感谢阅读,愿你我共同进步,谢谢!!!

相关链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: