【一行行看源码】Commit 阶段全流程讲解!

前言

随着 render 阶段的完成,也意味着在内存中构建 workInProgress Fiber 的所有工作都已经完成,这其中包括了对 Fiber 节点的 updatediffflags 标记subtreeFlags(effectList) 的收集等操作 我们知道,在 render 阶段,会将需要更新的节点标记上 flags (effectTag),在 completeWork 阶段会形成 effectList 链表,连接所有需要被更新的节点

为了将这些需要更新的节点应用到真实 DOM 上却不需要遍历整棵树,在 commit 阶段,会通过遍历这条 EffectList 链表,执行对应的操作,来完成对真实 DOM 的更新,这也叫做 mutation,即 DOM 节点的增删改操作

在新版本中不再需要 effectList 链表了,而是通过 rootFiber 自下而上调和的方式处理这些标志,执行对应的操作,来完成对真实 DOM 的更新

接下来我们带着以下的问题一起去思考 commit 阶段的工作!

  • commit 阶段分为几个子阶段,都做了什么事情?
  • useEffect 钩子是如何被调度的?

commit 阶段会做以下这些事情

  • 对一些生命周期和副作用钩子的处理,比如 类组件的 componentDidMountcomponentDidUpdate,函数组件的 useEffectuseLayoutEffectuseInsertionEffect
  • 另一方面,在一次 Update 中,进行添加节点(Placement)、更新节点(Update)、删除节点(Deletion)、同时有对 ref 的处理等。

commit 阶段的入口在 commitRoot 函数,在这里会发起一个最高优先级的调度任务,然后调用 commitRootImpl 函数来处理副作用,将最新的 Fiber 树同步到 DOM 上

function commitRoot(root) {
  const previousUpdateLanePriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  try {
    ReactCurrentBatchConfig.transition = 0;
    setCurrentUpdatePriority(DiscreteEventPriority); // 最高优先级调度
    commitRootImpl(root, previousUpdateLanePriority); // commit 主流程
  } finally {
    // 重置
    ReactCurrentBatchConfig.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }

  return null;
}

流程概览

commit阶段主要针对 rootFiber上的 effectList进行处理,根据对 DOM 的操作时机可以分为三个子阶段

  • Before mutation阶段(执行 DOM 操作前):读取组件变更前的状态

    • 对于 CC 而言,会执行 getSnapshotBeforeUpdate,获取 DOM 更新前的组件实例信息(更新前)
    • 对于 FC 而言,会异步调度 useEffect 钩子
  • mutation 阶段(执行 DOM 操作):

    • 对于 HostComponent 会执行相应的 DOM 操作
    • 对于 CC 会调用 componentWillUnmount
    • 对于 FC 会执行 useLayoutEffect销毁函数
  • layout 阶段(执行 DOM 操作后):在 DOM 操作完成后,读取当前组件的状态(更新后)

-   对于 CC ,会调用 `componentDidMount` 和 `componentDidUpdate` 以及 `setState` 的回调函数
-   对于 FC ,会执行 `useLayoutEffect` 的回调函数

在这当中,需要注意的是,在 mutation 阶段结束后,layout 开始之前,workInProgress 树会切换成 current 树。这样做是为了

  • mutation 阶段调用类组件的 componentWillUnmount的时候, 可以获取到卸载前的组件信息
  • layout阶段调用 componentDidMount/Update 时,获取的组件信息是组件更新后的。

commit 阶段的主流程在 commitRootImpl 这个函数中,可以明确的看到三个子阶段的执行

