React源码解读之 commit 阶段

823 阅读7分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

react 版本:v17.0.3

在《React源码解读之任务调度流程》一文中我们将 reconciler 的过程分为了四个节点,其中第四个阶段是输出DOM节点阶段,也即是 commit 阶段,它负责与渲染器(react-dom) 交互,渲染DOM节点。

commit 阶段主要做的事情,是根据之前生成的 effectList,对相应的真实DOM进行更新和渲染,这个阶段是不可中断。

commitRoot

commitRoot 函数是commit阶段的入口函数,我们来看看它的源码:

// react-reconciler/src/ReactFiberWorkLoop.new.js

function commitRoot(root) {
    // TODO: This no longer makes any sense. We already wrap the mutation and
    // layout phases. Should be able to remove.
    const previousUpdateLanePriority = getCurrentUpdatePriority();
    const prevTransition = ReactCurrentBatchConfig.transition;
    try {
        ReactCurrentBatchConfig.transition = 0;
        // 将更新优先级置为 DiscreteEventPriority (事件最高优先级)
        setCurrentUpdatePriority(DiscreteEventPriority);
        // 执行提交
        commitRootImpl(root, previousUpdateLanePriority);
    } finally {
        // 重置 transition 和 更新优先级
        ReactCurrentBatchConfig.transition = prevTransition;
        setCurrentUpdatePriority(previousUpdateLanePriority);
    }

    return null;
}

可以看到,commitRoot 只是将更新的优先级设置为了事件最高优先级,然后调用commitRootImpl函数来处理副作用,将最新的fiber树结构反映到DOM上。

接下来我们看看 commitRootImpl 函数。

commitRootImpl

// packages/react-reconciler/src/ReactFiberWorkLoop.new.js

function commitRootImpl(root, renderPriorityLevel) {

  // do while 循环,执行所有的副作用
  do {

    // flushPassiveEffects 在最后会调用 flushSyncUpdateQueue
    // 循环执行 flushPassiveEffects,直到没有挂载阶段的副作用
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

 

  // ...

  if (subtreeHasEffects || rootHasEffect) {

    // 有副作用,处理 fiber树上的副作用

    // ...

    // 第一个阶段是 before mutation ,在这个阶段可以读取改变之前的 host tree 的state
    // 这个阶段是 生命周期函数getSnapshotBeforeUpdate 调用的地方
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );

    // ...

    // The next phase is the mutation phase, where we mutate the host tree.
    // 下一个阶段是 mutation phase,可以在这个阶段 改变 host tree
    commitMutationEffects(root, finishedWork, lanes);

    // ...
   
    // 下一个阶段是 layout phase(布局阶段)
    // 提交 layout 阶段的副作用
    commitLayoutEffects(finishedWork, root, lanes);
    
    // ...

    // Tell Scheduler to yield at the end of the frame, so the browser has an
    // opportunity to paint.
    // 告诉调度器在帧结束时让出,这样浏览器就有机会进行绘制。
    requestPaint();

    // 重置执行栈环境
    executionContext = prevExecutionContext;

    // Reset the priority to the previous non-sync value.
    // 将优先级重置为之前的 非同步优先级
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  } else {

    // 没有副作用

    // ...
  }

  // ...
  // 在退出 commitRoot 之前总是调用 ensureRootIsScheduled(),确保 root 节点上的任何额外任务都被调度
  ensureRootIsScheduled(root, now());

  // ...

  // If layout work was scheduled, flush it now.
  // 布局工作已安排,立即刷新它
  flushSyncCallbacks();

  // ...
}

commitRootImpl 的核心处理逻辑可分为三个部分,也就是commit阶段的三个阶段:

  1. before mutation 阶段,这个阶段通过执行 commitBeforeMutationEffects 函数来更新 class 组件实例上的 state、props 等,并且这个阶段是生命周期函数 getSnapshotBeforeUpdate 调用的地方。

  2. mutation phase 阶段(挂载阶段),这个阶段通过调用 commitMutationEffects 来完成副作用的执行,主要是处理副作用队列中带有Placement、Update、Deletion、Hydrating标记的fiber节点,与 react-dom 交互,完成DOM节点的插入、更新以及删除操作。

  3. layout phase 阶段(布局阶段),这个阶段通过执行 commitLayoutEffects 函数来处理副作用队列中带有Update | Callback标记的fiber节点,并触发 componentDidMount、componentDidUpdate 以及各种回调函数等。

before mutation 阶段

在 before mutation 阶段,我们需要重点关注的是 commitBeforeMutationEffectsOnFiber 函数,下面,我们从before mutation 阶段的入口函数 commitBeforeMutationEffects 看起。

commitBeforeMutationEffects

// react-reconciler/src/ReactFiberCommitWork.new.js

export function commitBeforeMutationEffects(
    root: FiberRoot,
    firstChild: Fiber,
) {
    // 调用 ReactDOM的 getClosestInstanceFromNode 方法
    // 获取当前节点 最近的 HostComponent 或 HostText fiber祖先节点
    focusedInstanceHandle = prepareForCommit(root.containerInfo);

    nextEffect = firstChild;
    // 创建 beforeblur 事件并派发
    commitBeforeMutationEffects_begin();

    // We no longer need to track the active instance fiber
    // 不再跟踪fiber节点
    const shouldFire = shouldFireAfterActiveInstanceBlur;
    shouldFireAfterActiveInstanceBlur = false;
    focusedInstanceHandle = null;

    return shouldFire;
}

commitBeforeMutationEffects 函数的逻辑比较简单,其主要做的事情就是初始化全局变量nextEffect (nextEffect变量在commit的整个阶段都会使用到),然后调用 commitBeforeMutationEffects_begin 函数来处理副作用。

commitBeforeMutationEffects_begin

// react-reconciler/src/ReactFiberCommitWork.new.js

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) {
            // TODO: Should wrap this in flags check, too, as optimization
            const deletions = fiber.deletions;
            if (deletions !== null) {
                for (let i = 0; i < deletions.length; i++) {
                    const deletion = deletions[i];
                    // 在 ReactDOM 中调用 dispatchBeforeDetachedBlur()创建 beforeblur 事件并派发
                    commitBeforeMutationEffectsDeletion(deletion);
                }
            }
        }

        const child = fiber.child;
        if (
            (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
            child !== null
        ) {
            ensureCorrectReturnPointer(child, fiber);
            nextEffect = child;
        } else {
            // 在 ReactDOM 中调用 dispatchBeforeDetachedBlur()创建 beforeblur 事件并派发
            // 更新fiber节点的 props 和 state
            commitBeforeMutationEffects_complete();
        }
    }
}

