前言
哈喽大家好,我是 SuperYing 。最近看 React
源码的时候,发现了一个我不太常用的玩意儿 - MessageChannel
,这是什么东西呢?到底是用来做什么的呢?如我一般好奇的小伙伴们请跟着我一起往下看......
阅读本文您将获得:
了解 MessageChannel 及其常用应用场景。
了解 MessageChannel 在 React 源码中的应用,及对应的兼容方案。
技能点 +1。
本文示例代码已上传至 GitHub:github.com/ying2gege/m...
什么是 MessageChannel
MessageChannel
允许我们建立一个消息通道,并通过两端的端口发送消息实现通信。 其以 DOM Event
的形式发送消息,属于浏览器 宏任务(macro task)的一种。
MessageChannel
实例有两个只读属性:
- port1: 消息通道的第一个端口,连接源上下文通道。
- port2: 消息通道的第二个端口,连接目标上下文通道。
以下
port1
和port2
统称为MessagePort
。
MessageChannel
可以通过调用 MessagePort
的 postMessage
函数相互发送消息,并通过监听 MessagePort
的 message
事件获取对方端口发送的消息内容。
示例代码:
js
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
函数断开链接,断开连接后,消息通道两端将无法继续通信。
实例代码
js
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
进行通信。
实例代码:
html
<!-- parent -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe</title>
</head>
<body>
<h1>parent page</h1>
<input id="input" />
<p id="p">parent p</p>
<iframe src="./iframe.html"></iframe>
</body>
<script>
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])
})
</script>
</html>
html
<!-- iframe -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iframe</title>
</head>
<body>
<h1>iframe</h1>
<p id="p">iframe p</p>
</body>
<script>
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
}
})
</script>
</html>
执行效果如下:
Web Worker
MessageChannel
通信也可以应用到 Web Worker
,过程与 iframe
类似。只不过将 MessageChannel
的两个 port
通过 Worker.postMessage
分别移交给需要通信的两个 Worker
。
实例代码:
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worker</title>
</head>
<body>
</body>
<script>
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])
</script>
</html>
js
// worker1.js
onmessage = (e) => {
e.ports[0].onmessage = (e) => {
console.log(`worker1.js 接收:${e.data}`)
}
}
js
// worker2.js
onmessage = (e) => {
e.ports[0].postMessage("message from the worker2 port!");
}
执行结果:
注:
由于浏览器安全策略,不允许通过本地文件访问
Worker
,直接在浏览器打开index.html
可能会报错,建议将以上测试代码部署到Nginx
或Tomcat
上运行。
深拷贝
由于 MessageChannel
通信过程中会对发送的数据进行序列化,因此也可以通过 MessageChannel
实现深拷贝。
代码示例:
js
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
,以实现任务(如更新组件)的优先级调度和高效执行。
我们在看下简化后的代码:
js
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
的基础用法和应用场景有了大致的了解,爱动手的亲们快去行动起来吧😁。
若本文内容有错误或遗漏的地方,欢迎评论指出。感谢阅读,愿你我共同进步,谢谢!!!