function commitRootImpl(root, renderPriorityLevel) {
  // NOTE: 采用 do while 的作用是,在 useEffect 内部可能会触发新的更新,新的更新可能会触发新的副作用 ,因此需要不断的循环,直到 为 null
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null); // Note:这一步是为了看看还有没有没有执行的 useEffect, 有的话先执行他们
  ...
  const finishedWork = root.finishedWork; // 当前的 rootFiber
  const lanes = root.finishedLanes; // 优先级相关
  ...
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  ...
  // 绑定 scheduler 的回调函数
  root.callbackNode = null;
  root.callbackPriority = NoLane;
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  markRootFinished(root, remainingLanes);
  // Note:处理光标,重置一些 render 阶段使用的变量
  ...
  // 子树是否有更新
  const subtreeHasEffects =
    (finishedWork.subtreeFlags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
  const rootHasEffect =
    (finishedWork.flags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;

  if (subtreeHasEffects || rootHasEffect) {
    // 存在副作用,处理 Fiber 上的副作用
    ...
    // 第一个阶段是 before mutation ,在这个阶段可以读取改变之前的的 state
    // 生命周期函数 getSnapshotBeforeUpdate 的调用
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );
    ...
    //  mutation 阶段,可以在这个阶段 改变 host tree
    commitMutationEffects(root, finishedWork, lanes);
    ...
    // 交换 workInProgress
    root.current = finishedWork;
    ...
    // 执行 layout
    commitLayoutEffects(finishedWork, root, lanes);
    ...
    requestPaint();
    // 重置执行栈环境
    executionContext = prevExecutionContext;
    // 将优先级重置为之前的 非同步优先级
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  } else {
    // No effects.
    ...
  }
  ...
  // Note:commit 阶段结尾,可能会在 commit 阶段产生新的更新,因此在 commit 阶段的结尾会重新调度一次
  ensureRootIsScheduled(root, now());
  ...
  // Note:react 中会将同步任务放在 flushSync 队列中,执行这个函数会执行它里面的同步任务
  // Note:默认 react 中开启的是 legacy 模式,这种模式下的更新都是 同步的 更新,未来会开启 concurrent 模式(并发模式),会出现不同优先级的更新
  flushSyncCallbacks();
  ...
  return null;
}

接下来我们去看看每个阶段都分别做了哪些工作!

BeforeMutation 阶段

首先是 before mutation 阶段,在 before mutation 阶段,会执行 commitBeforeMutationEffects 函数,因为此时还没有对真实 DOM 进行修改,因此是获取 DOM 快照的最佳时期,同时也会在此异步调用 useEffect

  • 执行 commitBeforeMutationEffectsOnFiber 函数
  • DOM 组件的 blurfocus 事件相关
  • 对于类组件,执行 getSnapshotBeforeUpdate 生命周期函数
  • 如果 FC 中使用到的 useEffect ,会通过 scheduleCallback 来调度 passiveEffect 异步执行

passiveEffect 就是 useEffect 对应的 effectTag

beforemutation 阶段的主要控制函数在于 commitBeforeMutationEffects,主要做的事情就是初始化全局变量 nextEffect 以及 focusedInstanceHandle,然后调用 commitBeforeMutationEffects_begin 来处理副作用

图片

exportfunction commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);

  nextEffect = firstChild;
  // NOTE:开始执行,
  commitBeforeMutationEffects_begin();

  // 不再跟踪fiber节点
  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

准备工作

首先会执行 prepareForCommit 函数,调用 getClosestInstanceFromNode 方法,获取当前节点最近的 HostComponentHostText 类型对应的 Fiber 节点,来初始化全局变量 focusedInstanceHandle,用来处理 focus 状态

exportfunction prepareForCommit(containerInfo: Container): Object | null {
  eventsEnabled = ReactBrowserEventEmitterIsEnabled();
  selectionInformation = getSelectionInformation();
  let activeInstance = null;
  if (enableCreateEventHandleAPI) {
    const focusedElem = selectionInformation.focusedElem;
    if (focusedElem !== null) {
      activeInstance = getClosestInstanceFromNode(focusedElem);
    }
  }
  ReactBrowserEventEmitterSetEnabled(false);
  return activeInstance;
}

commitBeforeMutationEffects_begin

commitBeforeMutationEffects_begin 函数中会从上往下遍历,找到最底部并且有标记了 before mutation 的 fiber 节点,调用 commitBeforeMutationEffects_complete 函数来更新 props 和 state
如果当前的 Fiber 节点上的 deletions 字段被标记了值,意味着节点即将被删除,会调用 commitBeforeMutationEffectsDeletion创建 blur 事件并进行派发