commitBeforeMutationEffects_begin函数做的事情,就是遍历fiber树,对每个fiber节点,调用commitBeforeMutationEffects_complete 函数来更新fiber节点的 props 和 state 。

commitBeforeMutationEffects_complete

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitBeforeMutationEffects_complete() {
    while (nextEffect !== null) {
        const fiber = nextEffect;
        setCurrentDebugFiberInDEV(fiber);
        try {
            // 在 ReactDOM 中调用 dispatchBeforeDetachedBlur()创建 beforeblur 事件并派发
            // 更新fiber节点的 props 和 state
            commitBeforeMutationEffectsOnFiber(fiber);
        } catch (error) {
            reportUncaughtErrorInDEV(error);
            captureCommitPhaseError(fiber, fiber.return, error);
        }
        resetCurrentDebugFiberInDEV();

        const sibling = fiber.sibling;
        if (sibling !== null) {
            ensureCorrectReturnPointer(sibling, fiber.return);
            nextEffect = sibling;
            return;
        }

        nextEffect = fiber.return;
    }
}

在 commitBeforeMutationEffects_complete 函数中,继续对fiber树进行遍历,然后调用 commitBeforeMutationEffectsOnFiber 函数来更新fiber节点的 props 和 state 。

下面,我们重点来看下 before mutation 阶段的 commitBeforeMutationEffectsOnFiber 函数。

commitBeforeMutationEffectsOnFiber

// react-reconciler/src/ReactFiberCommitWork.new.js

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;
            }
            //  ...
        }

        //  ...
    }
}

可以看到,在 commitBeforeMutationEffectsOnFiber 函数中,根据fiber节点的tag属性,主要是对 ClassComponent 进行处理,更新 ClassComponent 实例上的 state、props 等,并执行 getSnapshotBeforeUpdate 生命周期函数。

mutation phase 阶段(挂载阶段)

在 mutation phase 阶段,通过遍历整棵fiber树,对fiber节点执行插入、更新及删除操作。

commitMutationEffects

// react-reconciler/src/ReactFiberCommitWork.new.js

export function commitMutationEffects(
    root: FiberRoot,
    firstChild: Fiber,
    committedLanes: Lanes,
) {
    inProgressLanes = committedLanes;
    inProgressRoot = root;
    nextEffect = firstChild;

    commitMutationEffects_begin(root);

    inProgressLanes = null;
    inProgressRoot = null;
}

commitMutationEffects 同样对全局变量 nextEffect 进行了赋值,然后调用 commitMutationEffects_begin 函数,传入 root 节点,执行副作用。

commitMutationEffects_begin

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitMutationEffects_begin(root: FiberRoot) {
    while (nextEffect !== null) {
        const fiber = nextEffect;

        // TODO: Should wrap this in flags check, too, as optimization
        const deletions = fiber.deletions;
        if (deletions !== null) {
            for (let i = 0; i < deletions.length; i++) {
                const childToDelete = deletions[i];
                try {
                    // 断开当前fiber节点与父节点之间的连接
                    // 分离 refs 引用并在整棵子树上调用 componentWillUnmount周期函数
                    commitDeletion(root, childToDelete, fiber);
                } catch (error) {
                    reportUncaughtErrorInDEV(error);
                    captureCommitPhaseError(childToDelete, fiber, error);
                }
            }
        }

        const child = fiber.child;
        if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
            ensureCorrectReturnPointer(child, fiber);
            nextEffect = child;
        } else {
            commitMutationEffects_complete(root);
        }
    }
}

在 commitMutationEffects_begin 中,也是对fiber树进行遍历,如果fiber上有 Deletion副作用标记,则调用commitDeletion 分离 refs 引用,并在子树上调用 componentWillUnmount 生命周期函数,断开当前fiber节点与父节点之间的连接。最后调用 commitMutationEffects_complete 来执行 mutation phase 阶段的副作用。

commitMutationEffects_complete

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitMutationEffects_complete(root: FiberRoot) {
    while (nextEffect !== null) {
        const fiber = nextEffect;
        setCurrentDebugFiberInDEV(fiber);
        try {
            // 根据不同的组件类型,提交work
            commitMutationEffectsOnFiber(fiber, root);
        } catch (error) {
            reportUncaughtErrorInDEV(error);
            captureCommitPhaseError(fiber, fiber.return, error);
        }
        resetCurrentDebugFiberInDEV();

        const sibling = fiber.sibling;
        if (sibling !== null) {
            ensureCorrectReturnPointer(sibling, fiber.return);
            nextEffect = sibling;
            return;
        }

        nextEffect = fiber.return;
    }
}

在 commitMutationEffects_complete 函数中,继续对fiber树进行遍历,然后调用 commitMutationEffectsOnFiber 函数,根据不同的组件类型,来执行更新、插入、删除操作。

下面,我们重点来看下 mutation phase 阶段的 commitMutationEffectsOnFiber 函数。

commitMutationEffectsOnFiber

由于在 commitMutationEffects_begin 函数和 commitMutationEffects_complete 函数中已经对fiber树进行了遍历,并且已执行了fiber的删除操作,因此 commitMutationEffectsOnFiber 函数就是对单个fiber节点执行更新、插入操作。

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
    // TODO: The factoring of this phase could probably be improved. Consider
    // switching on the type of work before checking the flags. That's what
    // we do in all the other phases. I think this one is only different
    // because of the shared reconciliation logic below.
    const flags = finishedWork.flags;

    // 重置 文本节点
    if (flags & ContentReset) {
        commitResetTextContent(finishedWork);
    }

  	// 更新 Ref 
    if (flags & Ref) {
        const current = finishedWork.alternate;
        if (current !== null) {
            // 更新 ref 的current 值
            commitDetachRef(current);
        }
        if (enableScopeAPI) {
            // TODO: This is a temporary solution that allowed us to transition away
            // from React Flare on www.
            if (finishedWork.tag === ScopeComponent) {
                commitAttachRef(finishedWork);
            }
        }
    }

    // ...

    // 执行更新、插入操作
    const primaryFlags = flags & (Placement | Update | Hydrating);
    outer: switch (primaryFlags) {
        // 插入
        case Placement: {
            commitPlacement(finishedWork);
            // Clear the "placement" from effect tag so that we know that this is
            // inserted, before any life-cycles like componentDidMount gets called.
            // TODO: findDOMNode doesn't rely on this any more but isMounted does
            // and isMounted is deprecated anyway so we should be able to kill this.
            finishedWork.flags &= ~Placement;
            break;
        }
        // 插入并更新
        case PlacementAndUpdate: {
            // 插入
            // Placement
            commitPlacement(finishedWork);
            // Clear the "placement" from effect tag so that we know that this is
            // inserted, before any life-cycles like componentDidMount gets called.
            finishedWork.flags &= ~Placement;

            // 更新
            // Update
            const current = finishedWork.alternate;
            commitWork(current, finishedWork);
            break;
        }
        case Hydrating: {
            finishedWork.flags &= ~Hydrating;
            break;
        }
        case HydratingAndUpdate: {
            finishedWork.flags &= ~Hydrating;

            // Update
            const current = finishedWork.alternate;
            commitWork(current, finishedWork);
            break;
        }
        // 更新
        case Update: {
            const current = finishedWork.alternate;
            commitWork(current, finishedWork);
            break;
        }
    }
}

