前言
哈喽大家好,我是 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 进行通信。
实例代码:
parent page
parent p
const mc = new MessageChannel()
const port1 = mc.port1
const port2 = mc.port2
const p = document.querySelector('#p')
const input = document.querySelector('#input')
input.oninput = (e) => {
port1.postMessage(e.data)
}
const ifm = document.querySelector('iframe')
const ifmWindow = ifm.contentWindow
ifm.addEventListener('load', () => {
port1.onmessage = (e) => {
console.log('load', e.data)
p.innerHTML = e.data
}
ifmWindow.postMessage('hello from the parent page!', '*', [port2])
})
iframe
iframe p
const p = document.querySelector('#p')
let port = null
window.addEventListener('message', (e) => {
p.innerHTML = e.data
port = e.ports[0]
port.postMessage("message back from the iframe!");
port.onmessage = (e) => {
p.innerHTML = p.textContent + e.data
}
})
执行效果如下:
Web Worker
MessageChannel 通信也可以应用到 Web Worker,过程与 iframe 类似。只不过将 MessageChannel 的两个 port 通过 Worker.postMessage 分别移交给需要通信的两个 Worker。
实例代码:
const mc = new MessageChannel()
const port1 = mc.port1
const port2 = mc.port2
const worker1 = new Worker('worker1.js')
const worker2 = new Worker('worker2.js')
worker1.postMessage('hello worker1', [port1])
worker2.postMessage('hello worker2', [port2])
// 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 的基础用法和应用场景有了大致的了解,爱动手的亲们快去行动起来吧。
若本文内容有错误或遗漏的地方,欢迎评论指出。感谢阅读,愿你我共同进步,谢谢!!!
相关链接
发表评论