window.MessageChannel——虫洞般的存在

Hokori 3月28日

MessageChannel

题记

window.MessageChannel是一个异步操作的API,

它可以抽象成一个管道,

既然是一个管道,那么它肯定有两个端口,这两个端口是信息源,也是消息源,可以互相通信

首先来看看它的兼容性

兼容性有时候决定了你是否需要学它,能否用到它

可以看到,大多主流浏览器都已经实现了这个API,可以放心使用

不多bb直接上代码理解它的基础用法

Tip:

  • MessageChannel并不是一定要new出来才能用
  • windowWorker对象下也有对应postMessage方法,同样可以监听message事件
  • windowWorker用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,看到bubblescancelBubble等这样熟悉的属性,

我们可以类比到MouseEvent这样我们熟悉的事件对象,

重点看到dataports属性吧,

  • datapostMessage()传入的第一个参数实际上就是它,用来承载传送的数据
  • ports:用Worker对象或window对象调用的postMessage()传入的第二、第三个参数,用来传送MessagePort对象

Tip:
可以用ES6解构直接获取port1、port2

const {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] 发起的
  • onmessage

    • index.html 中是由port1监听的
    • index2.html 中是由其window对象监听的



除此之外,postMessage() 共使用到了3个参数

分别是 ( message: [Object], targetOrigin: [String], ports: [Object] ) ,

  • message:这里我们传入message的是字符串,实际上可以传入任何类型,挂载到事件对象MessageEventdata
  • targetOrigin:指的是目标源地址

    • 默认是/,只对同源地址有效
    • 可为任何url,如:http://example.com
    • 也可以为通配符*,意思是广播到所有iFrame中
  • ports:用来传入接口,挂载到事件对象MessageEventports

2.Web Worker

假设我们有两个Web Worker进程,分别为js/w1.jsjs/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

    • 在这里通过WorkerpostMessage()方法将端口推入各个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 申明了 microTimerFuncmacroTimerFunc 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

icon_biggrin.pngicon_neutral.pngicon_twisted.pngicon_arrow.pngicon_eek.pngicon_smile.pngicon_sad.pngicon_cool.pngicon_evil.pngicon_mrgreen.pngicon_exclaim.pngicon_surprised.pngicon_razz.pngicon_rolleyes.pngicon_wink.pngicon_cry.pngicon_confused.pngicon_lol.pngicon_mad.pngicon_question.pngicon_idea.pngicon_redface.png
expand_less