在 commitMutationEffectsOnFiber 中,会根据 fiber上的 flags 的类型进行 二进制与 运算,然后根据运算后的结果去执行不同的操作,对真实DOM进行修改:

  • ContentReset:如果 flags 中包含 ContentReset 类型,说明文本节点内容发生改变,则执行 commitResetTextContent 函数重置文本节点的内容。

  • Ref:如果 flags 中包含 Ref 类型,则执行 commitDetachRef 函数更改 ref 对应的 current 的值。

  • Placement:如果 flags 中包含 Placement 类型,代表需要插入新节点,执行 commitPlacement 函数插入DOM节点。

  • Update:如果 flags 中包含 Update 类型,则执行 commitWork 执行更新操作。

  • PlacementAndUpdate:如果是 PlacementAndUpdate 类型,则先调用 commitPlacement 执行插入操作,然后再调用 commitWork 执行更新操作。

下面,我们再来看看react是如何对真实DOM节点进行插入、更新及删除操作的。

插入DOM节点

无论 fiber 的 flags 中包含 Placement 还是 PlacementAndUpdate,都是调用 commitPlacement 执行DOM节点插入操作。

commitPlacement -- 获取父节点及插入位置

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitPlacement(finishedWork: Fiber): void {
    if (!supportsMutation) {
        return;
    }

    // Recursively insert all host nodes into the parent.
    // 获取当前fiber节点的父fiber节点
    const parentFiber = getHostParentFiber(finishedWork);

    // Note: these two variables *must* always be updated together.
    // 这两个变量总是会一起被更新
    let parent;
    let isContainer;
    // 获取父fiber节点对应的真实DOM节点
    const parentStateNode = parentFiber.stateNode;
    // 根据父fiber节点的 tag 的类型(标签类型) 来获取 父fiber节点对应的DOM节点是否可以作为 container
    switch (parentFiber.tag) {

        // tag类型为 HostComponent,不可以作为 container
        case HostComponent:
            parent = parentStateNode;
            isContainer = false;
            break;

        // tag 类型为 HostRoot,可以作为 container
        case HostRoot:
            parent = parentStateNode.containerInfo;
            isContainer = true;
            break;

        // tag 类型为 HostPortal,可以作为 container
        case HostPortal:
            parent = parentStateNode.containerInfo;
            isContainer = true;
            break;
        // eslint-disable-next-line-no-fallthrough
        default:
            throw new Error(
                'Invalid host parent fiber. This error is likely caused by a bug ' +
                'in React. Please file an issue.',
            );
    }

    // 如果父 fiber节点有 ContentReset 的 flags 副作用,则重置其文本内容
    if (parentFiber.flags & ContentReset) {
        // Reset the text content of the parent before doing any insertions
        resetTextContent(parent);
        // Clear ContentReset from the effect tag
        parentFiber.flags &= ~ContentReset;
    }

    // 获取要在哪个兄弟fiber节点之前插入
    const before = getHostSibling(finishedWork);
    // We only have the top Fiber that was inserted but we need to recurse down its
    // children to find all the terminal nodes.
    if (isContainer) {
        insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
    } else {
        insertOrAppendPlacementNode(finishedWork, before, parent);
    }
}

在 commitPlacement 中,首先获取当前fiber节点的父fiber节点对应的真实DOM节点以及在父节点下要插入的位置,然后根据fiber节点的 tag 类型 (标签类型) 来判断父fiber节点对应的DOM节点是否可以作为 container 容器,如果可以作为container,则调用 insertOrAppendPlacementNodeIntoContainer 函数在指定的位置插入节点,否则调用 insertOrAppendPlacementNode 函数在指定的位置插入新节点。

insertOrAppendPlacementNodeIntoContainer -- 判断是否为单节点

insertOrAppendPlacementNodeIntoContainer 和 insertOrAppendPlacementNode 的处理逻辑是一致,唯一的区别就是 insertOrAppendPlacementNode 在对应的位置插入节点时,不需要额外判断父节点(container) 的节点类型(nodeType) 是否为注释类型节点(COMMENT_NODE) 。因此我们以 insertOrAppendPlacementNodeIntoContainer 为例,看看react如何在对应的位置插入DOM节点的。

// react-reconciler/src/ReactFiberCommitWork.new.js

function insertOrAppendPlacementNodeIntoContainer(
    node: Fiber,
    before: ?Instance,
    parent: Container,
): void {
    const { tag } = node;
    // 判断当前节点是否为原生的 DOM节点
    const isHost = tag === HostComponent || tag === HostText;
    if (isHost) {
        // 是原生DOM节点

        const stateNode = node.stateNode;
        if (before) {
            // 插入节点
            insertInContainerBefore(parent, stateNode, before);
        } else {
            // 追加节点
            appendChildToContainer(parent, stateNode);
        }
    } else if (tag === HostPortal) {
        // If the insertion itself is a portal, then we don't want to traverse
        // down its children. Instead, we'll get insertions from each child in
        // the portal directly.
        
        // 是Portal 则不做处理
    } else {
        // 不是原生DOM节点,则遍历插入当前节点的各个子节点
        const child = node.child;
        // 继续调用 insertOrAppendPlacementNodeIntoContainer
        // 直到找到原生DOM节点,然后插入节点
        if (child !== null) {
            insertOrAppendPlacementNodeIntoContainer(child, before, parent);
            let sibling = child.sibling;
            while (sibling !== null) {
                insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
                sibling = sibling.sibling;
            }
        }
    }
}

在 insertOrAppendPlacementNodeIntoContainer 中,通过 fiber 的 tag 属性来判断当前fiber节点是否为原生DOM节点(HostComponent 或 HostText)。

如果是,则调用 insertInContainerBefore 或 appendChildToContainer 在相应的位置插入DOM节点。

