Skip to content

⚠️ Important Notice

This post was last updated on: which was . Please pay attention to its timelines.

Reconciler

进入具体的说明之前,先解释一下常说的render阶段commit阶段都和源码有什么关系:

  • render阶段就是在react-reconciler包中的执行逻辑,具体一些就是包中封装的fiber构造循环,会将这个函数传给scheduler进行调度执行。
  • commit阶段就是将生成的fiber传给render进行渲染转为真实dom
  • render和commit统称为work
  • scheduler中只进行优先级的调度

react-reconciler包中有一个fiber构造循环ReactFiberWorkLoop,执行这个循环之后就可以生成fiber树,最后将生成好的fiber树给render渲染器进行渲染。

reconciler

该图来自这里

进入react-reconciler流程

react-reconciler包暴露出了scheduleUpdateOnFiber这个函数,外面通过执行该函数,来进入reconciler的流程。

该函数的主要作用就是来执行注册调度任务的函数ensureRootIsScheduled。当然也有其他的一些作用,比如Suspence的处理。

注册调度任务ensureRootIsScheduled

scheduleUpdateOnFiber函数中执行ensureRootIsScheduled来注册调度任务。

该函数的主要作用就是用来给fiber节点注册调度任务的,会在每一次更新时以及退出任务时执行。

这里的主要逻辑有两部分:

  1. 判断是否需要调度新的任务,没有新的话会直接return
  2. 有新的任务会注册schedule task,准备进入scheduler逻辑中
    1. 将task封装到函数performSyncWorkOnRoot或者performConcurrentWorkOnRoot
    2. 然后将这个函数放到scheduleCallback或者scheduleLegacySyncCallback中作为参数,等待scheduler的调度
    3. scheduler调度会执行这个函数,最后将函数返回值(生成的fiber树)绑定到了root.callbackNode

render阶段

render的定义并没有明确的官方说明,只是人为划分便于理解react的运行机制

一般从performSyncWorkOnRoot或者performConcurrentWorkOnRoot开始的

performConcurrentWorkOnRoot

该函数是每个并发任务的入口点。大致的任务如下:

  1. 首先会清除等待状态的effects(passive effects),因为可能会在effects中取消本次任务

    passive effects是指那些不会阻塞主线程的副作用,如useEffect中执行的副作用。

  2. 然后通过getNextLanes拿到本次任务的优先级

  3. 通过renderRootConcurrentrenderRootSync来构造当前的fiber树

    1. 某些特殊情况(CPU时间过长或默认同步更新模式)禁用了时间切片就renderRootSync,否则renderRootConcurrent
  4. 进行错误处理,防止构造fiber树的过程中有错误

  5. 输出生成的fiber树,将root.current.alternate挂载到root.finishedWork

  6. 然后再退出之前判断是否还有新任务,有的话继续执行ensureRootIsScheduled

  7. performConcurrentWorkOnRoot中还会对中断进行处理,一旦中断,会将performConcurrentWorkOnRoot.bind(null, root)返回,等待下一次scheduler继续调用

performSyncWorkOnRoot

该函数是同步任务的入口点,是不需要经过scheduler的调度的。

该函数的任务和performConcurrentWorkOnRoot差不多,不过也有一些差别:

  1. 直接通过renderRootSync来构造fiber树,之后进行错误处理,在之后也是输出生成的fiber树
  2. 不过会直接commitRoot,开始commit阶段
  3. 然后在退出之前判断是否有新任务

performUnitOfWork

上面的构造fiber树是通过renderRootSync或者renderRootConcurrent来的,在这两个函数中又是调用workLoopSync或者workLoopConcurrent

这两个中又调用了performUnitOfWork,所以performUnitOfWork才是真正生成fiber树的逻辑。

workLoopSync和workLoopConcurrent的唯一区别,就在是否会中断

通过shouldYield函数来判断是否会中断

js
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中的会执行beginWorkcompleteUnitOfWork

会对每一个节点都执行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

js
/**
  接受三个参数
  current:workInProgress.alternate,当前组件上一次更新时的fiber节点
  workInProgress:当前组件对应的fiber节点
  renderLanes: 优先级
*/
function beginWork(
  current,
  workInProgress,
  renderLanes
){
  // ...
}

根据是否有上一次的fiber来分为两个部分:

js
// 因为current是上一次的fiber,第一次挂载的时候肯定是没有的,之后肯定是有的,所以可以通过这个来区分。
if(current !== null) {
  // update阶段
  // 经过diff来尝试复用current的fiber节点
}else {
  // mount阶段
  // 根据workInProgress.tag来创建不同的fiber节点
}

在创建fiber节点时(只分析几个常用的tag,如FunctionComponent),会调用reconcileChildren函数

reconcileChildren(diff)

这个函数也根据current分为两个部分,分别调用了mountChildFibersreconcileChildFibers函数

这两个函数其实是一个函数,只是传参不同,都是根据tag来创建fiber,不同的是reconcileChildFibers会为生成的fiber打上**effectTag属性**。

这个函数的大致逻辑就是常说的diff过程

可以看到在构建新树是就走了diff的流程(更新时,初始化不会),diff只是对旧树的复用策略。

effectTag是React的一种优化方式:

因为到目前为止都是在内存中进行的,等到commit阶段,才会去执行DOM操作。这些操作都保存在effectTag中,不同的操作对应不同的effectTag。

mount时,是在根节点添加对应的插入tag,避免了每个子节点都执行一遍插入操作。

经过该函数处理之后,会将生成的子fiber节点挂载到workInProgress.child

小结

经过beginWork的处理,遍历到叶子节点,完成了部分fiber树的构造(完成了child指针的指向),到目前为止的大致流程可以用下图来总结一下:

beginWork流程图

上图来自卡颂老师的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;否则就一直循环,直到遍历结束。

js
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是一个环形链表,有一个firstEffectlastEffect,中间的是有副作用的子节点

为什么是向上遍历的过程中,这样能收集到所有子节点的副作用

commit阶段

经过render阶段的处理,已经有了更新后的fiber树,在commit阶段会将fiber树传给renderer渲染器渲染到页面上,具体就是执行commitRoot(root)

render阶段有两个入口,performConcurrentWorkOnRoot以及performSyncWorkOnRoot

都是调用commitRoot进入commit阶段

commit阶段大致分为三个部分:

  1. 执行DOM操作之前before mutation:对应的执行了commitBeforeMutationEffects

    该阶段的主要做了:

    • 该阶段还没有操作真实DOM,会执行getSnapshotBeforeUpdate获取DOM快照
    • 还会异步调用useEffect。异步的原因是防止同步执行阻塞后面DOM的渲染
  2. 执行DOM操作mutation:对应的执行了commitMutationEffects

    该阶段的工作:

    • 判断是否需要清空ref

    • 根据effectList来操作真实DOM

  3. 执行DOM操作之后layout:对应的执行了commitLayoutEffects

    该阶段DOM已经更新完成,所以主要工作是一些后续的处理:

    • 对于类组件,会执行setState的callback函数;对于函数组件,会同步执行useLayoutEffect
    • 如果有ref,会重新赋值ref

可以看到,ref的赋值是在DOM更新之后的,所以需要格外注意ref的获取。

当然了,在执行DOM操作之前以及执行DOM操作之后还是有很多其他工作的。

image-20240903171043574

图来自掘金小册

总结

render阶段的beginWork/completeUnitOfWork将需要更新/创建的节点转为了fiber树,期间进行了diff尝试复用旧fiber节点,并且给fiber节点添加了需要操作的tag

之后进入commit阶段进行处理,commit阶段调度了effect/layouteffect,并且根据fiber树中的tag进行了对应的DOM操作,然后在layout前切换了缓存树。

上一次更新: