React源码解析 (五) —— commit阶段

338 阅读7分钟

在React的渲染流程中,commit阶段是一个关键的环节,它负责将虚拟DOM的变更最终反映到实际的DOM上。

一、commit过程

image.png

React将commit阶段分为多个子阶段:

  1. before mutation : 处理副作用队列中BeforeMutationMaskflag标记的副作用
  2. mutation: 处理副作用队列中MutationMaskflag标记的副作用,以及DOM元素的增删改操作
  3. layout: 执行DOM操作后需要处理的副作用函数等逻辑

二、commit起点: commitRoot

commitRoot中对优先级进行了处理,并调用了commitRootImpl,以下代码只保留核心逻辑

function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  didIncludeRenderPhaseUpdate: boolean,
  renderPriorityLevel: EventPriority,
  spawnedLane: Lane,
  updatedLanes: Lanes,
  suspendedRetryLanes: Lanes,
  suspendedCommitReason: SuspendedCommitReason,
  completedRenderStartTime: number,
  completedRenderEndTime: number,
) {
  
  const finishedWork = root.finishedWork;
    
  const subtreeHasEffects =
    (finishedWork.subtreeFlags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
  const rootHasEffect =
    (finishedWork.flags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;

  if (subtreeHasEffects || rootHasEffect) {
  
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );

    commitMutationEffects(root, finishedWork, lanes);

    resetAfterCommit(root.containerInfo);

    root.current = finishedWork;

    commitLayoutEffects(finishedWork, root, lanes);
  } else {
    root.current = finishedWork;
  }
  return null;
}

函数内部处理了很多东西: 清空passiveEffect、上下文检查、处理性能跟踪等,这些暂时不讨论。 commitRootImpl主要进行的操作是处理副作用队列渲染DOM节点;

可以看到函数内部首先拿到了在内存中构造的Fiber树finishedWork,对树上的副作用标记进行了判断,当根节点或者子孙节点存在副作用flag时才会进入处理过程;

if (subtreeHasEffects || rootHasEffect) {
     //...
  }

三、阶段 beforeMutation

beforeMutation的处理在函数commitBeforeMutationEffects中:
其中会经过几个子阶段:

  1. commitBeforeMutationEffects_begin
  2. commitBeforeMutationEffectsDeletion
  3. commitBeforeMutationEffects_complete
export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
): boolean {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);

  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();

  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

commitBeforeMutationEffects_begin 子阶段

子阶段的开始根据不同条件进入下一子阶段

function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;

    if (enableCreateEventHandleAPI) {
      const deletions = fiber.deletions;
      if (deletions !== null) {
        for (let i = 0; i < deletions.length; i++) {
          const deletion = deletions[i];
          commitBeforeMutationEffectsDeletion(deletion);
        }
      }
    }

    const child = fiber.child;
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      child.return = fiber;
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

当开启了enableCreateEventHandleAPI,并且fiber.deletions存在时,处理需要在commit过程中删除 的节点进入commitBeforeMutationEffectsDeletion 子阶段 当fiber.subtreeFlags存在并且属于需要在该阶段进行处理的副作用类型,则指向下一个子节点,继续处理 当子节点全部处理完成之后会进入commitBeforeMutationEffects_complete

commitBeforeMutationEffects_complete 子阶段

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    commitBeforeMutationEffectsOnFiber(fiber);

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

    nextEffect = fiber.return;
  }
}

可以看到,主要的处理逻辑在commitBeforeMutationEffectsOnFiber中进行,处理完之后会寻找当前节点的兄弟节点并进行, 若兄弟节点处理完成,则会将指针往上指向父节点,在此进入commitBeforeMutationEffects_complete时处理父节点的兄弟节点,直至整个树处理完成;