如果不是原生DOM节点,则对当前fiber节点的所有子fiber节点调用 insertOrAppendPlacementNodeIntoContainer 自身进行遍历,直到找到原生DOM节点,然后插入节点。

insertInContainerBefore -- 在对应位置插入节点

// react-dom/src/client/ReactDOMHostConfig.js

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

在 insertOrAppendPlacementNodeIntoContainer 函数中,如果 before 不为null,说明要在某个DOM节点之前插入新的DOM节点,此时调用 insertInContainerBefore 函数去进行插入,根据父节点是否为注释类型(nodeType 为 COMMENT_NODE) ,选择是在父节点的父节点下插入新的DOM节点还是直接在父节点下插入新的DOM节点,插入节点时调用的是原生DOM元素的 insertBefore 方法。

appendChildToContainer -- 在末尾追加节点

// react-dom/src/client/ReactDOMHostConfig.js

export function appendChildToContainer(
  container: Container,
  child: Instance | TextInstance,
): void {
  let parentNode;
  if (container.nodeType === COMMENT_NODE) {
    // 如果父节点是注释类型,则在父节点的父节点下插入新的DOM节点
    // 调用原生DOM的 insertBefore 方法插入新的节点
    parentNode = (container.parentNode: any);
    parentNode.insertBefore(child, container);
  } else {
    // 父节点不是注释类型,则直接插入新的DOM节点
    parentNode = container;
    // 调用原生DOM的appendChild 方法在末尾添加新的子节点
    parentNode.appendChild(child);
  }
  // This container might be used for a portal.
  // If something inside a portal is clicked, that click should bubble
  // through the React tree. However, on Mobile Safari the click would
  // never bubble through the *DOM* tree unless an ancestor with onclick
  // event exists. So we wouldn't see it and dispatch it.
  // This is why we ensure that non React root containers have inline onclick
  // defined.
  // https://github.com/facebook/react/issues/11918
  const reactRootContainer = container._reactRootContainer;
  if (
    (reactRootContainer === null || reactRootContainer === undefined) &&
    parentNode.onclick === null
  ) {
    // TODO: This cast may not be sound for SVG, MathML or custom elements.
    trapClickOnNonInteractiveElement(((parentNode: any): HTMLElement));
  }
}

在 insertOrAppendPlacementNodeIntoContainer 函数中,如果 before 为 null,则调用 appendChildToContainer 函数进行插入,在插入前,会判断父节点是否为注释类型,如果是,则调用原生DOM元素的 insertBefore 方法插入新节点;如果不是,则调用原生DOM元素的 appendChild 方法插入新节点。无论是调用 insertBefore 方法还是 appendChild 方法,appendChildToContainer 插入新节点都是在末尾的位置追加新的DOM节点。

可以看到,insertInContainerBefore 和 appendChildToContainer 的区别是:insertInContainerBefore 是在指定的位置插入新的DOM节点,而 appendChildToContainer是在末尾追加新的DOM节点。

更新DOM节点

当 fiber 的 flags 中包含 Update 还是 PlacementAndUpdate 时,都是调用 commitWork 执行DOM节点的更新操作。

commitWork

在 commitWork 中,主要是针对 HostComponent 和 HostText 两种类型的更新。

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
   
    // ...

    switch (finishedWork.tag) {
       
        // ...

        case ClassComponent: {
            return;
        }
        case HostComponent: {
            // 获取真实DOM节点
            const instance: Instance = finishedWork.stateNode;
            if (instance != null) {
                // Commit the work prepared earlier.
                // 获取新的 props
                const newProps = finishedWork.memoizedProps;
                // For hydration we reuse the update path but we treat the oldProps
                // as the newProps. The updatePayload will contain the real change in
                // this case.
                // 获取旧的props
                const oldProps = current !== null ? current.memoizedProps : newProps;
                const type = finishedWork.type;
                // TODO: Type the updateQueue to be specific to host components.
                // 取出 updateQueue
                const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
                // 清空 fiber 上的 updateQueue
                finishedWork.updateQueue = null;
                if (updatePayload !== null) {
                    // 提交更新
                    commitUpdate(
                        instance,
                        updatePayload,
                        type,
                        oldProps,
                        newProps,
                        finishedWork,
                    );
                }
            }
            return;
        }
        case HostText: {
            if (finishedWork.stateNode === null) {
                throw new Error(
                    'This should have a text node initialized. This error is likely ' +
                    'caused by a bug in React. Please file an issue.',
                );
            }

            // 获取真实 文本节点
            const textInstance: TextInstance = finishedWork.stateNode;
            // 获取新的文本内容
            const newText: string = finishedWork.memoizedProps;
            // For hydration we reuse the update path but we treat the oldProps
            // as the newProps. The updatePayload will contain the real change in
            // this case.
            // 获取旧的文本内容
            const oldText: string =
                current !== null ? current.memoizedProps : newText;
            // 提交更新
            commitTextUpdate(textInstance, oldText, newText);
            return;
        }
        case HostRoot: {
            if (supportsHydration) {
                const root: FiberRoot = finishedWork.stateNode;
                if (root.isDehydrated) {
                    // We've just hydrated. No need to hydrate again.
                    root.isDehydrated = false;
                    commitHydratedContainer(root.containerInfo);
                }
            }
            return;
        }
        case Profiler: {
            return;
        }

       // ...
    }

    // ...
}

对于 HostComponent 类型的更新,首先获取真实的DOM节点,节点的props 以及 updateQueue,然后调用 commitUpdate 对DOM进行更新。

对于 HostText 类型的更新,也是首先获取真实的文本节点,新旧文本内容,然后调用commitTextUpdate更新文本内容。

commitUpdate -- 更新HostComponent

// react-dom/src/client/ReactDOMHostConfig.js

export function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  // Update the props handle so that we know which props are the ones with
  // with current event handlers.
  // 做了 domElement[internalPropsKey] = props 的操作
  updateFiberProps(domElement, newProps);
  // Apply the diff to the DOM node.
  // 将 diff 结果应用于真实DOM
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

在 commitUpdate 函数中,首先调用 updateFiberProps 执行domElement[internalPropsKey] = props 的操作,然后调用 updateProperties 函数将 diff 结果应用于真实DOM上。

updateProperties

// react-dom/src/client/ReactDOMComponent.js