因此可以知道 begin 流程主要做了两件事

  • 如果子代 Fiber 树上有 before mutation 标记,会把 nextEffect 赋值给子 Fiber,也就是向下递归找到有标记 before mutation 的 Fiber

  • 找到后,执行 commitBeforeMutationEffects_complete 函数

commitBeforeMutationEffects_begin 的执行上,我们可以知道:commit 阶段执行的生命周期以及钩子函数是子先后父的

这是因为,如果在子组件中的生命周期内改变 DOM 状态,并且还要在父组件生命周期中同步状态,就需要子先后父之行生命周期

function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    // This phase is only used for beforeActiveInstanceBlur.
    // Let's skip the whole loop if it's off.
    if (enableCreateEventHandleAPI) {
      const deletions = fiber.deletions;
      if (deletions !== null) {
        for (let i = 0; i < deletions.length; i++) {
          const deletion = deletions[i];
          // 调用 dispatchBeforeDetachedBlur() 来创建 blur 事件并派发
          commitBeforeMutationEffectsDeletion(deletion);
        }
      }
    }

    const child = fiber.child;
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      // 更新fiber节点的 props 和 state
      commitBeforeMutationEffects_complete();
    }
  }
}

commitBeforeMutationEffectsOnFiber

commitBeforeMutationEffects_begin 中会调用 commitBeforeMutationEffects_complete 函数,在 commitBeforeMutationEffects_complete 中会从下到上归并,(sibling 到 parent)执行 commitBeforeMutationEffectsOnFiber函数,这也是 before_mutation核心逻辑

  • 首先会处理 blur 和 focus 相关逻辑
  • 其次会执行 getSnapshotBeforeUpdate 的生命周期函数

会根据 Fiber 节点 tag 的不同进入不同的处理逻辑,同时会根据 current 是否存在来判断是 mount 还是 update 阶段,进入不同的处理逻辑

对于 CC 而言,最重要的就是触发生命周期函数,获取当前 DOM 的数据信息

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
    const current = finishedWork.alternate;
    const flags = finishedWork.flags;
     ...
    if ((flags & Snapshot) !== NoFlags) {
         ...
        switch (finishedWork.tag) {
            case FunctionComponent:
            case ForwardRef:
            case SimpleMemoComponent: {
                break;
            }
            case ClassComponent: {
                if (current !== null) {
                    // 非首次渲染的情况
                    // 获取上一次的props
                    const prevProps = current.memoizedProps;
                    // 获取上一次的 state
                    const prevState = current.memoizedState;
                    // 获取当前 class组件实例
                    const instance = finishedWork.stateNode;
                    // 更新 props 和 state
                    ...
                    // 调用 getSnapshotBeforeUpdate 生命周期方法
                    const snapshot = instance.getSnapshotBeforeUpdate(
                        finishedWork.elementType === finishedWork.type
                            ? prevProps
                            : resolveDefaultProps(finishedWork.type, prevProps),
                        prevState,
                    );
                     ...
                    // 将生成的 snapshot 保存到 instance.__reactInternalSnapshotBeforeUpdate 上
                    // 供 DidUpdate 生命周期使用
                    instance.__reactInternalSnapshotBeforeUpdate = snapshot;
                }
                break;
            }
             ...
        }
      ...
    }
}

从 React 16 版本开始,componentWillXX 生命周期函数加上了 UNSAFE_ 的前缀,这是因为 Reconciler 重构为 Fiber Reconciler 后,render 阶段执行的任务可能会因为某些特殊原因(有优先级更高任务)会被中断或者是重新开始,对应的组件在 render 阶段的生命周期钩子(即 componentWillXX )可能会有触发多次的情况,因此加上了 UNSAFE_ 前缀,减少使用
而新增的 getSnapShotBeforeUpdate 生命周期函数,它是在 commit 阶段内的 before mutation 阶段调用的,由于 commit 阶段是同步执行的,所以不会遇到多次调用的情况。

调度 useEffect

这一部分在 commitBeforeMutationEffects 函数执行之前,也属于 before mutation 阶段

对于 useEffect ,会通过 scheduler 模块提供的 scheduleCallback 进行调度,用来以某个优先级异步调度一个回调函数。

// 调度 useEffect
if (
  (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
  (finishedWork.flags & PassiveMask) !== NoFlags
) {
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    pendingPassiveEffectsRemainingLanes = remainingLanes;
    scheduleCallback(NormalSchedulerPriority, () => {
      // 触发 useEffect
      flushPassiveEffects();
      returnnull;
    });
  }
}

在此处,被异步调度的回调函数就是触发 useEffect 的方法 flushPassiveEffects,这个回调函数会在调度后执行,相当于在这里注册了这个回调函数。
所以整个 useEffect 异步调用分为三步:

  1. before mutation 阶段在 scheduleCallback 中调度 flushPassiveEffects
  2. layout 阶段之后将 effectList 赋值给 rootWithPendingPassiveEffects
  3. scheduleCallback 触发 flushPassiveEffectsflushPassiveEffects内部遍历rootWithPendingPassiveEffects

在 React 官方文档中,也对 useEffect 的执行时机做出了解释

componentDidMountcomponentDidUpdate 不同的是,传给 useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。

此外,从 React 18 开始,当它是离散的用户输入(如点击)的结果时,或者当它是由 flushSync 包装的更新结果时,传递给 useEffect 的函数将在屏幕布局和绘制之前同步执行。这种行为便于事件系统或 flushSync 的调用者观察该效果的结果。少用!!少用!!会影响性能

下面我们来看看 mutation 阶段的工作

Mutation 阶段

上面讲了 beforeMutation阶段的工作,接下来到了执行 DOM 操作mutation 阶段的工作。

before mutation 阶段中,会一上一下的之行 begin 和 complete 的工作,最后 nextEffect 又回到了起始点

mutation 阶段会用同样的方式,向下遍历,向上归并,执行对应的函数,这里执行的是 commitMutationEffects 函数,它会通过调用 commitMutationEffects_begin函数来开始本次的 mutation 阶段的工作

React 将每一个阶段又分为了 begin 和 complete,这样将逻辑进行抽离,主函数流程更加清晰

exportfunction commitMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
  committedLanes: Lanes,
) {
  inProgressLanes = committedLanes; // 优先级相关
  inProgressRoot = root;
  nextEffect = firstChild;

  commitMutationEffects_begin(root);

  inProgressLanes = null;
  inProgressRoot = null;
}

commitMutationEffects_begin 入口

可以看到在这个函数中,主体是一个 while 循环,会从 rootFiber 开始向下遍历,和 before mutation 的工作一样,找到最底层的有 mutation 标志的 fiber 节点,执行 commitMutationEffects_complete 函数
如果遍历到的 Fiber 上有 Deletion 标记,则调用 commitDeletion函数,分离 ref 引用,并调用 componentWillUnmount 生命周期函数,断开 Fiber 与父节点的连接关系。这些工作都在 commitDeletion 函数中进行处理

这是在 React 17.0.3 之后才启用的字段,会在需要被 delete 掉的 Fiber 节点上的 deletions 字段上打上标记,这样可以直接通过 deletions 字段来判断是否需要删除该节点

function commitMutationEffects_begin(root: FiberRoot) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const deletions = fiber.deletions;
    if (deletions !== null) {
      for (let i = 0; i < deletions.length; i++) {
        const childToDelete = deletions[i];
        try {
          // 断开当前 Fiber 节点与 父节点之间的连接
          // 分离 ref ,调用 componentWillUnmount
          commitDeletion(root, childToDelete, fiber);
        } catch (error) {
          ...
        }
      }
    }

    const child = fiber.child;
    // ... 省去判断逻辑 nextEffect = child;
    commitMutationEffects_complete(root);
  }
}

commitMutationEffects_complete

commitMutationEffects_complete 函数中,会开始归并,优先处理兄弟节点,最后处理父节点,调用 commitMutationEffectsOnFiber 函数,根据不同的组件类型,来执行更新、插入、删除 DOM 的操作

function commitMutationEffects_complete(root: FiberRoot) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    ...
    // 核心,根据不同的类型,进行处理
    commitMutationEffectsOnFiber(fiber, root);
    ...
    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

commitMutationEffectsOnFiber

