前言
哈喽大家好,我是 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 的基础用法和应用场景有了大致的了解,爱动手的亲们快去行动起来吧😁。
若本文内容有错误或遗漏的地方,欢迎评论指出。感谢阅读,愿你我共同进步,谢谢!!!