export function updateProperties(
  domElement: Element,
  updatePayload: Array<any>,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
): void {
  // Update checked *before* name.
  // In the middle of an update, it is possible to have multiple checked.
  // When a checked radio tries to change name, browser makes another radio's checked false.

  // 根据表单类型进行特殊的处理,例如更新 radio 的checked 值
  if (
    tag === 'input' &&
    nextRawProps.type === 'radio' &&
    nextRawProps.name != null
  ) {
    ReactDOMInputUpdateChecked(domElement, nextRawProps);
  }

  // 判断是否为用户自定义的组件,即是否包含 “-”
  const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
  const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
  // Apply the diff.
//   将 diff 结果应用于真实DOM
  updateDOMProperties(
    domElement,
    updatePayload,
    wasCustomComponentTag,
    isCustomComponentTag,
  );

  // TODO: Ensure that an update gets scheduled if any of the special props
  // changed.
  // 针对表单的特殊处理 
  switch (tag) {
    case 'input':
      // Update the wrapper around inputs *after* updating props. This has to
      // happen after `updateDOMProperties`. Otherwise HTML5 input validations
      // raise warnings and prevent the new value from being assigned.
      ReactDOMInputUpdateWrapper(domElement, nextRawProps);
      break;
    case 'textarea':
      ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
      break;
    case 'select':
      // <select> value update needs to occur after <option> children
      // reconciliation
      ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
      break;
  }
}

在 updateProperties 中,首先对 radio 类型的表单DOM节点进行特殊的处理,然后调用 updateDOMProperties 函数,将 diff 结果应用于真实的DOM节点。最后根据 fiber 的 tag 类型,对 input、textarea、select 等表单类型的DOM节点做特殊处理。

updateDOMProperties

// react-dom/src/client/ReactDOMComponent.js

function updateDOMProperties(
  domElement: Element,
  updatePayload: Array<any>,
  wasCustomComponentTag: boolean,
  isCustomComponentTag: boolean,
): void {
  // TODO: Handle wasCustomComponentTag
  // 遍历 updatePayload,即遍历 updateQueue
  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);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
        // 处理 innerHTML 改变
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
        // 处理 textContent
      setTextContent(domElement, propValue);
    } else {
        // 处理其它节点属性
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}

在 updateDOMProperties 中,会遍历在render阶段生成的 updatePayload,即 updateQueue,将其映射到真实DOM节点的属性上。根据 propKey 的类型,对 style、innerHTML以及 textContext做处理,从而实现DOM的更新。

commitTextUpdate -- 更新 HostText

// react-dom/src/client/ReactDOMHostConfig.js

export function commitTextUpdate(
  textInstance: TextInstance,
  oldText: string,
  newText: string,
): void {
  textInstance.nodeValue = newText;
}

HostText 的更新处理则非常简单,直接将DOM节点的nodeValue属性重新赋值为 newText 的值即可。

删除DOM节点

mutation phase 阶段,在 commitMutationEffects_begin 函数中,对fiber树进行遍历时,如果fiber上有 Deletion副作用标记,则调用commitDeletion 来执行删除操作。

commitDeletion

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitDeletion(
    finishedRoot: FiberRoot,
    current: Fiber,
    nearestMountedAncestor: Fiber,
): void {
    if (supportsMutation) {

        // 支持 useMutation

        // Recursively delete all host nodes from the parent.
        // Detach refs and call componentWillUnmount() on the whole subtree.
        // 通过深度优先遍历,从父节点开始递归删除所有的 host节点
        // 卸载 refs 引用并在整棵子树上调用 componentWillUnmount 生命周期函数
        unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
    } else {

        // 不支持 useMutation

        // Detach refs and call componentWillUnmount() on the whole subtree.
        // 通过深度优先遍历,卸载 refs 引用并在整棵子树上调用 componentWillUnmount 生命周期函数
        commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor);
    }
    // 重置fiber的各项属性,将其重置为 null,断开当前fiber节点与父节点之间的连接
    detachFiberMutation(current);
}

commitDeletion 函数是删除DOM节点操作的入口函数,在commitDeletion函数中,会调用 unmountHostComponents 和 commitNestedUnmounts,通过深度优先遍历整棵fiber树,找到需要删除的fiber节点,卸载 ref 引用,执行 componentWillUnmount 生命周期函数,将对应的DOM节点删除。最后调用 detachFiberMutation 函数,将当前fiber节点上的各项属性重置为 null,断开当前fiber节点与父节点之间的连接。

unmountHostComponents

// react-reconciler/src/ReactFiberCommitWork.new.js

function unmountHostComponents(
    finishedRoot: FiberRoot,
    current: Fiber,
    nearestMountedAncestor: Fiber,
): void {
    // We only have the top Fiber that was deleted but we need to recurse down its
    // children to find all the terminal nodes.
    let node: Fiber = current;

    // Each iteration, currentParent is populated with node's host parent if not
    // currentParentIsValid.
    let currentParentIsValid = false;

    // Note: these two variables *must* always be updated together.
    let currentParent;
    let currentParentIsContainer;

    while (true) {
        if (!currentParentIsValid) {
            // 如果当前的父节点不是非法的DOM节点,则寻找一个合法的DOM父节点
            let parent = node.return;
            findParent: while (true) {
                if (parent === null) {
                    throw new Error(
                        'Expected to find a host parent. This error is likely caused by ' +
                        'a bug in React. Please file an issue.',
                    );
                }

                const parentStateNode = parent.stateNode;
                switch (parent.tag) {
                    // HostComponent 不可以作为container
                    case HostComponent:
                        currentParent = parentStateNode;
                        currentParentIsContainer = false;
                        break findParent;
                    
                    // HostRoot 可以作为container
                    case HostRoot:
                        currentParent = parentStateNode.containerInfo;
                        currentParentIsContainer = true;
                        break findParent;

                    // HostPortal 可以作为container
                    case HostPortal:
                        currentParent = parentStateNode.containerInfo;
                        currentParentIsContainer = true;
                        break findParent;
                }
                parent = parent.return;
            }
            currentParentIsValid = true;
        }

        if (node.tag === HostComponent || node.tag === HostText) {
            // 如果是原生DOM节点或文本节点,调用 commitNestedUnmounts 卸载DOM节点
            commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor);
            // After all the children have unmounted, it is now safe to remove the
            // node from the tree.
            if (currentParentIsContainer) {
                // 移除当前container下的子节点
                removeChildFromContainer(
                    ((currentParent: any): Container),
                    (node.stateNode: Instance | TextInstance),
        );
            } else {
                // 移除子节点
                removeChild(
                    ((currentParent: any): Instance),
                    (node.stateNode: Instance | TextInstance),
        );
            }
            // Don't visit children because we already visited them.
        } else if (
            enableSuspenseServerRenderer &&
            node.tag === DehydratedFragment
        ) {

            // Fragment 节点

            if (enableSuspenseCallback) {
                const hydrationCallbacks = finishedRoot.hydrationCallbacks;
                if (hydrationCallbacks !== null) {
                    const onDeleted = hydrationCallbacks.onDeleted;
                    if (onDeleted) {
                        onDeleted((node.stateNode: SuspenseInstance));
                    }
                }
            }

            // Delete the dehydrated suspense boundary and all of its content.
            if (currentParentIsContainer) {
                clearSuspenseBoundaryFromContainer(
                    ((currentParent: any): Container),
                    (node.stateNode: SuspenseInstance),
        );
            } else {
                clearSuspenseBoundary(
                    ((currentParent: any): Instance),
                    (node.stateNode: SuspenseInstance),
        );
            }
        } else if (node.tag === HostPortal) {

            // Portal 节点,直接向下遍历 child,因为它没有 ref 和生命周期等额外要处理的事情

            if (node.child !== null) {
                // When we go into a portal, it becomes the parent to remove from.
                // We will reassign it back when we pop the portal on the way up.
                currentParent = node.stateNode.containerInfo;
                currentParentIsContainer = true;
                // Visit children because portals might contain host components.
                node.child.return = node;
                node = node.child;
                continue;
            }
        } else {

            // 其它 react 节点,调用 commitUnmount,卸载 ref、执行生命周期函数等

            commitUnmount(finishedRoot, node, nearestMountedAncestor);
            // Visit children because we may find more host components below.

            // 深度优先遍历子节点
            if (node.child !== null) {
                node.child.return = node;
                node = node.child;
                continue;
            }
        }

        // node 和 current 相等时,说明整棵树的深度遍历完成
        if (node === current) {
            return;
        }

        // 如果当前遍历到的子节点没有兄弟节点,说明当前子树遍历完毕,返回到父节点继续深度遍历
        while (node.sibling === null) {
            if (node.return === null || node.return === current) {
                return;
            }
            node = node.return;
            if (node.tag === HostPortal) {
                // When we go out of the portal, we need to restore the parent.
                // Since we don't keep a stack of them, we will search for it.
                currentParentIsValid = false;
            }
        }

        // 继续遍历兄弟节点
        node.sibling.return = node.return;
        node = node.sibling;
    }
}

