React也使用了好一段时间,最近才有空把源码阅读一下(真是惭愧),由于项目用的React版本比较老,因此找的版本也就对应项目使用的版本恰好是16.8.6,不过React源码分析的文章已经不少了,并且有不少也质量很高,因此这里仅仅当作本身的笔记做为记录吧。react
在还没接触React的时候就已经了解到React16的一些新特性:协程,分片等,那个时候恰好接触到go语言对React的协程一直有几个疑问,会不会跟go语言的协程同样有调度器,React的调度单位是怎样的,Fiber是怎样抢占,中断和恢复执行的?git
React的调度器其实代码量并很少,仅仅只有700多行,而后核心功能就是如下两点:github
Scheduler目的是为了让任务都在每一帧的Idle阶段来执行,利用的是每帧空闲时间,而不阻塞浏览器的布局和绘制;
那么为何不在requestAnimationFrame阶段来执行尼?咱们都知道raf会在浏览器布局和绘制以前执行,但React是根本不知道浏览器接着后面布局和绘制须要消耗多少时间,因此在raf阶段处理是很难估计该预留多少时间本身去执行,而后让回给浏览器。api
那么为何不使用requestIdleCallback来控制在每帧的Idle阶段来执行尼?一开始React确实是这么干,可是后面由于requestIdleCallback的一些问题,并且新的api也有兼容性问题。浏览器
那么如今的新办法是如何处理的尼,首先用requestAnimationFrame先触发一个anmiationTick,这里有两个做用:第一能够预估每帧大概的时间;第二等anmiationTick触发时再用postMessage触发一个宏任务,这样这个宏任务就会在浏览器的布局和绘制以后执行,等同于在idle阶段执行了,固然这个宏任务里面还须要判断当前帧时间是否没有了(可是若是任务已经超时了无论还有没有时间剩下也是会执行的),这个判断就利用第一点获取的帧时间来进行的,若是没有剩余时间了就再触发一次animationTick,重复一次整个过程。异步
而每一个调度的任务都会带有一个优先级priorityLevel,这个优先级是指:源码分析
var ImmediatePriority = 1; var UserBlockingPriority = 2; var NormalPriority = 3; var LowPriority = 4; var IdlePriority = 5;
这个优先级很重要,涉及到当前任务和这个任务执行时派生的任务的超时时间计算。布局
另一些杂七杂八的点:post
requestAnimationTime有个缺点就是页面被隐藏的时候,有可能不执行,因此React采用了一个处理办法:code
var requestAnimationFrameWithTimeout = function(callback) { // schedule rAF and also a setTimeout rAFID = localRequestAnimationFrame(function(timestamp) { // cancel the setTimeout localClearTimeout(rAFTimeoutID); callback(timestamp); }); rAFTimeoutID = localSetTimeout(function() { // cancel the requestAnimationFrame localCancelAnimationFrame(rAFID); callback(getCurrentTime()); }, ANIMATION_FRAME_TIMEOUT); };
利用setTimeout来兜底,这样就万无一失了。
React的Scheduler算是一个独立的包,彻底没有包含React其余内容,因此也很难回答我开头的疑问,究竟它的调度单位是什么,Fiber是怎样抢占和恢复的。
直接来到ReactFiberScheduler.js,scheduleWork方法:
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { const root = scheduleWorkToRoot(fiber, expirationTime); // 1 if ( !isWorking && nextRenderExpirationTime !== NoWork && expirationTime > nextRenderExpirationTime ) { resetStack(); // 2 } markPendingPriorityLevel(root, expirationTime); // 3 if ( !isWorking || isCommitting || nextRoot !== root ) { // 4 const rootExpirationTime = root.expirationTime; requestWork(root, rootExpirationTime); } }
分四步来分析这个方法:
标记ReactFiberRoot的优先级,在我一开始的源码阅读中,我一开始简单认为expirationTime就是超时时间,实际上还包含优先级的意思,并且源码中更多时候表明的是优先级,越往前调度的任务优先级越高,越日后就越低,高于当前的帧的deadline,都表示这些任务是过时任务,过时任务哪怕当前帧时间不够都会所有调度执行。而ReactFiberRoot上会有好几个字段跟优先级相关:
earliestPendingTime latestPendingTime earliestSuspendedTime latestSuspendedTime latestPingedTime nextExpirationTimeToWorkOn expirationTime
开头那5兄弟一开始真的让我感受有点懵逼,一开始彻底不知道为何须要5个字段来标记优先级,在我认知里面每一个节点仅仅须要一个expirationTime标记自身的优先级和childExpirationTime标记子级最高的优先级就足够了;可是后面多阅读几遍代码就发现它的意图,在这些优先级里面也是有分类的:Pending > Pinged > Suspended;React老是会先把Pending优先级任务清理完才会清理后面的任务,而Pending优先级表明的是尚未执行过的任务。
而nextExpirationTimeToWorkOn和expirationTime通常状况下它们是相等,可是还有其余状况是不同(就是处理Suspended类型优先级的时候),nextExpirationTimeToWorkOn表明的是准备处理的优先级,大于或者等于这个优先级的fiber节点都会获得处理;expirationTime固然表明的是root总体的优先级,会用来跟其余root来比较,看谁应该更优先处理。
不过总的来讲应该是React为了支持Suspend这个特性引入的复杂度,固然复杂度还不仅这里,若是把Suspend相关的代码去掉,总体会很清爽,Suspend这个特性是否有这么大的价值,在后面的章节再具体分析一下。