MessageChannel在react中的作用?

857 阅读4分钟

MessageChannel是什么?

  • MessageChannel可以帮助我们创建一个消息通道,并通过它的两个MessagePort 属性发送数据。
  • react使用MessageChannel来模拟requestIdleCallback将回调延迟至绘制操作之后执行。
  • MessageChannel创建一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,其中一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据
  • MessageChannel是一个宏任务。

为什么不直接使用requestIdleCallback?

  • 什么是requestIdleCallback及其作用?
    • requestIdleCallback是浏览器原生提供的一个api,使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
    • 正常帧任务完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务
    • FPS(frame per second) 是指网页每秒传输的帧数,可以理解为刷新率,每秒刷新的频率越高,画面越流畅,一般来说显示器的刷新频率为60Hz,即每秒刷新60次,每秒单帧所需时间为1000ms/60 =16.6ms
    • 每一帧都做了哪些事情呢?
      • 输入事件(连续事件touch,离散事件click)
      • javascript代码(定时器)
      • 开始帧(window resize、scroll)
      • RequestAnimationFrame(callbacks 如果需要渲染)
      • 布局(计算样式,更新布局)
      • 绘制(分层,绘制)
      • 空闲时间(执行idle callback也就是requestIdleCallback的回调) image.png
  • requestIdleCallback存在什么问题?
    • 浏览器兼容问题(目前不支持Safari浏览器)
    • 稳定性差(正常情况下渲染一帧时长控制在16.67ms,requestIdleCallback FPS只有20ms )
    • requestIdleCallback 目的主要是用来处理不重要且不紧急的任务,因为React渲染内容并非是不重要且不紧急;

MessageChannel简单使用

const { port1, port2 } = new MessageChannel();
port1.onmessage = function(event) {
  console.log("port1收到的数据:", event.data);
}
port2.onmessage = function(event) {
  console.log("port2收到的数据:", event.data);
}
port1.postMessage("port1发送的数据");
port2.postMessage("port2发送的数据");

react Scheduler过程

  • 当我们调用setState函数时,会触发组件更新,react会请求当前fiber最新的车道lane再结合当前的更新信息(如action,next指针指向下一个更新)生成一个update对象,然后将update对象入队缓存,开启调度更新。
  • 首先将当前更新的lane合并到root,然后从根节点自上而下开启调度。
  • 向 Scheduler 中存入一个任务,react在调和阶段更新一个fiber后,会暂停,然后询问Scheduler是否需要暂停,如果不需要暂停(还有时间片)则继续更新下一个fiber。
  • 如果需要暂停,则react会返回一个函数,该函数用于告诉 Scheduler 任务还没有完成,并且react会使用MessageChannel开启一个宏任务,以便在一帧继续更新还没有完成的任务。

react Scheduler模块的主要作用

  • 可以控制javacscript代码的暂停,将执行权归还给浏览器,让浏览器有机会更新页面。
  • 在未来的某个时刻继续调度任务,执行上次还未完成的任务。
  • 假如Scheduler在调度完成一个fiber工作循环后,发现没有时间片了,这个时候它要暂停掉js代码的执行,让浏览器有时间刷新页面,那么暂停之后如何才能接上呢,这个时候是不是需要新开启一个宏任务,目地是让浏览器完成本帧的渲染,在下个工作循环中继续调度剩下未完成的任务。
  • 这里的时间片react默认是每帧5ms,也就是一帧当中正常情况下留下来5ms时间执行调度任务(这个时间随着设备fps的不同也会改变)

为什么不使用setTimeout

  • MessageChannel的执行时机会早于setTimeout
  • setTimeout(fn,0) 所创建的宏任务,会有至少 4ms 的执行时差
  • 如果当前环境不支持 MessageChannel 时,会默认使用 setTimeout

为什么不使用# requestAnimationFrame

  • requestAnimationFrame执行时机是页面渲染前,将React Task放到RAF中,依然有可能会阻塞渲染
  • 有可能过了几次loop才调用一次RAF,React Task就会被搁置太久(浏览器并没有规定应该何时渲染页面,因此RAF是不稳定的)

MessageChannel还能用来做什么?

  • 深拷贝
    • MessageChannel的消息在发送和接收的过程需要序列化和反序列化。利用这个特性,我们可以实现深拷贝
function deepClone(obj) {
 return new Promise((resolve, reject) => {
   try {
     const { port1, port2 } = new MessageChannel();

     port2.onmessage = function (e) {
       resolve(e.data);
     };
     port1.postMessage(obj);
   } catch (e) {
     reject(e);
   }
 });
}
const oldObj = { a: { b: 1 } };
deepClone(oldObj).then((newObj) => {
 console.log(oldObj === newObj); // false
 newObj.a.b = 2;
 console.log(oldObj.a.b); // 1
});
  • MessageChannel实现两个worker的直接通信
  • window与单个iframe或者多个iframe之间的通信可以使用MessageChannel
  • MessageChannel可以作为简单的EventEmitter做事件的订阅发布,实现不同脚本之间的通信
// a.js
export default function a(port) {
  port.postMessage({ from: 'a', message: 'ping' });
}

// b.js
export default function b(port) {
  port.onmessage = (e) => {
    console.log(e.data); // {from: 'a', message: 'ping'}
  };
}
// index.js
import a from './a.js';
import b from './b.js';
const { port1, port2 } = new MessageChannel();
b(port2);
a(port1);