共计 3830 个字符,预计需要花费 10 分钟才能阅读完成。
开篇
React 也应用了好一段时间,最近才有空把源码浏览一下(真是羞愧),因为我的项目用的 React 版本比拟老,所以找的版本也就对应我的项目应用的版本刚好是 16.8.6,不过 React 源码剖析的文章曾经很多了,而且有很多也品质很高,所以这里仅仅当做本人的笔记作为记录吧。
问题
在还没接触 React 的时候就曾经理解到 React16 的一些新个性:协程,分片等,那个时候刚好接触到 go 语言对 React 的协程始终有几个疑难,会不会跟 go 语言的协程一样有调度器,React 的调度单位是怎么的,Fiber 是怎么抢占,中断和复原执行的?
Scheduler
React 的调度器其实代码量并不多,仅仅只有 700 多行,而后外围性能就是以下两点:
- 利用 requestAnimationFrame 来动静计算每帧的工夫
- 利用 MessageChannel 来创立宏工作,来执行调度的工作
Scheduler 目标是为了让工作都在每一帧的 Idle 阶段来执行,利用的是每帧闲暇工夫,而不阻塞浏览器的布局和绘制;
那么为什么不在 requestAnimationFrame 阶段来执行尼?咱们都晓得 raf 会在浏览器布局和绘制之前执行,但 React 是基本不晓得浏览器接着前面布局和绘制须要耗费多少工夫,所以在 raf 阶段解决是很难预计该预留多少工夫本人去执行,而后让回给浏览器。
那么为什么不应用 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;
这个优先级很重要,波及到当前任务和这个工作执行时派生的工作的超时工夫计算。
另外一些杂七杂八的点:
-
requestAnimationTime 有个毛病就是页面被暗藏的时候,有可能不执行,所以 React 采纳了一个解决方法:
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 来兜底,这样就十拿九稳了。
- 而判断工作是否要过期,就要不停应用 peformance.now/Date.now 来获取以后工夫,而获取以后工夫个别也是一个零碎调用,频繁调用也是一种耗费;所以会利用 timeoutTime 来记录最近调度的一个工作的超时工夫,执行的时候如果判断曾经过期,则认为调度的工作列表外面存在过期工作,先把所有的过期工作清理完,所以整个过程只须要获取一次以后工夫就能够了,缩小获取以后工夫的耗费。
与 React 联合
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);
}
}
分四步来剖析这个办法:
- 首先更新 current 树和 workInProgress(如果存在)树 fiber 节点的 childExpirationTime,而后返回 root;这里简略阐明一下,current 和 workInProgress 树,current 树就代表着以后显示在用户背后的 fiber 节点树,workInProgress 树就是正在做更新的 fiber 节点树,毕竟当初的 React 是 diff 过程曾经能够异步的,如果只有一棵树,那就很有可能呈现更新到一半就显示给用户了,综合起来这种也算是游戏中罕用的双缓冲技术的利用;而 childExpirationTime 代表的是子级节点中最高的优先级,能够用在前面更新的时候疾速判断子级节点需不需要更新,因为每次调度更新的时候,都是从 ReactFiberRoot 往下遍历,所以这个属性就很重要了,能够提高效率。
- 如果不在更新过程中,呈现了一种优先级更高的更新工作,也就是抢占,这个时候会重置执行栈,之前更新到一半的工作后果都会被摈弃,等下次调度从新开始。
-
标记 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 这个个性是否有这么大的价值,在前面的章节再具体分析一下。 - 如果不在更新过程中,或者这个 Root 跟以后调度的 Root 不一样,把这个 Root 也退出到调度队列外面,如果优先级比以后调度的 Root 更高,就会申请一次新的调度。
集体总结
- React 利用 Scheduler 寻找一个适合的执行机会
- 在这个适合的机会外面 ReactFiberRoot 就是它的调度单位
- 如果被更高优先级的工作打断,React 会非常简单粗犷放弃掉之前实现到一半的更新,期待前面调度重头再开始