在 unmountHostComponents 函数中,会首先判断当前的父节点是否是合法的DOM节点,如果不合法,则寻找一个合法的父节点。然后通过深度优先遍历,去遍历整棵fiber树,如果遍历的的节点是HostComponent或HostText ,则调用commitNestedUnmounts函数卸载ref引用,执行componentWillUnmount生命周期函数,如果是其它的react节点,则调用 commitUnmount 函数卸载 ref引用,执行 componentWillUnmount 生命周期函数。

commitNestedUnmounts

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitNestedUnmounts(
    finishedRoot: FiberRoot,
    root: Fiber,
    nearestMountedAncestor: Fiber,
): void {
    // While we're inside a removed host node we don't want to call
    // removeChild on the inner nodes because they're removed by the top
    // call anyway. We also want to call componentWillUnmount on all
    // composites before this host node is removed from the tree. Therefore
    // we do an inner loop while we're still inside the host node.
    let node: Fiber = root;
    while (true) {
        // 调用 commitUnmount 卸载 ref ,执行生命周期函数
        commitUnmount(finishedRoot, node, nearestMountedAncestor);
        // Visit children because they may contain more composite or host nodes.
        // Skip portals because commitUnmount() currently visits them recursively.
        if (
            node.child !== null &&
            // If we use mutation we drill down into portals using commitUnmount above.
            // If we don't use mutation we drill down into portals here instead.
            (!supportsMutation || node.tag !== HostPortal)
        ) {
            // 深度优先遍历向下遍历子树
            node.child.return = node;
            node = node.child;
            continue;
        }

        // node 与 root 相等时说明整棵树的深度优先遍历已完成
        if (node === root) {
            return;
        }

        // 当前子节点没有兄弟节点,说明当前子树已经遍历完成,返回父节点继续深度遍历
        while (node.sibling === null) {
            if (node.return === null || node.return === root) {
                return;
            }
            node = node.return;
        }

        // 遍历兄弟节点
        node.sibling.return = node.return;
        node = node.sibling;
    }
}

commitNestedUnmounts 函数同样是通过深度优先遍历,去遍历整棵fiber树,然后执行commitUnmount方法卸载 ref引用,执行componentWillUnmount生命周期函数。它与unmountHostComponents的不同点就是,commitNestedUnmounts不需要判断当前父节点是否是合法的DOM节点以及react节点类型的判断,而是直接调用commitUnmount函数执行删除操作。

commitUnmount

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitUnmount(
    finishedRoot: FiberRoot,
    current: Fiber,
    nearestMountedAncestor: Fiber,
): void {
    onCommitUnmount(current);

    switch (current.tag) {
        case FunctionComponent:
        case ForwardRef:
        case MemoComponent:
        case SimpleMemoComponent: {
            const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
            if (updateQueue !== null) {
                const lastEffect = updateQueue.lastEffect;
                if (lastEffect !== null) {
                    // 获取effect 链上的第一个副作用
                    const firstEffect = lastEffect.next;

                    let effect = firstEffect;
                    do {
                        const { destroy, tag } = effect;

                        // 销毁副作用

                        if (destroy !== undefined) {
                            if (
                                (tag & HookInsertion) !== NoHookEffect ||
                                (tag & HookLayout) !== NoHookEffect
                            ) {
                                if (
                                    enableProfilerTimer &&
                                    enableProfilerCommitHooks &&
                                    current.mode & ProfileMode
                                ) {
                                    startLayoutEffectTimer();
                                    safelyCallDestroy(current, nearestMountedAncestor, destroy);
                                    recordLayoutEffectDuration(current);
                                } else {
                                    safelyCallDestroy(current, nearestMountedAncestor, destroy);
                                }
                            }
                        }
                        effect = effect.next;
                    } while (effect !== firstEffect);
                }
            }
            return;
        }
        case ClassComponent: {
            // 卸载 ref 引用
            safelyDetachRef(current, nearestMountedAncestor);
            // 获取当前组件实例
            const instance = current.stateNode;
            // 执行 componentWillUnmount 生命周期函数
            if (typeof instance.componentWillUnmount === 'function') {
                safelyCallComponentWillUnmount(
                    current,
                    nearestMountedAncestor,
                    instance,
                );
            }
            return;
        }
        case HostComponent: {
            // 卸载 ref 引用
            safelyDetachRef(current, nearestMountedAncestor);
            return;
        }
        case HostPortal: {
            // TODO: this is recursive.
            // We are also not using this parent because
            // the portal will get pushed immediately.
            if (supportsMutation) {
                // 递归遍历子树
                unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
            } else if (supportsPersistence) {
                emptyPortalContainer(current);
            }
            return;
        }
        case DehydratedFragment: {
            if (enableSuspenseCallback) {
                const hydrationCallbacks = finishedRoot.hydrationCallbacks;
                if (hydrationCallbacks !== null) {
                    const onDeleted = hydrationCallbacks.onDeleted;
                    if (onDeleted) {
                        onDeleted((current.stateNode: SuspenseInstance));
                    }
                }
            }
            return;
        }
        case ScopeComponent: {
            if (enableScopeAPI) {
                safelyDetachRef(current, nearestMountedAncestor);
            }
            return;
        }
    }
}