commitMutationEffectsOnFiber 函数中

  1. 首先会判断是否需要重置文本节点
  2. 然后判断是否有 ref 的更新
  3. 然后会根据 Fiber 上的 flags 的类型进行二进制计算,根据计算结果来执行不同的操作逻辑,这和前面介绍的 effectTag 的计算是相同的。会有多个 case 存在
  • Placement:执行 commitPlacement 函数插入 DOM 节点,然后删除 Placement 的 effectTag
  • Update:执行 commitWork 函数来执行更新操作,然后删除 Update 的 effectTag
  • PlacementAndUpdate:先调用 commitPlacement 执行插入操作,然后再调用 commitWork 执行更新操作。

对于 Deletion 的操作已经前置处理了,这里不介绍

function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
  const flags = finishedWork.flags;
  // 判断是否存在 文本节点,重置文本节点
  if (flags & ContentReset) {
    commitResetTextContent(finishedWork);
  }

  if (flags & Ref) {
    const current = finishedWork.alternate;
    if (current !== null) {
      commitDetachRef(current);
    }
    if (enableScopeAPI) {
      if (finishedWork.tag === ScopeComponent) {
        commitAttachRef(finishedWork);
      }
    }
  }
  // ... 处理副作用
  const primaryFlags = flags & (Placement | Update | Hydrating);
  outer: switch (primaryFlags) {
    case Placement: {
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      break;
    }
    case PlacementAndUpdate: {
      // Placement
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;

      // Update
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    // SSR 相关 case
     ...
    case Update: {
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
  }
}

接下来我们来看看相应的对真实 DOM 节点的操作是如何进行的

Placement 插入节点

flags 包含 PlacementeffectTag 时,会调用这个 commitPlacement 函数来执行对 DOM 节点的插入操作
主要的思路是

  1. 首先会根据当前的 Fiber 节点,来找到离他最近的 Host 类型的 Parent Fiber 节点

Host 类型包括:HostComponent、HostRoot ...

  1. 然后根据 parent Fiber 节点的 tag 类型,来判断父 Fiber 节点对应的 DOM 节点是否可以作为 container 容器,因为父节点有可能是一个 component 这样就不能直接插入
  2. 当找到 parent Fiber 之后,如果 parent Fiber 上存在 contentReseteffectTag ,就需要执行 resetTextContent,来重置文本
  3. 接下来会找到当前 Fiber 节点的 Host 类型的 slibing 节点
  • 当执行 insertBefore 时,就需要知道当前 Fiber 节点对应的兄弟节点
  • 当需要执行 appendChild 时,需要知道当前 Fiber 节点的 Host 类型 Parent 节点
  1. 根据是否可以作为 container ,来调用不同的函数在指定的位置插入新的节点。实际上这两个函数的处理逻辑是一致的,唯一的区别就是需不需要判断父节点是不是 COMMENT_NODE
function commitPlacement(finishedWork: Fiber): void {
  // NOTE:如果不支持 mutation 会直接返回了
  if (!supportsMutation) {
    return;
  }
  // NOTE:根据当前节点找到离他最近的 host 类型 fiber 节点
  // getHostParentFiber 一直向上递归查找,直到找到为止
  const parentFiber = getHostParentFiber(finishedWork);

  let parent;
  let isContainer;
  const parentStateNode = parentFiber.stateNode;
  //根据父节点的 tag 类型,来判断是否能够作为被插入节点的container,(有可能是组件形式)
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
      ...
  }
      //  如果父节点有 ContentReset 的 flags,则重置其文本内容
  if (parentFiber.flags & ContentReset) {
    resetTextContent(parent);
    parentFiber.flags &= ~ContentReset;
  }
  // 找到 host 的兄弟节点,需要在哪插入
  const before = getHostSibling(finishedWork);

  if (isContainer) {
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

insertOrAppendPlacementNodeIntoContainer

在这个函数中分为两部分

  • 如果是原生 DOM 节点,调用 insertInContainerBeforeappendChildToContainer 来在相应的位置插入 DOM 节点
  • 如果不是原生 DOM 节点,会对当前 Fiber 节点的所有子 Fiber 节点调用 insertOrAppendPlacementNodeIntoContainer 对自身进行遍历,直到找到 DOM 节点,然后插入
function insertOrAppendPlacementNodeIntoContainer(
  node: Fiber,
  before: ?Instance,
  parent: Container,
): void {
  const {tag} = node;
// 判断当前节点是否为原生的 DOM 节点
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost) {
    const stateNode = node.stateNode;
    if (before) {
      // 插入
      insertInContainerBefore(parent, stateNode, before);
    } else {
      // 追加
      appendChildToContainer(parent, stateNode);
    }
  } elseif (tag === HostPortal) {
    // 不处理
  } else {
    // 不是原生 DOM 节点,需要遍历插入当前节点的子节点
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

insertInContainerBefore 插入节点

before 存在时,会进入这个逻辑。也说明当前需要插入节点的前一个节点是明确的了
在这里需要判断当前父节点是否为注释类型的节点

  • 如果是注释类型的节点,会在父节点的父节点下插入新的 DOM 节点
  • 如果不是,则调用原生 DOM 节点的 insertBefore 方法来直接插入节点
exportfunction insertInContainerBefore(
  container: Container,
  child: Instance | TextInstance,
  beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
  if (container.nodeType === COMMENT_NODE) {
    (container.parentNode: any).insertBefore(child, beforeChild);
  } else {
    container.insertBefore(child, beforeChild);
  }
}

appendChildToContainer 差不多,不多讲,源码位置 packages/react-dom/src/client/ReactDOMHostConfig.js

Update 更新节点

在前面的代码中我们也能看到,在更新节点时,都会调用 commitWork函数来处理
下面我们来揭开它的面纱

commitWork

commitWork 函数会对不同类型的更新做出处理,重点关注 HostComponent 和 HostText 类型

整体流程如下

  • 首先会判断是否支持 mutation,执行其他的逻辑,这里我们的宿主环境不会进入当前逻辑,跳过这部分
  • 接下来会根据 Fiber 节点的 tag 类型,进入不同的条件语句:

对于和 Function Component 相关的类型,例如 simpleMemoComponentfunctionComponent 等类型,会执行 commitHookEffectListUnmount函数,也就是会调用 useLayoutEffectuseInsertionEffect 的销毁函数
具体是会遍历当前的 updateQueue 链表,如果当前 Fiber 节点的 effectTag 等于传入的 tag(HookLayout | Insertion),这个 effectTag 就表示,当前 Fiber 节点包含对 useLayoutEffectuseInsertionEffect 的调用,会执行它们的销毁函数

function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          ...
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
          ...
          
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

  • 对于 HostComponent 类型的节点,首先会获取到 新旧props以及 updateQueue ,最后调用 commitUpdate来对 DOM 进行更新
case HostComponent: {
  // 获取对应的 DOM 节点
  const instance: Instance = finishedWork.stateNode;
  if (instance != null) {
    // 新旧 props
    const newProps = finishedWork.memoizedProps;
    const oldProps = current !== null ? current.memoizedProps : newProps;
    const type = finishedWork.type;
    // 获取 updateQueue
    const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
    finishedWork.updateQueue = null; // 清空
    if (updatePayload !== null) {
      // 提交更新
      commitUpdate(
        instance,
        updatePayload,
        type,
        oldProps,
        newProps,
        finishedWork,
      );
    }
  }
  return;
}

  • 对于 HostText 类型的更新,首先获取到真实的文本节点、新旧文本的内容,调用 commitTextUpdate 来更新文本节点的 nodeValue
// FC 相关的 case 调用 commitHookEffectListUnmount
case HostText: {
  // ...错误处理
  const textInstance: TextInstance = finishedWork.stateNode;
  const newText: string = finishedWork.memoizedProps;

  const oldText: string =
    current !== null ? current.memoizedProps : newText;
  // 更新新旧 text
  commitTextUpdate(textInstance, oldText, newText);
  return;
}
// ... 不关注
}

commitUpdate

commitWork 中,会调用 commitUpdate 函数来进行元素的更新,commitUpdate 主要做以下几件事

  • 执行 domElement[internalPropsKey] = props ,来更新 props
  • 然后调用 updateProperties 函数,来更新 DOM 的属性,将 diff 的结果应用到真实 DOM 上,首先会对 radio 进行特殊的处理,然后会调用 updateDOMProperties,然后根据 Fiber 的 tag 类型,对 input、textarea、select 等表单类型的节点做处理
exportfunction commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  // domElement[internalPropsKey] = props
  updateFiberProps(domElement, newProps);
  // 将 diff 结果应用于真实DOM
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

updateDOMProperties

updateDOMProperties 中会遍历 updateQueue 链表,将更新作用到真实 DOM 节点上,根据 propKey 进行不同的更新操作

function updateDOMProperties(
  domElement: Element,
  updatePayload: Array<any>,
  wasCustomComponentTag: boolean,
  isCustomComponentTag: boolean,
): void {
  // 遍历 updatePayload
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) {
      // 处理 style
      setValueForStyles(domElement, propValue);
    } elseif (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // 处理 innerHtml
      setInnerHTML(domElement, propValue);
    } elseif (propKey === CHILDREN) {
      // 处理 children
      setTextContent(domElement, propValue);
    } else {
      // 处理其他节点属性
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}


接下来执行 layout 阶段!

Layout 阶段

layout 阶段正式开始之前,也就是在 mutation 阶段之后,会执行 current 树变更的操作,这是一个非常重要的过程

current Fiber 树的切换

mutation 阶段和 layout 阶段之间有一句关键的代码

root.current = finishedWork;

在双缓存机制部分中,我们也有写过,当 workInProgressFiber 树完成了渲染,就会将 current 指针从 current Fiber 树指向 workInProgress Fiber 树,也就是这行代码所做的工作
为什么要在 mutation 阶段结束后,layout 阶段之前执行呢?
这是因为 componentWillUnmount 这个生命周期钩子函数,会在 mutation 阶段执行,此时可能会操作原来 Fiber 上的内容,为了保证数据的可靠性所以不会修改 current 指针
而在 layout 阶段会执行 componentDidMountcomponentDidUpdate 生命周期钩子,此时需要获取到的 DOM 是更新后的

流程概览

本部分来讲解 commit 阶段的最后一个子阶段 Layout 阶段的主要工作
layout 阶段会执行 commitLayoutEffect 这个方法

commitLayoutEffects(finishedWork, root, lanes);

同样的会分为 begincomplete 两部分来执行,核心流程也是在 xxxOnFiber 中执行
commitLayoutEffect 函数中,首先会对全局变量 nextEffect 进行赋值
然后会执行 commitLayoutEffects_begin 函数,在这个函数中,会从 nextEffect 开始,向下遍历子树,调用 commitLayoutMountEffects_complete 函数来处理副作用,触发 componentDidMountcomponentDidUpdate 以及各种回调函数等。

commitLayoutEffects

作为入口函数,会对全局变量 nextEffect 进行赋值,调用 commitLayoutEffects_begin 处理副作用,触发生命周期钩子

export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = finishedWork;

  commitLayoutEffects_begin(finishedWork, root, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

commitLayoutEffects_begin

在这个函数中会从 rootFiber 开始,向下遍历。对当前屏幕内的节点调用 commitLayoutMountEffects_complete 函数来处理副作用,触发 componentDidMountcomponentDidUpdate 以及各种回调函数等,跳过未显示的节点。

function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;

    if (
      enableSuspenseLayoutEffectSemantics &&
      fiber.tag === OffscreenComponent &&
      isModernRoot
    ) {
      // 跟踪当前屏幕外堆栈的状态
      const isHidden = fiber.memoizedState !== null;
      const newOffscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
      // 当前节点是否显示
      if (newOffscreenSubtreeIsHidden) {
        // 遍历 alternate 树进行布局,循环处理兄弟节点和父节点
        commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
        continue;
      } else {
        ...
        let child = firstChild;
        // 递归调用 commitLayoutEffects_begin
        while (child !== null) {
          nextEffect = child;
          commitLayoutEffects_begin(
            child, // New root; bubble back up to here and stop.
            root,
            committedLanes,
          );
          child = child.sibling;
        }
        ...
        commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);

        continue;
      }
    }

    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      ensureCorrectReturnPointer(firstChild, fiber);
      nextEffect = firstChild;
    } else {
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}

commitLayoutMountEffects_complete

在 complete 中,同样会从 nextEffect 开始进行归并。调用 commitLayoutEffectOnFiber 函数,根据不同的组件类型,处理相关的副作用

function commitLayoutMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // 循环处理兄弟节点和父节点
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if ((fiber.flags & LayoutMask) !== NoFlags) {
      const current = fiber.alternate;
      ...
      commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
      ...
    }
    // fiber 树遍历完成
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    // 遍历 sibling 节点
    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }
    // 回到 parent 节点,继续遍历
    nextEffect = fiber.return;
  }
}

