我们都知道JS代码是在浏览器5个进程(下面有介绍)中渲染进程中的Js引擎线程执行的,其他还有GUI渲染线程、定时器线程等,而页面的布局和绘制是在GUI线程中完成的,这些线程之间是互斥的,所以在执行Js的同时会阻塞页面的渲染绘制。
60帧我们是认识标准帧率,所以我们本文都是以60帧来进行说明,即16ms。
所以我们需要在16ms之内完成Js解析执行、样式布局、页面绘制这三个步骤,如果Js执行太长时间到站页面不能及时绘制就会导致卡顿。而在React15及之前都是同步执行的,当组件过多会很容易导致卡顿,这和React设计理念快速响应不符,所以在React16之后调整架构,自实现了Scheduler来通过时间分片结合可中断的方式来进行异步渲染,以及在此基础上新增了优先级来进行优先调度。
本文主要介绍了以下两个问题:
MessageChannel能帮助我们构建一条通道,以DOM Event的形式发送信息,通道两端的端口发送消息实现通信,是一个宏任务。MessageChannel 实例有两个只读属性:
port1、port2统称为MessagePort。
MessageChannel 可以通过调用 MessagePort 的 postMessage 函数相互发送消息,并通过监听 MessagePort 的 message 事件获取对方端口发送的消息内容。
const { port1, port2 } = new MessageChannel();
port1.onmessage = (e) => {
console.log(`port1 接收来自 port2 的消息:${e.data}`)
port1.postMessage('hello port2')
port1.close()
}
port2.onmessage = (e) => {
console.log(`port2 接收来自 port1 的消息:${e.data}`)
port2.postMessage('hello, are you ok?') // 由于port1发送消息之后关闭了连接,所以这个不会没有地方接收
}
port2.postMessage('hello port1')`
简单来说,MessageChannel就是构建一个信息通道,通过postMessage发布消息,通过onMessage来订阅消息,通过close可以来关闭连接。
MessageChannel除了在Scheduler中进行分片和调度之外还有以下一些应用场景,下面进行简单介绍:
简单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);
MessageChannel实现深拷贝类似JSON.parse(JSON.stringify()),也有一些限制,比如:函数 和 Symbol 等特殊值无法拷贝,执行过程中会报错
MessageChannel的应用场景可以参考下这篇文章的应用场景介绍,其中有代码举例说明,较本文更加详细React 源码中的 MessageChannel 到底是什么
针对这块内容,我们先从几个Q&A来了解一下原因,然后再看看MessageChannel在Scheduler中是如何使用的。
Q: 为什么不使用requestIdleCallback?
A: requestIdleCallback虽然是浏览器自带的api,用以在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。在一帧16ms里面如果有富余时间则会执行该函数里面注册的任务,但是其存在一些问题让React选择不使用:
从上面可以看出对safari浏览器不友好,详细兼容问题可以去can i use上查询。
Q:为什么不使用 requestAnimationFrame
Q: 为什么不使用setTimeout
A: 虽然MessageChannel也是宏任务,但其执行时间在setTimeout之前,而且setTimeout最低会有4ms的延迟,并且当浏览器不支持MessageChannel也会降级使用setTimeout。
至于为什么setTimeout会有4ms延迟,请查看这篇文章:为什么 setTimeout 有最小时延 4ms ?
React 源码中 MessageChannel 做了什么
上面解释了为什么React选择自己实现时间分片也不使用已有API来进行处理,下面从源码的角度来看看在React 源码中 MessageChannel 做了什么?
源码位置:packages/scheduler/src/forks/Scheduler.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 -> setTimeout -> MessageChannel ,这几种方法都属于宏任务,决定该顺序的主要原因在于其运行时间的先后。
至于setImmediate、Promise、setTimeout、MessageChannel的先后执行时间可以在控制台执行下方代码查看,其中setImmediate和setTimeout谁先执行主要看setImmediate的注册时机
// setImmediate111比setTimeout先执行
setImmediate(() => {
console.log('setImmediate111')
})
setTimeout(() => {
console.log('setTimeout')
}, 0)
// setImmediate222比setTimeout后执行比MessageChannel先执行
setImmediate(() => {
console.log('setImmediate222')
})
const { port1, port2 } = new MessageChannel()
port2.onmessage = function () {
console.log('MessageChannel')
}
port1.postMessage('ping')
requestAnimationFrame(() => {
console.log('requestAnimationFrame')
})
Promise.resolve().then(() => {
console.log('Promise')
})
// setImmediate333最后执行
setImmediate(() => {
console.log('setImmediate333')
})
在旧版本chrome上MessageChannel会先于setTimeout打印,在新版本chrome上则反过来,应该是chrome在某个版本上修改了宏任务优先级的实现。
由于上面提及到了浏览器进程,在这简单介绍下:仅限于Chrome浏览器
Chrome浏览器是多进程架构,主要是5个进程:
为了避免一个进程崩溃而导致整个页面无法正常工作,不同的进程之前是相互隔离的,如果需要通信则要通过IPC机制(Inter Process Communication)来进行通信。
进程和线程之间的关系有以下特点: