Reconciler
进入具体的说明之前,先解释一下常说的
render阶段、commit阶段都和源码有什么关系:
render阶段就是在react-reconciler包中的执行逻辑,具体一些就是包中封装的fiber构造循环,会将这个函数传给scheduler进行调度执行。commit阶段就是将生成的fiber传给render进行渲染转为真实dom- render和commit统称为work
scheduler中只进行优先级的调度
react-reconciler包中有一个fiber构造循环ReactFiberWorkLoop,执行这个循环之后就可以生成fiber树,最后将生成好的fiber树给render渲染器进行渲染。

该图来自这里
进入react-reconciler流程
react-reconciler包暴露出了scheduleUpdateOnFiber这个函数,外面通过执行该函数,来进入reconciler的流程。
该函数的主要作用就是来执行注册调度任务的函数ensureRootIsScheduled。当然也有其他的一些作用,比如Suspence的处理。
注册调度任务ensureRootIsScheduled
在scheduleUpdateOnFiber函数中执行ensureRootIsScheduled来注册调度任务。
该函数的主要作用就是用来给fiber节点注册调度任务的,会在每一次更新时以及退出任务时执行。
这里的主要逻辑有两部分:
- 判断是否需要调度新的任务,没有新的话会直接return
- 有新的任务会注册schedule task,准备进入
scheduler逻辑中- 将task封装到函数
performSyncWorkOnRoot或者performConcurrentWorkOnRoot - 然后将这个函数放到
scheduleCallback或者scheduleLegacySyncCallback中作为参数,等待scheduler的调度 scheduler调度会执行这个函数,最后将函数返回值(生成的fiber树)绑定到了root.callbackNode上
- 将task封装到函数
render阶段
render的定义并没有明确的官方说明,只是人为划分便于理解react的运行机制一般从
performSyncWorkOnRoot或者performConcurrentWorkOnRoot开始的
performConcurrentWorkOnRoot
该函数是每个并发任务的入口点。大致的任务如下:
首先会清除等待状态的effects(
passive effects),因为可能会在effects中取消本次任务passive effects是指那些不会阻塞主线程的副作用,如
useEffect中执行的副作用。然后通过
getNextLanes拿到本次任务的优先级通过
renderRootConcurrent或renderRootSync来构造当前的fiber树- 某些特殊情况(
CPU时间过长或默认同步更新模式)禁用了时间切片就renderRootSync,否则renderRootConcurrent
- 某些特殊情况(
进行错误处理,防止构造fiber树的过程中有错误
输出生成的fiber树,将
root.current.alternate挂载到root.finishedWork然后再退出之前判断是否还有新任务,有的话继续执行
ensureRootIsScheduledperformConcurrentWorkOnRoot中还会对中断进行处理,一旦中断,会将performConcurrentWorkOnRoot.bind(null, root)返回,等待下一次scheduler继续调用
performSyncWorkOnRoot
该函数是同步任务的入口点,是不需要经过scheduler的调度的。
该函数的任务和performConcurrentWorkOnRoot差不多,不过也有一些差别:
- 直接通过
renderRootSync来构造fiber树,之后进行错误处理,在之后也是输出生成的fiber树 - 不过会直接
commitRoot,开始commit阶段 - 然后在退出之前判断是否有新任务
performUnitOfWork
上面的构造fiber树是通过renderRootSync或者renderRootConcurrent来的,在这两个函数中又是调用workLoopSync或者workLoopConcurrent,
这两个中又调用了performUnitOfWork,所以performUnitOfWork才是真正生成fiber树的逻辑。
workLoopSync和workLoopConcurrent的唯一区别,就在是否会中断
通过
shouldYield函数来判断是否会中断
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}在performUnitOfWork中的会执行beginWork和completeUnitOfWork
会对每一个节点都执行beginWork,遍历到叶子结点之后会执行completeUnitOfWork。
会从
rootFiber()开始深度遍历,每一个fiber节点执行beginWork,根据传入的fiber节点创建子fiber节点,然后将这两个fiber节点连接起来,到遍历到叶子节点时,开始往回执行,即completeUnitOfWork。遍历到
叶子节点,会给当前fiber节点执行completeUnitOfWork,然后判断是否存在兄弟节点sibling,有的话进入其递阶段,如果没有的话进入父节点return的归阶段。这两个交替执行,直到再次回到
rootFiber,至此会生成一整颗fiber树。
在performUnitOfWork中会操作workInProgress,将生成的子fiber节点赋值给workInProgress,然后将已创建的fiber节点与workInProgress连接起来。
构造fiber树的具体逻辑
fiber树的构建是通过递归来生成的,在向下遍历的时候为每个节点执行beginWork,遍历到叶子节点时向上返回执行completeUnitOfWork,经过这两个函数的不断递归处理,最终会生成一个fiber树。
beginWork的具体逻辑
根据fiber树上的child指针向下遍历,会执行函数组件,实例化类组件,diff调和子节点,添加effectTag。
/**
接受三个参数
current:workInProgress.alternate,当前组件上一次更新时的fiber节点
workInProgress:当前组件对应的fiber节点
renderLanes: 优先级
*/
function beginWork(
current,
workInProgress,
renderLanes
){
// ...
}根据是否有上一次的fiber来分为两个部分:
// 因为current是上一次的fiber,第一次挂载的时候肯定是没有的,之后肯定是有的,所以可以通过这个来区分。
if(current !== null) {
// update阶段
// 经过diff来尝试复用current的fiber节点
}else {
// mount阶段
// 根据workInProgress.tag来创建不同的fiber节点
}在创建fiber节点时(只分析几个常用的tag,如FunctionComponent),会调用reconcileChildren函数
reconcileChildren(diff)
这个函数也根据current分为两个部分,分别调用了mountChildFibers和reconcileChildFibers函数
这两个函数其实是一个函数,只是传参不同,都是根据tag来创建fiber,不同的是reconcileChildFibers会为生成的fiber打上**effectTag属性**。
这个函数的大致逻辑就是常说的
diff过程可以看到在构建新树是就走了
diff的流程(更新时,初始化不会),diff只是对旧树的复用策略。
effectTag是React的一种优化方式:因为到目前为止都是在内存中进行的,等到commit阶段,才会去执行DOM操作。这些操作都保存在
effectTag中,不同的操作对应不同的effectTag。mount时,是在根节点添加对应的插入tag,避免了每个子节点都执行一遍插入操作。
经过该函数处理之后,会将生成的子fiber节点挂载到workInProgress.child上
小结
经过beginWork的处理,遍历到叶子节点,完成了部分fiber树的构造(完成了child指针的指向),到目前为止的大致流程可以用下图来总结一下:

上图来自卡颂老师的React技术揭秘
completeUnitOfWork的具体逻辑
是向上归并的过程,会先判断sibling兄弟节点,没有的话返回return父级,直到返回fiberRoot。期间收集了effectList;如果是初始化还是创建DOM。
遍历到叶子节点之后会判断是否有兄弟节点,没有的话执行completeUnitOfWork函数,依次向上遍历,为fiber树添加return指针;有兄弟节点会进入其beginWork的过程,并给fiber树添加sibling指针。
函数的核心逻辑在completeWork中,也是根据传入的tag不同做不同的操作。特别介绍一下HostComponent对应的操作(原生DOM对应的Fiber节点),大致做了这么几件事情:
update时:将需要更新的update对象保存到了updateQueue中,并给节点打上更新的tag
mount时:给fiber节点创建出对应的真实DOM(调用了render渲染器的方法),然后调用appendAllChildren函数,将子节点插入到已生成的父级DOM节点上,通过这个操作可以在commit阶段只执行一次插入操作,就将整个DOM树插入到页面上。
简单来说,在completeUnitOfWork阶段,收集了将要执行的DOM操作即副作用。
注意点
在向上遍历的过程中是如何避免到上级节点再次进入beginWork的?
在completeUnitOfWork中有一个循环,在这个循环中判断的,如果有兄弟节点就直接返回这个兄弟节点,进入兄弟节点的beginWork;否则就一直循环,直到遍历结束。
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
workInProgress = unitOfWork
do {
const siblingFiber = workInProgress.sibling;
if(siblingFiber !== null) return siblingFiber
const returnFiber = workInProgress.return
workInProgress = siblingFiber;
} while(workInProgress !== null)
return null
}副作用的收集
上面构造Fiber树的过程会打上各种的tag标记,与其在遍历结束之后再遍历一遍拿到所有的副作用节点,不如直接在构造的遍历中收集副作用。
是在向上遍历的过程中将这些副作用收集在effectList中,effectList是一个环形链表,有一个firstEffect和lastEffect,中间的是有副作用的子节点
为什么是
向上遍历的过程中,这样能收集到所有子节点的副作用
commit阶段
经过render阶段的处理,已经有了更新后的fiber树,在commit阶段会将fiber树传给renderer渲染器渲染到页面上,具体就是执行commitRoot(root)。
render阶段有两个入口,performConcurrentWorkOnRoot以及performSyncWorkOnRoot。都是调用
commitRoot进入commit阶段的
commit阶段大致分为三个部分:
执行
DOM操作之前before mutation:对应的执行了commitBeforeMutationEffects该阶段的主要做了:
- 该阶段还没有操作真实DOM,会执行
getSnapshotBeforeUpdate获取DOM快照 - 还会
异步调用useEffect。异步的原因是防止同步执行阻塞后面DOM的渲染
- 该阶段还没有操作真实DOM,会执行
执行
DOM操作mutation:对应的执行了commitMutationEffects该阶段的工作:
判断是否需要清空ref
根据effectList来操作真实DOM
执行
DOM操作之后layout:对应的执行了commitLayoutEffects该阶段DOM已经更新完成,所以主要工作是一些后续的处理:
- 对于类组件,会执行setState的callback函数;对于函数组件,会同步执行
useLayoutEffect - 如果有ref,会重新赋值ref
- 对于类组件,会执行setState的callback函数;对于函数组件,会同步执行
可以看到,ref的赋值是在DOM更新之后的,所以需要格外注意ref的获取。
当然了,在执行DOM操作之前以及执行DOM操作之后还是有很多其他工作的。

图来自掘金小册
总结
render阶段的beginWork/completeUnitOfWork将需要更新/创建的节点转为了fiber树,期间进行了diff尝试复用旧fiber节点,并且给fiber节点添加了需要操作的tag。
之后进入commit阶段进行处理,commit阶段调度了effect/layouteffect,并且根据fiber树中的tag进行了对应的DOM操作,然后在layout前切换了缓存树。