commitLayoutEffectOnFiber

commitLayoutEffectOnFiber中会根据 fibertag 的不同,执行不同的操作
对于 Function component 来说,会调用 commitHookEffectListMount函数,首先会遍历所有 useLayoutEffect ,去执行它的回调函数
在前面我们知道了 useLayoutEffect 会在 mutation 阶段执行它上一次的销毁函数
在这里我们知道了在 layout阶段会执行 useLayoutEffect 的回调函数,因此 useLayoutEffect先执行所有的销毁函数,再执行回调函数,这两步是同步执行

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
      switch (finishedWork.tag) {
    // FC 相关
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      // 执行 useLayoutEffect 的回调函数
      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
      return;
    }
    ...
    commitAttachRef(finishedWork);
}

对于 ClassComponent 而言

  • 如果 currentnull 会调用 componentDidMount 这个生命周期函数,因此也可以知道 componentDidMount 是在 commit layout 阶段同步执行的
  • current 不为 null 时,会执行 componentDidUpdate 生命周期函数,然后会调用 commitUpdateQueue函数,遍历 updateQueue上的 effects,执行 effect副作用
  • 如果 setState 有 callback 会放入 updateQueue 中,通过 commitUpdateQueue 来执行 callback 回调函数

current 为 null 时,是首屏渲染

