// packages/react-reconciler/src/ReactFiberScheduler.js
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
// 调度的root是通过链表来串联的。
// 如果当前root不在链表中,则在链表末尾添加当前root
// 如果已在链表中,查看此次优先级是否更大,优先级更大就会更新此root的expirationTime。
addRootToSchedule(root, expirationTime);
// ...
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
// 如果优先级是同步,马上执行同步工作
performSyncWork();
} else {
// 根据 expirationTime 优先级去调度
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
接着React会尝试为这次“操作”生成一个带有expirationTime(过期时间)的callbackNode。在React内部,会用一个链表callbackList来管理callbackNode。除了React自身向callbackList添加callbackNode外,我们还可以通过React暴露的unstable_scheduleCallback api 向callbackList中添加callbackNode,来制定callback的过期时间。
export function processUpdateQueue<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
props: any,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
// ...
// Iterate through the list of updates to compute the result.
while (update !== null) {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
// This update does not have sufficient priority. Skip it.
if (newFirstUpdate === null) {
// This is the first skipped update. It will be the first update in
// the new list.
newFirstUpdate = update;
// Since this is the first update that was skipped, the current result
// is the new base state.
newBaseState = resultState;
}
// Since this update will remain in the list, update the remaining
// expiration time.
if (newExpirationTime < updateExpirationTime) {
newExpirationTime = updateExpirationTime;
}
} else {
// This update does have sufficient priority. Process it and compute
// a new result.
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
// ...
}
// Continue to the next update.
update = update.next;
}
// ...
queue.baseState = newBaseState;
queue.firstUpdate = newFirstUpdate;
queue.firstCapturedUpdate = newFirstCapturedUpdate;
workInProgress.expirationTime = newExpirationTime;
workInProgress.memoizedState = resultState;
}
在React中,我们大部分带有副作用的操作都会触发React的更新,而在Concurrent Mode模式中。React使用了一套全新的调度模式来进行应用的更新。时至今日,大家都知道React 的Concurrent Mode整个过程分为Render和Commit阶段。其中Render阶段是纯Javascript的运算,可中断。所以将Render阶段的运算分割到浏览器每一帧的空闲时间中去执行,从而减少JavaScript线程对页面UI渲染的阻塞,提高用户体验。但是说里面的一些细节,估计很多人都还比较懵懂,比如:
这篇文章就是尽量深入浅出的去展示笔者的总结,从一次完整的调度过程探索上面提到的种种问题。当然,人无完人,如果发现有什么不对的地方也欢迎指正。
调度前期准备
首先我们一些具有副作用的操作比如setState,会触发应用的调度更新。当我们执行这些操作的时候,React会为当前操作计算优先级,生成一个update对象保存到当前的FiberNode节点的updateQueue属性上。比如执行:
然后发起调度 scheduleWork(fiber, expirationTime)。这时候React首先会从触发操作的fibreNode节点开始回溯到Root节点,并且更新沿路经过的祖先节点fibreNode中的childExpirationTime属性,该属性在调度过程中代表着当前节点所有后代节点中最大的优先级。然后从当前Root中所有还在等待执行的任务中选择优先级最大的来发起调度。
在React应用中,我们可能不只一个Root节点。在React内部是通过链表来管理这些需要被调度的Root。在更新了祖先节点之后React就会将当前需要调度的Root添加到这个链表中,如果该Root已经存在就会尝试去更新链表上该Root的expirationTime:
接着React会尝试为这次“操作”生成一个带有expirationTime(过期时间)的callbackNode。在React内部,会用一个链表callbackList来管理callbackNode。除了React自身向callbackList添加callbackNode外,我们还可以通过React暴露的unstable_scheduleCallback api 向callbackList中添加callbackNode,来制定callback的过期时间。
所以callbackList可能具有不只一个callbackNode,他们会根据优先级由大到小排序。不过其中有且只有一个callbackNode的callback为performAsyncWork。
每当React准备创建一个performAsyncWork的callbackNode的时候,会查看callbackList中是否已经存在performAsyncWork的callbackNode,如果存在的话,就会对比它们的优先级。如果当前新建的优先级更高,就会将callbackList上的回调函数为performAsyncWork的callbackNode移除,然后将新建的callbackNode按优先级插入到callbackList中;否则取消新建callbackNode。通过unstable_scheduleCallback 添加的callbackNode也会按优先级进行插入。
React 会在每一帧的空闲时间里,遍历这个callbackList。依次执行第一个节点的回调(callback),直到当前时间片用完。被执行的callbackNode会从链表中移除。如果执行的callback不是performAsyncWork,那么就会尝试生成新的 callbackNode(performAsyncWork)来调整它在callbackList中的位置。如果执行的回调是performAsyncWork那么就会开始执行异步工作(遍历fiber tree等)。
当 flushFirstCallback 函数中就会执行回调函数performAsyncWork时,React就会对此Fiber Tree进行一轮异步调度
performAsyncWork 调用的是performWork(NoWork, true)。(ps:对应的performSyncWork调用的是performWork(Sync, true))
执行异步工作
当前的performAsyncWork中React会持续的从收集到的Roots链表中找出优先级最高的Root,然后对该Root执行调度工作。直到将所有Roots的工作都执行完毕,或者有更高的优先级的performAsyncWork需要执行才会结束。
这里的逻辑很清晰,就是如果render阶段如果还没开始或还没有结束就进入render阶段,如果render阶段已经完成,那就可以进入commit阶段了。
进入Render 阶段
首先我们知道,React的Concurrent Mode模式中采用了一种类似双缓冲的方式以便在必要的时候去恢复一些数据。也就是对于我们正在调度的FiberTree,React会同时维护两份。一份叫做current tree,一份叫做 workInProgress tree。current tree代表UI的当前状态,workInProgress tree代表新的UI状态。而render阶段遍历调度的过程就是对workInProgress tree的构造和打tag的过程。
Render阶段就是对当前选定的Root节点(FIber Tree)进行深度优先遍历,同时根据前面提到的每个FiberNode节点的childExpirationTime来查看FiberNode的后代节点中是否还有需要在这次调度中执行的更新工作,如果没有就不会继续遍历FiberNode的后代节点。整个遍历的过程涉及到了多个方法。我们以ClassComponent为例子来了解一下整个遍历过程。
renderRoot 方法中,调用workLoop方法来对FiberTree进行遍历。nextUnitOfWork代表下一个进行调度的FiberNode。要了解这个过程首先你需要了解深度优先遍历,以深度优先遍历的思想来看下面的内容。继续来看看performUnitOfWork:
深度优先遍历在遍历每个节点的时候是怎么样的逻辑呢?查看是否有子节点,有的话继续遍历子节点。没有的话首先看看有没有兄弟节点,都没有的话就会回溯到父节点。这里的逻辑也一样:
再来看看 beginWork、completeUnitOfWork这两个关键函数做了什么。首先不同类型的组件做要的具体工作是不一样的,所以我们看到beginWork方法里用了一个switch条件语句来区分每一类组件的具体工作,这里就按ClassComponent来看一下。
React对FiberTree的深度优先遍历并不是盲目的进行遍历,当遍历到一些在此轮调度中不需要更新的节点时,会在 bailoutOnAlreadyFinishedWork 方法中根据上文中提到了childExpirationTime(后代节点中所有任务中的最大的优先级)来判断是否需要返回子代节点作为下一个工作单元,也就是是否需要继续往下遍历。如果当前节点需要进行更新操作的话,会执行对于的更新方法,ClassComponent 对应updateClassComponent 。updateClassComponent具体做了什么就不展开仔细看代码了,毕竟本文重点是Concurrent Mode模式本身,这里就简单提一下 updateClassComponent 做了的工作有:
在处理updateQueue的时候,会筛选出优先级足够的update执行来更新state。剩下的update会成为一个新的updateQueue,其中最大的优先级作为该fiberNode的新优先级
执行完beginWork之后,拿到nextUnitOfWork。如果nextUnitWork为空,就会执行completeUnitOfWork方法,因为当前节点不需要再往下遍历了,是时候去遍历它的兄弟节点或回溯到父节点了,所以可以complete这个工作单元。来看一下completeUnitOfWork:
首先执行completeWork,然后正如前面所说的如果有兄弟节点就返回兄弟节点(siblingFiber)返回作为下一个工作单元;如果没有则返回父节点(returnFiber)作为下一个工作单元。同时如果当前节点有更新,就会将当前节点添加到EffectList中。最终EffectList中会包含所有具有更新的FiberNode,在commit阶段就会遍历这个EffectList,根据FiberNode上打的Tag,执行对应的操作。
completeWork类似beginWork,也是针对不同类型的组件执行对应的操作,ClassComponent的话就是对context的更新,这里就不详细看了。
到目前为止就是一轮 workLoop。React会在每一轮workLoop结束后检查时间片是否过期以及是否有更高优先级的任务插队。这就衍生出了两个问题:
时间片用完,下一帧如何继续工作?
前文讲到,React会为每个调度任务创建一个callbackNode按优先级插入到callbackList中,然后依次执行和移除第一个callbackNode。同时workLoop中通过nextUnitWork来指向下一个工作单元。
当时间片用完的时候,就会跳出while循环。但是React并不会重制nextUnitOfWork的值,而是让它继续指向当次调度任务的下一个工作单元。同时,会为当前未执行完的调度任务(performAsyncWork)重新创建一个callbackNode(performAsyncWork),并插入到callbackList中。
然后在下一帧执行新的performAsyncWork时,沿用nextUnitOfWork的值,从上次结束的地方继续执行调度工作。
较低任务执行过程中出现高优先级任务
高优先级任务中断低优先级任务有三种情况,第一种是在 !isWorking 状态,也就是既不在render阶段也不在commit阶段。第二种是在isRendering状态,也就是render阶段。第三种是在isCommitting,也就是commit阶段。这里主要涉及到了两个问题:
处于 !isWorking 状态情况下
如果不在render阶段也不在commit阶段,React会重置nextUnitOfWork等过程变量,然后重新requestWork。生成新的callbackNode(performAsyncWork)。在当前帧结束时,由于nextUnitOfwork === null 所以会结束原本执行的较低优先级的performAsyncWork,在下一帧的空闲时间中执行新的高优先级的performAsyncWork。
处于render阶段的情况下
当处于Render阶段的时候,出现了高优先级任务的话。首先高优先级任务会例行的更新Roots链表。然后在performWork中,React会找出Roots链表中最高优先级的Roots进行调度。并且在renderRoot函数中判断到当前执行的Root的expirationTime高于前一次的话,会回resetStack()重置nextUnitOfWork指向Root节点。进而开始执行高优先级的任务。
处于commit阶段的情况下
如果当前处于commit阶段,说明performAsyncWork的render阶段已经结束,nextUnitOfWork已经为空。所以不需要resetStack(),继续requestWork,创建一个新的callbackNode(performAsyncWork)插入到callbackList,等待它执行。具体逻辑可以看上面的scheduleWork。
被打断的低优先级任务如何重新恢复执行?
如上文所说,在workLoop中遍历FiberTree执行工作的时候,我们同时在构建workInProgress Tree。当我们低优先级任务执行到一半,也就是workInProgress Tree被构建到一半的时候,我们被高优先级插。nextUnitOfWork被置为Null,React就会从头去构建我们的workInProgress Tree。
因为我们执行update的操作是在workInprogress上进行的,即使在低优先级任务中有update被执行且移出updateQueue,这时都会从current中重新恢复回来。然后高优先级任务 进入commitRoot阶段时 ,会重新findNextExpirationTimeToWorkOn,fiberRoot的expirationTime就会恢复回之前低优先级任务的expirationTIme。
于是低优先级任务就会在下一轮performWorkOnRoot中重新由头开始执行。之前已经执行过的fiberNode会被二次执行。
问题
二次执行的fiberNode对应组件的部分生命周期(componentWillMount,componentWillReceiveProps,componentWillUpdate)会多次执行。React的做法是考虑到这些生命周期本身使用起来也有比较容易写出问题问题,所以当前逐步给这些生命周期添加“UNSAFE_”前缀。
v16.9.0之后的版本直接使用不带“UNSAFE_”的这三个生命周期的话会得到警告,同时React团队保证新的方法名(如 UNSAFE_componentWillMount)在 React 16.9 和 React 17.x 中,仍可以继续使用。官网对应信息