在 commitUnmount 函数中,会根据组件的类型,去销毁fiber上的副作用,卸载ref引用,如果是ClassComponent,还会执行 componentWillUnmount 生命周期函数。

通过以上的这些操作,react最终完成了DOM的删除操作。

layout phase 阶段(布局阶段)

commitLayoutEffects

// react-reconciler/src/ReactFiberCommitWork.new.js

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;
}

和 before mutation 阶段和 mutation phase 阶段一样,layout phase 节点的入口函数commitLayoutEffects首先对全局变量nextEffect进行了赋值,然后调用 commitLayoutEffects_begin 函数来处理副作用,并触发 componentDidMount、componentDidUpdate 以及各种回调函数等。

commitLayoutEffects_begin

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitLayoutEffects_begin(
    subtreeRoot: Fiber,
    root: FiberRoot,
    committedLanes: Lanes,
) {
    // Suspense layout effects semantics don't change for legacy roots.
    const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

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

        if (
            enableSuspenseLayoutEffectSemantics &&
            fiber.tag === OffscreenComponent &&
            isModernRoot
        ) {
            // Keep track of the current Offscreen stack's state.
            // 跟踪当前屏幕外堆栈的状态
            const isHidden = fiber.memoizedState !== null;
            const newOffscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
            if (newOffscreenSubtreeIsHidden) {
                // The Offscreen tree is hidden. Skip over its layout effects.
                // 屏幕外树被隐藏。 跳过其布局效果。

                //遍历 alternate 树进行布局,循环处理兄弟节点和父节点
                commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
                continue;
            } else {
                // TODO (Offscreen) Also check: subtreeFlags & LayoutMask
                const current = fiber.alternate;
                const wasHidden = current !== null && current.memoizedState !== null;
                const newOffscreenSubtreeWasHidden =
                    wasHidden || offscreenSubtreeWasHidden;
                const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
                const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;

                // Traverse the Offscreen subtree with the current Offscreen as the root.
                // 以当前屏幕外的fiber树(alternate) 为根,遍历 alternate 的子树
                offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
                offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;

                if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
                    // This is the root of a reappearing boundary. Turn its layout effects
                    // back on.
                    // 重新出现的根,重新打开其布局效果
                    nextEffect = fiber;
                    reappearLayoutEffects_begin(fiber);
                }

                let child = firstChild;
                while (child !== null) {
                    nextEffect = child;
                    // 处理下一个节点的布局,递归调用 commitLayoutEffects_begin 自身
                    commitLayoutEffects_begin(
                        child, // New root; bubble back up to here and stop.
                        root,
                        committedLanes,
                    );
                    child = child.sibling;
                }

                // Restore Offscreen state and resume in our-progress traversal.
                nextEffect = fiber;
                offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
                offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
                //遍历 alternate 树进行布局,循环处理兄弟节点和父节点
                commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);

                continue;
            }
        }

        if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
            ensureCorrectReturnPointer(firstChild, fiber);
            nextEffect = firstChild;
        } else {
            //遍历 alternate 树进行布局,循环处理兄弟节点和父节点
            commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
        }
    }
}

在 commitLayoutEffects_begin 函数中,从nextEffect开始,向下遍历子树,调用 commitLayoutMountEffects_complete 函数来处理副作用,触发 componentDidMount、componentDidUpdate 以及各种回调函数等。

commitLayoutMountEffects_complete

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitLayoutMountEffects_complete(
    subtreeRoot: Fiber,
    root: FiberRoot,
    committedLanes: Lanes,
) {
    //遍历 alternate 树进行布局,循环处理兄弟节点和父节点
    while (nextEffect !== null) {
        const fiber = nextEffect;
        if ((fiber.flags & LayoutMask) !== NoFlags) {
            const current = fiber.alternate;
            setCurrentDebugFiberInDEV(fiber);
            try {
                // 根据不同的组件类型,实现布局效果
                commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
            } catch (error) {
                reportUncaughtErrorInDEV(error);
                captureCommitPhaseError(fiber, fiber.return, error);
            }
            resetCurrentDebugFiberInDEV();
        }

        // fiber 树已遍历完
        if (fiber === subtreeRoot) {
            nextEffect = null;
            return;
        }

        // 遍历兄弟节点
        const sibling = fiber.sibling;
        if (sibling !== null) {
            ensureCorrectReturnPointer(sibling, fiber.return);
            nextEffect = sibling;
            return;
        }

        // 回到父节点,继续遍历其它节点
        nextEffect = fiber.return;
    }
}

在 commitLayoutMountEffects_complete 函数中,继续对 nextEffect 进行遍历,从 nextEffect 开始,遍历整棵树,继续调用 commitLayoutEffectOnFiber 函数,根据不同的组件类型,处理相关的副作用以及执行对应的生命周期函数。

commitLayoutEffectOnFiber -- 执行生命周期

// react-reconciler/src/ReactFiberCommitWork.new.js

