React 源码中的 MessageChannel 到底是什么

前言

哈喽大家好,我是 SuperYing 。最近看 React 源码的时候,发现了一个我不太常用的玩意儿 - MessageChannel,这是什么东西呢?到底是用来做什么的呢?如我一般好奇的小伙伴们请跟着我一起往下看......

阅读本文您将获得:

  1. 了解 MessageChannel 及其常用应用场景。

  2. 了解 MessageChannel 在 React 源码中的应用,及对应的兼容方案。

  3. 技能点 +1。

本文示例代码已上传至 GitHub:github.com/ying2gege/m...

什么是 MessageChannel

MessageChannel 允许我们建立一个消息通道,并通过两端的端口发送消息实现通信。 其以 DOM Event 的形式发送消息,属于浏览器 宏任务(macro task)的一种。

MessageChannel 实例有两个只读属性:

  • port1: 消息通道的第一个端口,连接源上下文通道。
  • port2: 消息通道的第二个端口,连接目标上下文通道。

以下 port1port2 统称为 MessagePort

MessageChannel 可以通过调用 MessagePortpostMessage 函数相互发送消息,并通过监听 MessagePortmessage 事件获取对方端口发送的消息内容。

示例代码:

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')

执行结果如下:

建立消息后,可以通过调用 MessagePortclose 函数断开链接,断开连接后,消息通道两端将无法继续通信。

实例代码

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 apimessage 事件监听实现。若使用 MessageChannel 则可以将通信工作完全有 MessageChannel 接管,仅使用两端 port 进行通信即可。

具体步骤如下:

  1. parent 页面监听 iframe 加载事件,初次使用 postMessageiframe 发送一条消息,并将 port2 转移到 iframe
  2. iframe 监听 message 事件,接受 parent 页面转移过来的 port2
  3. 到此 parent 页面管理 port1iframe 接管 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 可能会报错,建议将以上测试代码部署到 NginxTomcat 上运行。

深拷贝

由于 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)
})

执行结果如下:

注:

  1. MessageChannel 实现深拷贝存在限制,函数Symbol 等特殊值无法拷贝,执行过程中会报错
  2. MessageChannel 通信异步的,因此其实现的深拷贝也是异步函数,效率不如同步处理。

React 源码中 MessageChannel 做了什么

了解过源码的同学可能会发现,在 ReactVue 的源码中都有 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 的基础用法和应用场景有了大致的了解,爱动手的亲们快去行动起来吧😁。

若本文内容有错误或遗漏的地方,欢迎评论指出。感谢阅读,愿你我共同进步,谢谢!!!

相关推荐
.生产的驴1 分钟前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw4 分钟前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
老齐谈电商6 分钟前
Electron桌面应用打包现有的vue项目
javascript·vue.js·electron
九圣残炎24 分钟前
【Vue】vue-admin-template项目搭建
前端·vue.js·arcgis
柏箱1 小时前
使用JavaScript写一个网页端的四则运算器
前端·javascript·css
TU^1 小时前
C语言习题~day16
c语言·前端·算法
一颗花生米。4 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐014 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19954 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式