题记
window.MessageChannel
是一个异步操作的API,
它可以抽象成一个管道,
既然是一个管道,那么它肯定有两个端口,这两个端口是信息源,也是消息源,可以互相通信
首先来看看它的兼容性
兼容性有时候决定了你是否需要学它,能否用到它
可以看到,大多主流浏览器都已经实现了这个API,可以放心使用
不多bb直接上代码理解它的基础用法
Tip:
MessageChannel
并不是一定要new出来才能用window
和Worker
对象下也有对应postMessage
方法,同样可以监听message
事件window
和Worker
用DOM2级事件处理不需要调用start()
方法
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
//监听事件
//用DOM2级事件处理
port1.addEventListener('message', (msg) => {
console.log('port1接收到信息:' + msg.data);
})
port2.addEventListener('message', (msg) => {
console.log('port2接收到信息:' + msg.data);
})
//如果不调用start()方法的话,将监听不到message事件
port1.start();
port2.start();
//使用DOM0级事件处理,会隐式调用start()方法
//port1.onmessage = (msg)=>{
// console.log('port1接收到信息:' + msg.data);
//}
//port2.onmessage = (msg)=>{
// console.log('port2接收到信息:' + msg.data);
//}
port1.postMessage('来自port1的信息');
port2.postMessage('来自port2的信息');
控制台输出:
port2接收到信息:来自port1的信息 index.html
port1接收到信息:来自port2的信息 index.html
我们来看看MessageChannel
含有的属性
可以看到channel有两个属性对应两个端口,分别是port1,port2
注意:它们是只读的
我们再来看看监听事件取到的msg
长什么样子,包含了什么信息
可以看到它是一个MessageEvent
,看到bubbles
、cancelBubble
等这样熟悉的属性,
我们可以类比到MouseEvent
这样我们熟悉的事件对象,
重点看到data
和ports
属性吧,
data
:postMessage()
传入的第一个参数实际上就是它,用来承载传送的数据ports
:用Worker
对象或window
对象调用的postMessage()
传入的第二、第三个参数,用来传送MessagePort
对象
Tip:
可以用ES6解构直接获取port1、port2const {port1, port2} = new MessageChannel();
关于MessageChannel的应用场景
- MessageChannel提供了一个安全的方法来解决跨域问题(主要是iFrame之间的)
- MessageChannel支持在Web Worker中使用
- Vue的nextTick降级方案用到了MessageChannel
这三种场景所用到的postMessage方法形参有所不同,具体如下:
1.iFrame跨域场景
假设我们主页面为index.html,
iFrame页面为index2.html
代码如下:
//index.html
const {port1,port2} = new MessageChannel();
let iframe = document.querySelector('iframe');
iframe.addEventListener("load", () => {
port1.onmessage = (e) => {
console.log(e)
}
iframe.contentWindow.postMessage('来自index.html的信息', '*', [port2]);
});
//index2.html
window.addEventListener('message', e=>{
e.port[0].postMessage('来自index2.html的信息')
});
//甚至可以这么写
//window.addEventListener('message', e => {
// const port2 = e.ports[0];
// port2.addEventListener('message', () => {
// port2.postMessage('来自index2.html的信息')
// });
// port2.start();
//});
//或者这么写,会隐式调用start()方法
//window.addEventListener('message', e => {
// const port2 = e.ports[0];
// port2.onmessage = () => {
// port2.postMessage('来自index2.html的信息')
// };
//});
看到这里可能会很迷惑,
我们来一起好好理清一下,
笔者一开始写的例子中 postMessage 和 监听message 都是由MessageChannel
对象下的MessagePort
完成的
而这里并不是这样的,
postMessage
- 在 index.html 中是由
iFrame.contentWindow
也就是~index2.html 中的window对象调用的全局方法 - 在 index2.html 中则是由port2
也就是发起的e.port[0]
- 在 index.html 中是由
onmessage
- 在 index.html 中是由port1监听的
- 在 index2.html 中是由其window对象监听的
除此之外,postMessage()
共使用到了3个参数
分别是 ( message: [Object], targetOrigin: [String], ports: [Object] ) ,
- message:这里我们传入message的是字符串,实际上可以传入任何类型,挂载到事件对象
MessageEvent
的data
上 targetOrigin:指的是目标源地址
- 默认是
/
,只对同源地址有效 - 可为任何url,如:http://example.com
- 也可以为通配符
*
,意思是广播到所有iFrame中
- 默认是
- ports:用来传入接口,挂载到事件对象
MessageEvent
的ports
上
2.Web Worker
假设我们有两个Web Worker进程,分别为js/w1.js
、js/w2.js
主页面为index.html
首先我们应该知道Worker
对象本身就具有postMessage()
方法和监听message
事件的能力,
所以Worker线程和主线程的通信是很方便实现的,
我们这里主要通过new MessageChannel
来实现两个Worker线程的通信
代码如下:
//index.html
//生两个崽出来先(?)
const w1 = new Worker('js/w1.js');
const w2 = new Worker('js/w2.js');
//生成两个端口
const{port1,port2} = new MessageChannel()
w1.postMessage(null, [port1])
w2.postMessage(null, [port2])
//w1.js
self.addEventListener('message',e=>{
const port1 = e.ports[0]
//向另一个端口发送信息
port1.postMessage('来自w1的信息')
port1.onmessage = e=>{
console.log(e.data)
}
})
//w2.js
self.addEventListener('message',e=>{
const port2 = e.ports[0];
//向另一个端口发送信息
port2.postMessage('来自w2的信息')
port2.onmessage = e=>{
console.log(e.data)
}
})
控制台输出
来自w2的信息 w1.js
来自w1的信息 w2.js
大概讲一下思路:
index.html
- 在这里通过
Worker
的postMessage()
方法将端口推入各个Worker
中
- 在这里通过
w1.js、w2.js
- 监听
message
事件,拿到自己传给自己的端口
- 监听
至此就结束啦,接下来只要在port1、port2之间发送和接收数据就可以了
打个比方,一个数据从w1给到w2,实际流程是w1 -> port1 -> port2 -> w2
值得注意的是
- 这里的Worker所调用的
postMessage()
和iFrame跨域通信的参数有所不同, - 第二个参数是传入的
MessagePort
而不是targetOrigin
3.Vue的nextTick
Vue的nextTick是为了在DOM更新后可以执行回调函数来获取最新的DOM数据,
在 Vue 源码 2.5+ 后,nextTick
的实现单独有一个 JS 文件来维护它,在 src/core/util/next-tick.js
中,
笔者摘一些重点代码下来看看:
import { isIOS, isNative } from './env'
let microTimerFunc
let macroTimerFunc
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
这是官网说的原话
next-tick.js
申明了microTimerFunc
和macroTimerFunc
2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生setImmediate
,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的MessageChannel
,如果也不支持的话就会降级为setTimeout 0
;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。
也就是说nextTick对宏任务和微任务有各自的降级方案,
- 对于宏任务来说,优先度是
setImmediate -> MessageChannel -> setTimeout 0
- 对于微任务来说,优先度是
Promise -> 转换成宏任务执行
具体可以去官网看Vue.js 技术揭秘
结尾
MessageChannel甚至可以实现不完美的深拷贝(属性为函数时会报错)
有兴趣可以去参考链接1看一看
参考链接:
记录:window.MessageChannel那些事
HTML Living Standard
MDN web docs MessageChannel