Skip to content
目录

commit 阶段

Renderer工作阶段被称为commit阶段

不同于render阶段可以被打断,commit阶段一旦开始就会执行到结束

在不同的环境下会使用不同的Renderer,本章将用 Web 开发最常见的ReactDOM来讲解

主要分为三个子阶段

  • BeforeMutation

    主要任务是:

    • mutation阶段做准备,包括
      • 执行getSnapshotBeforeUpdate生命周期
      • 计算布局
      • 准备更新队列
  • Mutation

    主要任务是:

    • 进行 DOM 元素的增、删、改
  • Layout

    主要任务是:

    • 执行某些特殊情况下的回调

每一个子阶段还要再细分为三个阶段:(xxx 表示上面三个子阶段)

  • commitXXXEffects()

  • commitXXXEffects_begin()

    向下遍历到第一个满足如下条件之一的"fiberNode"

    • 当前fiberNode的子fiberNode不包含"该子阶段对应的flags",即当前fiberNode是包含该子阶段对应 flag 的层级最低的fiberNode
    • 当前fiberNode不存在子fiberNode,即当前fiberNode是叶子节点

    然后会执行下面的函数

  • commitXXXEffects_complete()

    执行"flags对应操作",包含三个步骤

    • 对当前的fiberNode执行"flags"对应操作,即执行commitXXXEffectsOnFiber()
    • 如果当前fiberNode存在兄弟fiberNode,则对兄弟fiberNode执行commitXXXEffect_begin()
    • 如果不存在兄弟fiberNode,则对父fiberNode执行commitXXXEffects_complete()

在实际源码中,只有BeforeMutation阶段是符合的,其余两个阶段都有些出入,简单来说:

BeforeMutation阶段存在三个子阶段——commitBeforeMutationEffects()_begin()_complete

MutationLayout阶段都只有——commitXXXEffects()

Question-1:

部分子阶段都有一些“特有的操作”,具体哪个阶段是什么?

BeforeMutation

Answer-1(1):

这个阶段没有"特有的操作"

BeforeMutation阶段的目的是为Mutation阶段做准备

主要工作发生在commitBeforeMutationEffects_complete中的commitBeforeMutationEffectsOnFiber

对于不同的fiberNode.tag有不同的处理方法

  • FunctionComponent:如果存在Update flag,进入commitUseEffectEventMount()
  • ClassComponent:更新组件实例的propsstate,执行getSnapShotBeforeUpdate生命周期
  • HostRoot:执行clearContainer(finishedWork.stateNode.container),清空HostRoot挂载的内容,方便Mutation阶段渲染

Mutation

Answer-1(2):

删除 DOM 元素

DOM3 Events规范中有一个DOM API: MutationObserver

commitMutation阶段可以推断应该是操作DOM的行为

18.2.0 版本的源码中不存在 commitMutationEffects_begin()和 commitMutationEffects_complete()方法,由 commitMutationEffects 直接调用 commitMutationEffectsOnFiber,在这个函数中再调用 recursivelyTraverseMutationEffects

所谓 commitMutationEffects_begin 和 commitMutationEffects_complete 方法是 recursivelyTraverseMutationEffects 方法的两个代码段

  • 删除DOM节点

    render阶段的beginWork执行reconcile操作时添加了一个fiberNode.deletions数组,Mutation阶段将遍历这个数组,并执行commitDeletionEffects()方法删除 DOM 元素

    完整逻辑比较复杂,删除 DOM 元素还需要考虑很多逻辑

    • 子树中所有组件的 unmount 逻辑
    • 子树中所有 ref 属性的卸载操作
    • 子树所有 Effect 相关 Hook(比如 useLauoutEffects 回调)的执行
  • 插入、移动 DOM 元素

ts
function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes
) {
  // commitMutationEffects_begin()
  const deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      try {
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }

  // commitMutationEffects_complete()
  const prevDebugFiber = getCurrentDebugFiberInDEV();
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child !== null) {
      setCurrentDebugFiberInDEV(child);
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }
  setCurrentDebugFiberInDEV(prevDebugFiber);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

recursivelyTraverseMutationEffects会在commitMutationEffectsOnFiber中调用

简而言之,这部分代码会"递归的遍历"(正如recursivelyTraverseMutationEffects名字)fiberNode,为fiberNode兑现其flags

img_v2_3d46928c-c82f-47f2-b8c7-80a1f442127l


对于不同的fiberNode.tag有不同的处理方法,以HostComponentPlacement举例:

这里先介绍两个函数

getHostParentFiber()

从当前 fiberNode 向上遍历,获取第一个类型为 HostComponent、HostRoot、HostPortal 三者之一的祖先 fiberNode,其对应 DOM 元素是"执行 DOM 操作的目标元素的父级 DOM 元素"

getHostSibling()

向上寻找需要 insertBefore(node, before)函数需要的 before 节点

对于每一个节点,先调用getHostParentFiber()获取父级 DOM 元素,再调用getHostSibling()获取最低层级的无Placement flag的真实 DOM 元素

接下来的行为很简单,调用insertOrAppendPlacementNode(finishedWork, before, parent)

Mount场景:

  • 如果before节点存在,则将目标 DOM 元素插入before之前
  • 如果before节点不存在,则将目标 DOM 元素作为父 DOM 的最后一个元素插入

Update场景:

  • 如果before节点存在,则将目标 DOM 元素移至before之前

  • 如果before节点不存在,则将目标 DOM 元素移至同级最后

  • 更新 DOM 元素

    书上写执行 DOM 元素更新是在 commitWork()中,实际在 18.2.0 源码中更新部分内联在 commitMutationEffectOnFiber 里,最终执行的 commitUpdate()为更新操作

    render阶段的completeWork中,所有"变化属性的[key, value]"会保存在fiberNode.updateQueue,最终在commitUpdate中调用

    • updateProperty——不存在updatePayload/updateQueue
    • updatePropertiesWithDiff——存在updatePayload/updateQueue

    遍历并改变对于属性:

    • style 属性变化
    • innerHTML
    • 直接文本节点的变化
    • 其他元素属性

Layout

Answer-1(3)

OffscreenComponent 的显隐逻辑

在 18.2.0 源码中同样不存在 commitLayoutEffects_begin()和 commitLayoutEffects_complete(),都是在 commitLayoutEffects()中直接调用 commitLayoutEffectsOnFiber()

对于不同的fiberNode.tag有不同的处理方法:

  • 对于OffscreenComponent,执行OffscreenComponent的显隐操作
  • 对于ClassComponent,执行componentDidMount/Update方法
  • 对于FC,执行useLayoutEffectcallback

HostComponent类型的fiberNode.updateQueue最终会在Mutation阶段处理,但除了HostComponent类型外,其他类型fiberNode也存在updateQueue

  • ClassComponentthis.setState(dispatch, callback)中的callback
  • HostRoot:执行React.DOM.render(element, container, callback)中的callback

这两种情况下产生的callback会在commitLayoutEffectOnFiber中执行

Released under the MIT License.