function commitLayoutEffectOnFiber(
    finishedRoot: FiberRoot,
    current: Fiber | null,
    finishedWork: Fiber,
    committedLanes: Lanes,
): void {
    if ((finishedWork.flags & LayoutMask) !== NoFlags) {
        switch (finishedWork.tag) {
            case FunctionComponent:
            case ForwardRef:
            case SimpleMemoComponent: {
                if (
                    !enableSuspenseLayoutEffectSemantics ||
                    !offscreenSubtreeWasHidden
                ) {
                    // At this point layout effects have already been destroyed (during mutation phase).
                    // This is done to prevent sibling component effects from interfering with each other,
                    // e.g. a destroy function in one component should never override a ref set
                    // by a create function in another component during the same commit.
                    if (
                        enableProfilerTimer &&
                        enableProfilerCommitHooks &&
                        finishedWork.mode & ProfileMode
                    ) {
                        try {
                            startLayoutEffectTimer();
                            // 通过 do...while 循环 挂载副作用
                            commitHookEffectListMount(
                                HookLayout | HookHasEffect,
                                finishedWork,
                            );
                        } finally {
                            recordLayoutEffectDuration(finishedWork);
                        }
                    } else {
                        // 通过 do...while 循环 挂载副作用
                        commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
                    }
                }
                break;
            }
            case ClassComponent: {
                const instance = finishedWork.stateNode;
                if (finishedWork.flags & Update) {
                    if (!offscreenSubtreeWasHidden) {
                        if (current === null) {

                            // 首次渲染

                            // We could update instance props and state here,
                            // but instead we rely on them being set during last render.
                            // TODO: revisit this when we implement resuming.
                          
                            // ...

                            if (
                                enableProfilerTimer &&
                                enableProfilerCommitHooks &&
                                finishedWork.mode & ProfileMode
                            ) {
                                try {
                                    startLayoutEffectTimer();
                                    // 执行 componentDidMount 生命周期函数
                                    instance.componentDidMount();
                                } finally {
                                    recordLayoutEffectDuration(finishedWork);
                                }
                            } else {
                                // 执行 componentDidMount 生命周期函数
                                instance.componentDidMount();
                            }
                        } else {

                            // 非首次渲染

                            // 获取旧的 props
                            const prevProps =
                                finishedWork.elementType === finishedWork.type
                                    ? current.memoizedProps
                                    : resolveDefaultProps(
                                        finishedWork.type,
                                        current.memoizedProps,
                                    );
                            // 获取旧的 state
                            const prevState = current.memoizedState;
                            // We could update instance props and state here,
                            // but instead we rely on them being set during last render.
                            // TODO: revisit this when we implement resuming.
                           
                            // ...


                            if (
                                enableProfilerTimer &&
                                enableProfilerCommitHooks &&
                                finishedWork.mode & ProfileMode
                            ) {
                                try {
                                    startLayoutEffectTimer();
                                    // 执行 componentDidUpdate 生命周期函数
                                    instance.componentDidUpdate(
                                        prevProps,
                                        prevState,
                                        instance.__reactInternalSnapshotBeforeUpdate,
                                    );
                                } finally {
                                    recordLayoutEffectDuration(finishedWork);
                                }
                            } else {
                                // 执行 componentDidUpdate 生命周期函数
                                instance.componentDidUpdate(
                                    prevProps,
                                    prevState,
                                    instance.__reactInternalSnapshotBeforeUpdate,
                                );
                            }
                        }
                    }
                }

                // TODO: I think this is now always non-null by the time it reaches the
                // commit phase. Consider removing the type check.
                const updateQueue: UpdateQueue<
                *,
                > | null = (finishedWork.updateQueue: any);
                if (updateQueue !== null) {
                   
                    // ...

                    // We could update instance props and state here,
                    // but instead we rely on them being set during last render.
                    // TODO: revisit this when we implement resuming.

                    // 在 commitUpdateQueue 函数中,遍历updateQueue,执行effect副作用
                    commitUpdateQueue(finishedWork, updateQueue, instance);
                }
                break;
            }
            case HostRoot: {
                // TODO: I think this is now always non-null by the time it reaches the
                // commit phase. Consider removing the type check.
                const updateQueue: UpdateQueue<
                    *,
                    > | null = (finishedWork.updateQueue: any);
                if (updateQueue !== null) {
                    let instance = null;
                    if (finishedWork.child !== null) {
                        switch (finishedWork.child.tag) {
                            case HostComponent:
                                instance = getPublicInstance(finishedWork.child.stateNode);
                                break;
                            case ClassComponent:
                                instance = finishedWork.child.stateNode;
                                break;
                        }
                    }
                    // 在 commitUpdateQueue 函数中,遍历updateQueue,执行effect副作用
                    commitUpdateQueue(finishedWork, updateQueue, instance);
                }
                break;
            }
            case HostComponent: {
                const instance: Instance = finishedWork.stateNode;

                // Renderers may schedule work to be done after host components are mounted
                // (eg DOM renderer may schedule auto-focus for inputs and form controls).
                // These effects should only be committed when components are first mounted,
                // aka when there is no current/alternate.
                if (current === null && finishedWork.flags & Update) {
                    const type = finishedWork.type;
                    const props = finishedWork.memoizedProps;
                    // commitMount 处理 input 标签有 auto-focus 的情况
                    commitMount(instance, type, props, finishedWork);
                }

                break;
            }
           
            // ...
            
        }
    }

    // 更新 ref 引用
    if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
        if (enableScopeAPI) {
            // TODO: This is a temporary solution that allowed us to transition away
            // from React Flare on www.
            if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
                commitAttachRef(finishedWork);
            }
        } else {
            if (finishedWork.flags & Ref) {
                commitAttachRef(finishedWork);
            }
        }
    }
}

在 commitLayoutEffectOnFiber 函数中,会根据组件的类型,执行不同的处理。

如果是 FunctionComponent、ForwardRef、SimpleMemoComponent 等组件,则调用 commitHookEffectListMount 函数,循环effect链表,执行effect副作用。

如果是 ClassComponent 组件,则会针对首次渲染和非首次渲染分别执行 componentDidMount 和 componentDidUpdate 生命周期函数,并调用commitUpdateQueue函数,遍历存储在 updateQueue上的effects,执行 effect 副作用。

如果是 HostComponent 组件,则调用 commitMount 处理 input 标签有 auto-focus 的情况。

commitUpdateQueue -- 处理回调

// react-reconciler/src/ReactUpdateQueue.new.js

export function commitUpdateQueue<State>(
  finishedWork: Fiber,
  finishedQueue: UpdateQueue<State>,
  instance: any,
): void {
  // Commit the effects
  // 遍历effects 列表,执行effect副作用   
  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);
      }
    }
  }
}


function callCallback(callback, context) {
  if (typeof callback !== 'function') {
    throw new Error(
      'Invalid argument passed as callback. Expected a function. Instead ' +
        `received: ${callback}`,
    );
  }

  callback.call(context);
}

存储在 updateQueue 上的 effects 副作用是通过 commitUpdateQueue 函数来执行的。在该函数中,会对 effects 进行遍历,若有callback,则执行callback,同时会重置 updateQueue 上的 effects 为 null 。

至此,layout phase 阶段的工作就完成了。

总结

本文深入解读了commit阶段的三个阶段:before mutation阶段,mutation phase 阶段、layout phase 阶段。这三个阶段主要做的事情在《commitRootImpl》小节中已有介绍,此处不再赘述。