if (current === null) {
  ...
    instance.componentDidMount();
} else {
  ...
    instance.componentDidUpdate(
      prevProps,
      prevState,
      instance.__reactInternalSnapshotBeforeUpdate,
    );
}

同样的对于 HostRoot 类型的 Fiber 而言,也会在这里调用 commitUpdateQueue函数来处理 effects,接下来看看 commitUpdateQueue的作用

commitUpdateQueue

commitUpdateQueue 函数会执行 updateQueue 上的 effects 副作用,通过遍历 effects,如果有 callback 就会执行,否则会重置 updateQueue 上的 effects 为 null

export function commitUpdateQueue<State>(
  finishedWork: Fiber,
  finishedQueue: UpdateQueue<State>,
  instance: any,
): void {
  // 遍历 effects 链表,执行 effect callback
  const effects = finishedQueue.effects;
  finishedQueue.effects = null;
  if (effects !== null) {
    for (let i = 0; i < effects.length; i++) {
      const effect = effects[i];
      const callback = effect.callback;
      if (callback !== null) {
        effect.callback = null;
        callCallback(callback, instance);
      }
    }
  }
}

commitAttachRef

commitLayoutEffectOnFiber中做的第二件事就是 commitAttachRef,获取 DOM 实例,更新 Ref

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    // 获取 DOM 实例
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    // Moved outside to ensure DCE works with this flag
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    if (typeof ref === 'function') {
        ...
        retVal = ref(instanceToUse);
      } else {
        retVal = ref(instanceToUse);
      }
    }
  }
}

小总结

至此 layout 阶段的工作已经完成了,Layout 做的事情有:

  • 对于类组件,会执行 componentDidMountcomponentDidUpdate 生命周期,setState 的 callback
  • 对于函数组件会执行 useLayoutEffectuseInsertionEffect 钩子
  • 如果有 ref ,会更新 ref

❤️ 谢谢支持

喜欢的话别忘了 分享、点赞、在看 三连哦~。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7124713816100700173