这是个人剖析 React 源码的第四篇文章,以前的文章都是具体剖析代码,可是以为这种方式可能并非太好。所以从这篇文章开始,我打算把在源码中学习到的内容单独写成一篇文章,这样对于读者来讲可能更加的友好。前端
你们都知道 JS 和渲染引擎是一个互斥关系。若是 JS 在执行代码,那么渲染引擎工做就会被中止。假如咱们有一个很复杂的复合组件须要从新渲染,那么调用栈可能会很长react
调用栈过长,再加上若是中间进行了复杂的操做,就可能致使长时间阻塞渲染引擎带来很差的用户体验,调度就是来解决这个问题的。git
React 会根据任务的优先级去分配各自的 expirationTime
,在过时时间到来以前先去处理更高优先级的任务,而且高优先级的任务还能够打断低优先级的任务(所以会形成某些生命周期函数屡次被执行),从而实如今不影响用户体验的状况下去分段计算更新(也就是时间分片)。github
React 实现调度主要靠两块内容:浏览器
接下来就让笔者为你们一一介绍着两块内容。函数
expriationTime 在前文简略的介绍过它的做用,这个时间能够帮助咱们对比不一样任务之间的优先级以及计算任务的 timeout。post
那么这个时间是如何计算出来的呢?学习
当前时间指的是 performance.now()
,这个 API 会返回一个精确到毫秒级别的时间戳(固然也并非高精度的),另外浏览器也并非全部都兼容 performance
API 的。若是使用 Date.now()
的话那么精度会更差,可是为了方便起见,咱们这里统一把当前时间认为是 performance.now()
。动画
常量指的是根据不一样优先级得出的一个数值,React 内部目前总共有五种优先级,分别为:spa
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
复制代码
它们各自的对应的数值都是不一样的,具体的内容以下
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
复制代码
也就是说,假设当前时间为 5000 而且分别有两个优先级不一样的任务要执行。前者属于 ImmediatePriority
,后者属于 UserBlockingPriority
,那么两个任务计算出来的时间分别为 4999
和 5250
。经过这个时间能够比对大小得出谁的优先级高,也能够经过减去当前时间获取任务的 timeout。
说完了 expriationTime
,接下来的主题就是实现 requestIdleCallback
了,咱们首先来了解下该函数的做用
该函数的回调方法会在浏览器的空闲时期依次调用, 可让咱们在事件循环中执行一些任务,而且不会对像动画和用户交互这样延迟敏感的事件产生影响。
在上图中咱们也能够发现,该回调方法是在渲染之后才执行的。那么介绍完了函数的做用,接下来就来讲说它的兼容性吧。
这个函数的兼容性并非很好,而且它还有一个致命的缺陷:
requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work.
也就是说 requestIdleCallback
只能一秒调用回调 20 次,这个彻底知足不了现有的状况,由此 React 团队才打算本身实现这个函数。
若是你想了解更多关于替换 requestIdleCallback
的内容,能够阅读 该 Issus。
实现 requestIdleCallback
函数的核心只有一点,如何屡次在浏览器空闲时且是渲染后才调用回调方法?
说到屡次执行,那么确定得使用定时器了。在多种定时器中,惟有 requestAnimationFrame
具有必定的精确度,所以 requestAnimationFrame
就是当下实现 requestIdleCallback
的一个步骤。
requestAnimationFrame
的回调方法会在每次重绘前执行,另外它还存在一个瑕疵:页面处于后台时该回调函数不会执行,所以咱们须要对于这种状况作个补救措施
rAFID = requestAnimationFrame(function(timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
// 定时 100 毫秒是算是一个最佳实践
localCancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, 100);
复制代码
当 requestAnimationFrame
不执行时,会有 setTimeout
去补救,两个定时器内部能够互相取消对方。
使用 requestAnimationFrame
只完成了屡次执行这一步操做,接下来咱们须要实现如何知道当前浏览器是否空闲呢?
你们都知道在一帧当中,浏览器可能会响应用户的交互事件、执行 JS、进行渲染的一系列计算绘制。假设当前咱们的浏览器支持 1 秒 60 帧,那么也就是说一帧的时间为 16.6 毫秒。若是以上这些操做超过了 16.6 毫秒,那么就会致使渲染没有完成并出现掉帧的状况,继而影响用户体验;若是以上这些操做没有耗时 16.6 毫秒的话,那么咱们就认为当下存在空闲时间让咱们能够去执行任务。
所以接下去咱们须要计算出当前帧是否还有剩余时间让咱们使用。
let frameDeadline = 0
let previousFrameTime = 33
let activeFrameTime = 33
let nextFrameTime = performance.now() - frameDeadline + activeFrameTime
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
if (nextFrameTime < 8) {
nextFrameTime = 8;
}
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
复制代码
以上这部分代码核心就是得出每一帧所耗时间及下一帧的时间。简单来讲就是假设当前时间为 5000,浏览器支持 60 帧,那么 1 帧近似 16 毫秒,那么就会计算出下一帧时间为 5016。
得出下一帧时间之后,咱们只需对比当前时间是否小于下一帧时间便可,这样就能清楚地知道是否还有空闲时间去执行任务。
那么最后一步操做就是如何在渲染之后才去执行任务。这里就须要用到事件循环的知识了
想必你们都知道微任务宏任务的区别,这里就再也不赘述这部分的内容了。从上图中咱们能够发现,在渲染之后只有宏任务是最早会被执行的,所以宏任务就是咱们实现这一步的操做了。
可是生成一个宏任务有不少种方式而且各自也有优先级,那么为了最快地执行任务,咱们确定得选择优先级高的方式。在这里咱们选择了 MessageChannel
来完成这个任务,不选择 setImmediate
的缘由是由于兼容性太差。
到这里为止,requestAnimationFrame
+ 计算帧时间及下一帧时间 + MessageChannel
就是咱们实现 requestIdleCallback
的三个关键点了。
上文说了这么多,这一小节咱们未来梳理一遍调度的整个流程。
expriationTime
,高优先级的任务会打断低优先级任务port.postMessage(undefined)
,这样就能在渲染后立刻执行过时任务了requestAnimationFrame
启动定时器,在重绘前调用回调方法port.postMessage(undefined)
channel.port1.onmessage
会在渲染后被调用,在这个过程当中咱们首先须要去判断当前时间是否小于下一帧时间。若是小于的话就表明咱们尚有空余时间去执行任务;若是大于的话就表明当前帧已经没有空闲时间了,这时候咱们须要去判断是否有任务过时,过时的话无论三七二十一仍是得去执行这个任务。若是没有过时的话,那就只能把这个任务丢到下一帧看能不能执行了在将来,调度这个功能不会仅仅只有 React 才拥有。由于 React 已经尝试把调度这个模块单独抽离成一个库,这个库在将来可以被你们置入到本身的应用中去提升用户体验。
而且社区也有提案,但愿浏览器能自带这方面的功能,具体能够阅读 这个库。
阅读源码是一个很枯燥的过程,可是收益也是巨大的。若是你在阅读的过程当中有任何的问题,都欢迎你在评论区与我交流。
另外写这系列是个很耗时的工程,须要维护代码注释,还得把文章写得尽可能让读者看懂,最后还得配上画图,若是你以为文章看着还行,就请不要吝啬你的点赞。
最后,以为内容有帮助能够关注下个人公众号 「前端真好玩」咯,会有不少好东西等着你。