commitBeforeMutationEffectsOnFiber

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  switch (finishedWork.tag) {
    case FunctionComponent: {
      if (enableUseEffectEventHook) {
        if ((flags & Update) !== NoFlags) {
          const updateQueue: FunctionComponentUpdateQueue | null =
            (finishedWork.updateQueue: any);
          const eventPayloads =
            updateQueue !== null ? updateQueue.events : null;
          if (eventPayloads !== null) {
            for (let ii = 0; ii < eventPayloads.length; ii++) {
              const {ref, nextImpl} = eventPayloads[ii];
              ref.impl = nextImpl;
            }
          }
        }
      }
      break;
    }
    case ClassComponent: {
      if ((flags & Snapshot) !== NoFlags) {
        if (current !== null) {
          commitClassSnapshot(finishedWork, current);
        }
      }
      break;
    }
    case HostRoot: {
      if ((flags & Snapshot) !== NoFlags) {
        if (supportsMutation) {
          const root = finishedWork.stateNode;
          clearContainer(root.containerInfo);
        }
      }
      break;
    }
    default: {
      if ((flags & Snapshot) !== NoFlags) {
        throw new Error(
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
      }
    }
  }
}

可以看到函数内部做了如下处理:

  1. 处理了针对函数式组件的enableUseEffectEventHook钩子
  2. 类组件中调用了commitClassSnapshot 处理类组件快照
  3. 清空HostRoot挂载的内容,方便后续处理

四、mutation阶段

mutation阶段主要逻辑在函数commitMutationEffectsOnFiber中,函数中代码比较多,可简略为如下结构:

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const prevEffectStart = pushComponentEffectStart();

  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      if (flags & Update) {
        commitHookEffectListUnmount(
          HookInsertion | HookHasEffect,
          finishedWork,
          finishedWork.return,
        );
        commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
        commitHookLayoutUnmountEffects(
          finishedWork,
          finishedWork.return,
          HookLayout | HookHasEffect,
        );
      }
      break;
    }
    ...
 }

  popComponentEffectStart(prevEffectStart);
}

可以看到内部针对不同类型的节点做了区分处理,不同组件的处理流程不一样,在需要处理的情况下有这几个过程:

删除

删除的逻辑主要是在recursivelyTraverseMutationEffects函数内部:

function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes,
) {
  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);
      }
    }
  }

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

主要还是根据deletions标识去处理是否需要删除,需要删除的情况下commitDeletionEffects被调用,整个过程还是遵循深度优先遍历的原则进行。 commitDeletionEffects后续根据不同的节点类型做了大量的处理,暂时不细看,大体如下:

  1. 调用safelyDetachRef函数清除节点的ref属性(HostComponent、ClassComponent等类型);
  2. 调用removeChildFromContainerremoveChild删除对应的子DOM节点(HostText类型);
  3. 调用clearSuspenseBoundary 处理 Suspense Boundary(DehydratedFragment类型);
  4. 调用生命周期钩子componentWillUnmount(ClassComponent类型)

插入 移动 水合操作

主要在函数commitReconciliationEffects中执行

function commitReconciliationEffects(finishedWork: Fiber) {
  const flags = finishedWork.flags;
  if (flags & Placement) {
    try {
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    finishedWork.flags &= ~Placement;
  }
  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}

其中Placement标识Fiber节点需要插入或者移动处理,Hydrating代表需要进行水合相关操作

来看一下commitPlacement中发生了什么:

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

  const parentFiber = getHostParentFiber(finishedWork);

  switch (parentFiber.tag) {
    case HostComponent: {
      const parent: Instance = parentFiber.stateNode;
      if (parentFiber.flags & ContentReset) {
        resetTextContent(parent);
        parentFiber.flags &= ~ContentReset;
      }

      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }
    case HostRoot:
    case HostPortal: {
      const parent: Container = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }

    default:
      throw new Error(
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
}

不同情况下逻辑类似,只看HostComponent的情况 首先获取到可用的父节点,便于操作:

  1. getHostParentFiber会找到待处理节点的可用祖先节点(类型为:HostComponentHostRootHostPortal)
  2. getHostSibling会找到待处理节点的第一个兄弟节点的DOM元素
  3. insertOrAppendPlacementNode 中会调用对应的方法处理节点的添加appendChild插入insertBefore操作,此过程会递归进行从而处理待处理节点的子节点

属性更新

当节点上存在Update flag时需要处理更新,以HostComponent属性的节点为例

        if (flags & Update) {
          const instance: Instance = finishedWork.stateNode;
          if (instance != null) 
            const newProps = finishedWork.memoizedProps;
            const oldProps =
              current !== null ? current.memoizedProps : newProps;
            const type = finishedWork.type;
            const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
            finishedWork.updateQueue = null;
            if (updatePayload !== null) {
              try {
                commitUpdate(
                  instance,
                  updatePayload,
                  type,
                  oldProps,
                  newProps,
                  finishedWork,
                );
              } catch (error) {
                captureCommitPhaseError(
                  finishedWork,
                  finishedWork.return,
                  error,
                );
              }
            }
          }
        }

可以看到属性更新主要进入commitUpdate处理

export function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
  // 标记一下变化的参数有哪些
  updateFiberProps(domElement, newProps);
}

经过updateProperties逻辑最终会走到updateDOMProperties函数中:

function updateDOMProperties(
  domElement: Element,
  updatePayload: Array<any>,
  wasCustomComponentTag: boolean,
  isCustomComponentTag: boolean,
): void {
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) {
      setValueForStyles(domElement, propValue);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    } else {
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}

可以看到针对不同类型的属性做了处理:

  1. STYLE:style属性
  2. DANGEROUSLY_SET_INNER_HTML: innerHTML
  3. CHILDREN:文本子节点
  4. 其它 function 、attribute等

FiberTree切换

在进入layout阶段之前还需要进行一步操作: 将处理好的最新的Fiber树切换为currentFiber树

root.current = finishedWork;

五、layout 阶段

类似beforeMutation, layout也分为多个子阶段:

  1. commitLayoutEffects_begin
  2. commitLayoutMountEffects_complete

commitLayoutEffects_begin

commitLayoutEffects_begin主要做的工作是遍历节点,然后进入commitLayoutMountEffects_complete进行后续处理

commitLayoutMountEffects_complete

function commitLayoutMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  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) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
      resetCurrentDebugFiberInDEV();
    }

    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }

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

    nextEffect = fiber.return;
  }
}

主要的逻辑在commitLayoutEffectOnFiber中进行

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: {
          // ...
        break;
      }
      case ClassComponent: {
          // ...
        break;
      }
      case HostRoot: {

        break;
      }
      case HostComponent: {
        // ...
        break;
      }
      case HostText: {
        // We have no life-cycles associated with text.
        break;
      }
      
      //...

      default:
        throw new Error(
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
    }
  }
}

代码量很多,主要也是针对不同类型的节点分类处理:

  • 调用commitUpdateQueue执行更新后的节点的副作用队列的callback
  • 调用componentDidMountcomponentDidUpdate钩子 (ClassComponent类型)
  • 调用commitMount处理DOM元素上的autoFocus属性 (HostComponent类型)
  • 触发useLayoutEffect钩子(FunctionComponent类型)

总结

本文详细介绍了React的commit过程,包括其子阶段和核心逻辑。commit过程是React渲染流程的一部分,负责将虚拟DOM的变化应用到实际的DOM上。文章分为以下几个部分:

  1. Commit过程:React将commit阶段分为三个子阶段:before mutationmutationlayout,分别处理不同类型的副作用和DOM操作。
  2. Commit起点commitRoot是commit过程的起点,它处理优先级并调用commitRootImpl函数,后者主要负责处理副作用队列和渲染DOM节点。
  3. BeforeMutation阶段beforeMutation阶段处理BeforeMutationMask标记的副作用,包括删除操作和对DOM元素的准备工作。
  4. Mutation阶段mutation阶段处理MutationMask标记的副作用,包括DOM元素的增删改操作。
  5. Layout阶段layout阶段处理DOM操作后的副作用函数等逻辑,包括触发生命周期钩子和useLayoutEffect钩子。