React源码剖析:fiber树的协调与渲染

本系列文章将深入源码剖析react的工作流程,在开始之前有几点说明:

  • 本文解析的react源码基于react17.0.3
  • 本文图示的源码都省略了与主逻辑无关的代码
  • 本文对每个模块将按照流程概述,源码图示,源码解析的流程进行

宏观架构与核心模块

让我们打开react源码目录,进入packages文件夹,重点关注以下几个文件夹

image.png

他们是构成react宏观架构的核心模块,其基本职责如下:

  • react React的基础模块,提供了绝大部分开发中使用的API,如react.Component,react.useState等。
  • react-reconciler 协调器,用于处理并生成fiber树(react中fiber节点即代表vnode,fiber树即vnode tree),输出给渲染器进行渲染。
  • react-dom react用于web端的渲染器,将协调器的输出结果渲染到web页面中
  • scheduler 调度器,用于注册任务,并在合适的时机执行任务,通常与协调器配合使用。

那么这几个模块之间是如何相互配合完成工作的呢?我们可以通过chrome devtools一探究竟:新建一个react项目并启动,打开proformance面板,刷新页面,找到render函数,观察整个流程的函数调用栈


image.png

我们可以将整个流程划分为三个阶段,他们分别与前面介绍的调度器,协调器,渲染器的工作一一对应。即首先进入调度阶段调度本次更新任务,调度成功后进入render阶段,处理本次更新并生成fiber树,最后进入渲染阶段将fiber树渲染到视图中。

接下来我们深入源码剖析协调与渲染阶段的工作细节,关于调度阶段的具体实现后续再继续讨论,我们现在只需要知道它会调度协调器注册的回调任务,并在某个合适的时机执行。

协调阶段(Render)

双缓存模型

在开始介绍render阶段之前,让我们先来了解一下双缓存模型。双缓存模型是一个解决图形编程中“闪烁问题”的方案。其基本原理是在内存中绘制当前帧,绘制完毕后直接用当前帧替换上一帧,由于省掉了帧与帧之间替换的时间因此可以有效的避免闪烁问题。
我们已经知道协调阶段的主要职责就是输出fiber树,事实上react正是使用双缓存模型来完成fiber树的构建,具体来说,react中最多会同时存在两颗fiber树,他们分别是workInProgressFiber树和currentFiber树,后面就简称(WIP和CUR),两棵树中的fiber节点通过alternate属性连接。其中CUR代表页面当前状态(正在展示的内容),WIP是正在内存中构建的fiber树,他代表了在本次更新后页面的状态。react应用的通过根fiber节点fiberRoot的current指针来完成两颗fiber树的更替。页面首次渲染时,current为null,当WIP构建完成并完成渲染后,current指向WIP,即此时WIP变成CUR。当触发状态更新时,会重新生成一颗WIP,以此往复,接下来我们以首屏渲染为例(current为null)来分析render阶段与commit阶段的主要工作。

入口函数

render阶段主要工作是构建WIP,他开始于performSyncWorkOnRoot函数或performConcurrentWorkOnRoot函数,这取决于本次更新是同步更新还是异步更新。


image.png

关于ensureRootIsScheduled的逻辑以及更新的优先级我们暂时先不关心,目前只需知道此函数会以不同的优先级注册本次更新的回调任务即可,我们发现render阶段的实际起点函数是performUnitOfWork,他会在一个循环中执行。performUnitOfWork职责是创建当前fiber节点的下一个fiber节点并赋值给workInProgress,并将当前节点与已创建的fiber节点连接起来。接下来看一下其实现细节:

function performUnitOfWork(unitOfWork: Fiber): void {
// 传参unitOfWork是当前fiber节点
  const current = unitOfWork.alternate;
  let next;
  ...
  // beginWork方法会创建当前fiber节点的第一个子fiber节点
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

可以发现,render阶段的执行其实是一个深度优先遍历的过程,它有两个核心函数,beginWork和completeUnitOfWork,他们的职责与执行时机有所不同,在遍历的过程中,会对每个遍历到的节点执行beginWork创建子fiber节点。若当前节点不存在子节点(next === null),则对其执行completeUnitOfWork。completeUnitOfWork方法内部会判断当前节点有无兄弟节点,有则进入兄弟节点的beinWork流程,否则进入父节点的completeUnitOfWork流程(此逻辑后面会分析)。

beginWork

我们已经明确beginWork的核心职责是创建当前节点的子fiber节点,接下来看其具体实现。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
   // update时的逻辑
  } else {
    didReceiveUpdate = false;
  }
  workInProgress.lanes = NoLanes;
  switch (workInProgress.tag) {
    case FunctionComponent: {
     .....
    }
    case ClassComponent: {
     .....
    }
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
  }
}

可以看到beginWork的执行大致分为两段逻辑:

  • 第一段 根据current是否存在进入不同逻辑,由于首屏渲染时current为null,因此我们暂时不关注,后面分析状态更新流程时再来看。
  • 第二段,根据不同tag类型进入不同case,我们重点关注最常见的HostComponent类型,即普通dom节点,他会进入updateHostComponent方法
function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  pushHostContext(workInProgress);
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  // 获取子节点
  let nextChildren = nextProps.children;
  const isDirectTextChild = shouldSetTextContent(type, nextProps);
  // 单一文本子节点不创建fiber节点
  if (isDirectTextChild) {
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    workInProgress.flags |= ContentReset;
  }
  markRef(current, workInProgress);
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

updateHostComponent最主要的逻辑是执行了reconcileChildren并返回workInProgress.child,因此可以合理猜测reconcileChildren内部会创建子fiber节点并将其与当前fiber节点(workInProgress)通过child指针连接。

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}

reconcileChildren内部根据current是否为null进入不同逻辑。我们看一下mountChildFibers和reconcileChildFibers这两个函数。

// react/packages/react-reconciler/src/ReactChildFiber.old.js
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackSideEffects) {
  ///  
  return reconcileChildFibers;
}

可以看到这两个函数事实上是同一个函数,即ChildReconciler的返回值reconcileChildFibers,只是传入ChildReconciler的参数shouldTrackSideEffects(追踪副作用)不同,事实上此参数与update时为fiber节点标记副作用(flags)的逻辑有关,后续在分析update的流程时会继续讨论,接下来我们看下reconcileChildFibers的实现逻辑:

function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
      }

      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
    }

    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

其主要职责就是根据子节点类型创建fiebr节点,以reconcileSingleElement为例看下其调用栈:

reconcileSingleElement => 
createFiberFromElement =>
createFiberFromTypeAndProps=>
createFiber=> return new FiberNode

最终返回了一个FiberNode实例,我们可以看一下FiberNode的结构,作为一种数据结构来描述dom节点,它有众多描述属性(可分为5大类,位置,类型,状态,副作用,alternate),现在我们先只关心如下几个属性,他们是多个fiber节点之间相互连接构成fiber树的核心属性。

sibling 兄弟节点
return 父节点
child 子节点

此外我们关注下reconcileChildrenArray的逻辑,若当前节点存在多个子节点时会进入该逻辑

<div>
   <p></p>
   <p></p>
   <p></p>
 </div>

reconcileChildrenArray的职责可概括为:为所有子节点创建fiber节点,并返回第一个子fiber节点,其余子节点通过sibling指针连接。
前面在分析performUnitOfWork时提到,在当前节点的completeUnitOfWork流程中会判断当前节点有无兄弟节点,其判断的依据就是通过sibling指针。

至此beginWork的大体逻辑已梳理完毕,接下来分析completeUnitOfWork

completeWork

在pergormUnitOfWork中我们发现,遍历阶段的入口函数是beginWork,但回溯阶段的入口却不是completeWork,而是completeUnitOfWork,事实上completeUnitOfWork内部会执行completeWork,接下来我们先分析completeUnitOfWork

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    if ((completedWork.flags & Incomplete) === NoFlags) {
      let next;
      // 为传入的节点执行completeWork
      next = completeWork(current, completedWork, subtreeRenderLanes);
      if (next !== null) {
        // completedWork派生出其他子节点时,回到beginWork阶段继续执行
        workInProgress = next;
        return;
      }
    } else {
      // 异常处理
    }
    const siblingFiber = completedWork.sibling;
    // 若存在兄弟节点 则进入其beginWork流程
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    // 不存在兄弟节点 则进入父节点的completeWork流程
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);

  // 回溯到根节点时,设置根节点状态
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

我们发现其执行流程与我们前面的分析是一致的 ,接下来分析completeWork的具体逻辑

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  popTreeContext(workInProgress);
  switch (workInProgress.tag) {
    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
       // update的流程,暂时先不关注
      } else {
        const currentHostContext = getHostContext();
        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
         // 服务端渲染的逻辑 咱不关注
        } else {
          // 为当前fiber创建dom实例
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          // 将子孙dom节点追加到当前创建的dom节点上
          appendAllChildren(instance, workInProgress, false, false);
          // 将当前创建的挂载到stateNode属性上
          workInProgress.stateNode = instance;
          // 处理props(绑定回调,设置dom属性...)
          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        }
        // ref属性相关逻辑
        if (workInProgress.ref !== null) {
          markRef(workInProgress);
        }
      }
      return null;
    }
  }
}

我们发现completeWork内部仍会对不同tag类型进入不同case,我们还是重点关注hostComponent类型的逻辑,他会根据current状态进入不同逻辑,我们分析首屏渲染时的逻辑,发现主要有以下几步

  • createInstance 为当前fiber创建dom实例
createInstance =>
createElement => 
document.createElement
  • appendAllChildren 遍历所有同级子代节点,执行父节点的appenChild方法,即此方法会将所有子dom节点与当前创建的dom实例连接

  • 赋值stateNode属性

  • finalizeInitialChildren 处理props

    至此,首屏渲染时render阶段的大体流程就梳理完了,现在我们可以捋一下beginWork与completeWork这二者是如何相互配合共同完成fiebr树的构建的。我们以一个简单的jsx结构为例:

 return (
      <>
        <div>
          <span>age: 18</span>
          <p>
            <span>name: zs</span>
          </p>
        </div>
      </>
    );

首先会创建根fiber节点与当前应用(App)的fiber节点,此逻辑具体流程后续会展开分析。

  • 执行div的beginWork,创建第一个span1对应的fiber节点与p对应的fiber节点,同时会将span.sibling指向p,使得span执行完completeWork可以进入p的beginWork阶段
  • 执行span的beginWork
  • 执行span的completeWork
  • 执行p的beginWork
  • 执行span2的completeWork
  • 执行span2的completeWork
  • 执行p的completeWork
  • 执行div的completeWork

我们最终会得到这样的一颗Fiber树

image.png

至此,render阶段全部工作已经完成,我们得到了WIP以及对应的dom树,会被赋值给fiberRoot.finisheWork,接下来的工作就是将渲染WIP,也就是提交阶段(commit)的流程。

提交阶段(commit)

由render转入commit的流程是在performSyncWorkOnRoot或performConcurrentWorkOnRoot中进行的,执行哪个看本次更新的优先级。

performSyncWorkOnRoot

function performSyncWorkOnRoot(root) {
  ...
  // 赋值WIP
  const finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  // commit阶段的入口函数
  commitRoot(root)
}
function commitRoot(root) {
  const previousUpdateLanePriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  try {
    ReactCurrentBatchConfig.transition = 0;
    setCurrentUpdatePriority(DiscreteEventPriority);
    commitRootImpl(root, previousUpdateLanePriority);
  } finally {
    ReactCurrentBatchConfig.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }
  return null;
}

commitRoot是commit阶段的入口函数,其入参root代表react根fiber节点。观察commit阶段的入口函数commitRoot,他会获取本次更新的优先级,并执行commitRootImpl,commit阶段的主逻辑就在此方法中,下面我们分析该方法的执行细节。

我们知道,commit阶段的主要职责就是将render阶段创建的fiber树渲染到页面中,也就是要执行具体的dom操作,因此commit阶段的主要流程可分为三个阶段

  • 渲染前(before mutation)
  • 渲染(mutation)
  • 渲染后(layout)

各个阶段分别会执行自己的主函数

  • before mutation阶段:commitBeforeMutationEffects
  • mutation阶段:commitMutationEffects
  • layout阶段 commitLayoutEffects

在beforeMutation之前以及layout之后还有一些逻辑,接下来就按这5段逻辑逐段分析

beforeMutation之前的逻辑

do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
  // root.finishedWork即是WIP
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  // 清空FiberRoot对象上的属性
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  root.callbackNode = null;
  root.callbackPriority = NoLane;
  // 重置全局变量
  if (root === workInProgressRoot) {
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  } 
  // 调度useEffect 
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      pendingPassiveEffectsRemainingLanes = remainingLanes;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

我们注意到此阶段首先会循环执行flushPassiveEffects这个函数,且之后还会以一个优先级来调度执行此函数,关于flushPassiveEffects的具体逻辑后面再展开讨论,现在只需知道他与函数组件的useEffect钩子的处理有关。此外此阶段还会进行一些变量赋值,状态重置的工作。

commitBeforeMutationEffects

接下来看beforeMutation阶段的主函数,他的调用栈如下,commit阶段的三个主函数的调用栈都是这样的

commitBeforeMutationEffects =>
commitBeforeMutationEffects_begin =>
commitBeforeMutationEffects_complete

我们分别来分析

function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
  //nextEffect是一个全局指针,首次调用传入的firstChild是finishedWork
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();
}
function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const child = fiber.child;
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      // 确认当前父节点指针
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

commitBeforeMutationEffects_begin中会向下遍历child指针。为每个遍历到的节点执行ensureCorrectReturnPointer,若不存在子节点则进入当前节点的commitBeforeMutationEffects_complete。

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

commitBeforeMutationEffects_complete会为当前的nextEffect执行commitBeforeMutationEffectsOnFiber,之后会判断sibling指针,若存在兄弟节点则进入其commitBeforeMutationEffects_begin,否则进入父节点的commitBeforeMutationEffects_complete。我们发现其实这个流程与render阶段的两个函数的执行是类似的,事实上,commit阶段的三个子阶段的主函数的执行顺序都是如此,后面就不再赘述。

下面分析commitBeforeMutationEffectsOnFiber的逻辑

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  if ((flags & Snapshot) !== NoFlags) {
    switch (finishedWork.tag) {
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // 执行Class组件的getSnapshotBeforeUpdate生命周期
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
    }
  }
}

至此我们可以明确beforeMutation阶段的主要职责就是执行Class组件的getsnapshotBeforeUpdate生命周期。

commitMutationEffects

下面分析mutation阶段主函数commitMutationEffects

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];
        commitDeletion(root, childToDelete, fiber);
      }
    }
    const child = fiber.child;
    if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitMutationEffects_complete(root);
    }
  }
}

commitMutationEffects_begind的逻辑主要是判是否有需要删除的子节点,执行commitDeletion。其调用栈如下

commitDeletion=>
unmountHostComponents=>
commitUnmount

具体的删除逻辑在commitUnmount中,我们来看下

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) {
          const firstEffect = lastEffect.next;
          let effect = firstEffect;
           do {
            const {destroy, tag} = effect;
            // 执行useLayoutEffect的销毁函数
            if ((tag & HookLayout) !== NoHookEffect) {
              safelyCallDestroy(current, nearestMountedAncestor, destroy);
            }
            effect = effect.next;
          } while (effect !== firstEffect);
        }
      }
      return;
    }
    case ClassComponent: {
      safelyDetachRef(current, nearestMountedAncestor);
      const instance = current.stateNode;
      if (typeof instance.componentWillUnmount === 'function') {
        safelyCallComponentWillUnmount(
          current,
          nearestMountedAncestor,
          instance,
        );
      }
      return;
    }
    case HostComponent: {
      safelyDetachRef(current, nearestMountedAncestor);
      return;
    }
  }
}

根据tag类型进入不同逻辑:

  • 函数组件 执行useLayoutEffect的销毁函数(destory方法)
  • class组件 调用componentsWillUnMount生命周期钩子
  • hostComponents 解绑ref属性
function commitMutationEffects_complete(root: FiberRoot) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    setCurrentDebugFiberInDEV(fiber);
    commitMutationEffectsOnFiber(fiber, root);
    resetCurrentDebugFiberInDEV();
    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

为每个遍历到的fiber执行commitMutationEffectsOnFiber

function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
  const flags = finishedWork.flags;
  // 处理ref 
  if (flags & Ref) {
    const current = finishedWork.alternate;
    if (current !== null) {
      commitDetachRef(current);
    }
  }
  const primaryFlags = flags & (Placement | Update | Hydrating);
  outer: switch (primaryFlags) {
    case Placement: {
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      break;
    }
    case PlacementAndUpdate: {
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    case Update: {
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
  }
}

根据flags不同类型进入不同case,调用相应处理函数后删除对应的flags。我们分别来分析,首先看placement的处理函数commitPlacement

function commitPlacement(finishedWork: Fiber): void {
 // 查找最近的父节点
  const parentFiber = getHostParentFiber(finishedWork);
  let parent;
  const parentStateNode = parentFiber.stateNode;
  if (parentFiber.flags & ContentReset) {
    resetTextContent(parent);
    parentFiber.flags &= ~ContentReset;
  }
  // 查找最近的兄弟节点
  const before = getHostSibling(finishedWork);
  insertOrAppendPlacementNode(finishedWork, before, parent);
}

总结commitPlacement的职责:查找离它最近的父级host类型的fiber节点以及离它最近的host类型的兄弟节点,调用insertOrAppendPlacementNode进行插入,该函数的主逻辑是兄弟节点存在则调用其insertBefore方法,否则调用父节点的appendChild方法进行插入。

下面分析update对应的处理函数commitWork

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
     // 执行所有useLayoutEffect的销毁函数
      commitHookEffectListUnmount(
          HookLayout | HookHasEffect,
          finishedWork,
          finishedWork.return,
        );
      return;
    }
    case HostComponent: {
      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) {
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    } 
  }
}

根据不通tag类型进入不同case:

  • 对于函数组件会执行commitHookEffectListUnmount,从名字上不难分析出此函数用于执行副作用的销毁函数,这里的第一个入参是HookLayout,这表明这里会执行所有useLayoutEffect的销毁函数。
  • 对于HostComponent会执行commitUpdate,此函数负责更新具体的dom属性,传入方法的updatePayload属性,他是一个数组,存储了本次需要更新的key和value,后面分析状态更新流程时会继续讨论。

PlacementAndUpdate 也就是即要插入又要开始更新 ,先执行commitPlacement再执行commitWork。

commitLayoutEffect

下面分析layout阶段的主函数commitLayoutEffect,要注意一点,在执行commitLayoutEffect之前,会执行root.current = finishwork,这印证了之前分析的双缓存架构,经过mutation阶段,WIP已经渲染完成,fiberRoot.current就指向了代表当前界面的fiber树,因此layout阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM。

function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;
    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      ensureCorrectReturnPointer(firstChild, fiber);
      nextEffect = firstChild;
    } else {
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}
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);
    }

    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, 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: {
        if (
          !enableSuspenseLayoutEffectSemantics ||
          !offscreenSubtreeWasHidden
        ) {
          // 执行useLayoutEffect的回调
          commitHookEffectListMount(
            HookLayout | HookHasEffect,
            finishedWork,
          );
        }
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
           // 根据current是否存在执行不同生命周期
            if (current === null) {
              if (
                enableProfilerTimer &&
                enableProfilerCommitHooks &&
                finishedWork.mode & ProfileMode
              ) {
                instance.componentDidMount();
              } else {
                instance.componentDidMount();
              }
            } else {
              const prevProps =
                finishedWork.elementType === finishedWork.type
                  ? current.memoizedProps
                  : resolveDefaultProps(
                      finishedWork.type,
                      current.memoizedProps,
                    );
              const prevState = current.memoizedState;
              instance.componentDidUpdate(
                prevProps,
                prevState,
                instance.__reactInternalSnapshotBeforeUpdate,
              );
            }
          }
        }

        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
    
    }
  }
}

依然是根据不同tag进入不同case,对于函数组件会执行commitHookEffectListMount,事实上此方法与前面分析的commitHookEffectListUnmount方法是相对应的,前者执行指定副作用的回调函数,后者执行指定副作用的销毁函数,我们来看一下commitHookEffectListMount的大概实现

function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
  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) {
        // create即我们在副作用中指定的回调
        const create = effect.create;
        // 执行回调得到销毁函数,赋值给destroy,将来会在commitHookEffectListUnmount中执行
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

此方法的第一个入参是副作用类型,方法内会遍历当前fiber的所有副作用链表,符合当前指定的副作用会执行其create函数并得到对应的销毁函数。这里传入方法的flag是useLayoutEffect对应的flag,因此我们可以明确,useLayoutEffect的回调函数会在layout阶段同步执行。

对于class组件则会根据current是否存在来决定执行compinentDidMount还是ComponentDidUpdate,此外还会执行commitUpdateQueue,该方法用于执行我们在this.setState中指定的callback(假设指定了)。

layout之后的逻辑

if (rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsLanes = lanes;
  } 
  // 1. 检测常规(异步)任务, 如果有则会发起异步调度
  ensureRootIsScheduled(root, now());
  // 2. 检测同步任务, 如果有则主动调用flushSyncCallbackQueue,再次进入fiber树构造循环
  flushSyncCallbacks
  return null;

layout阶段的最后会判断rootDoesHavePassiveEffects,即看看是否有未处理的副作用,有则将rootWithPendingPassiveEffects赋值为root(整个应用的根节点),这有什么用呢?让我们回到commitRootImpl的方法开头,它会循环判断rootWithPendingPassiveEffects,当其不为null时,执行flushPassiveEffects,此函数的调用栈如下:

flushPassiveEffects =>
flushPassiveEffectsImpl=>
// 执行useEffect的销毁函数
commitPassiveUnmountEffects(root.current);
// 执行useEffect的create函数
commitPassiveMountEffects(root, root.current);

总结flushPassiveEffects的职责就是执行useEffect在上次更新的产生的销毁函数以及本次更新的回调函数。因此我们可以明确commit阶段开始之前会先清理掉之前遗留的effect,由于effect中又可能触发新的更新而产生新的effect,因此要循环判断rootWithPendingPassiveEffects直到为null。

我们之前在介绍before mutation阶段之前的逻辑时提到过该阶段会以一个优先级来调度执行flushPassiveEffects,这表明在这里注册的flushPassiveEffects会在commit阶段之后执行,而我们已经知道了flushPassiveEffects的职责,因此也就明确了我们在useEffect中指定的回调是会在dom渲染后异步执行的,这就有别于useLayoutEffect,我们不妨来梳理下二者的回调和销毁的执行时机。

  • useLayoutEffect的销毁函数在mutation阶段执行
  • useLayoutEffect的回调在layout阶段执行
  • useEffect的销毁和回调都是在commit阶段后异步执行,先执行上次更新产生的销毁,再执行本次更新的回调。

我们发现相比useEffect,useLayoutEffect无论销毁函数和回调函数的执行时机都要更早一些,且会在commit阶段中同步执行。因此我们说useLayoutEffects中适合dom操作,因为其回调中进行dom操作时,由于此时页面未完成渲染,因此不会有中间状态的产生,可以有效的避免闪动问题。但同时useLayoutEffects的执行会阻塞渲染,因此需谨慎使用。

至此首屏渲染的render与commit的流程就梳理完成了,接下来分析update流程与mount流程的不同点。

状态更新的流程

分析update的流程我们只需抓住与mount的不同:current不为null,由于二者的不同主要体现在render阶段,因为我们分别分析beginWork与completeWork的不同。

beginwork

我们已经知道beginWork的逻辑总体分两段,第一段会根据current的状态进入不同逻辑,我们来分析第一段逻辑中current不为null的逻辑:

 if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else {
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );
      if (
        !hasScheduledUpdateOrContext &&
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } 

通过一系列判断逻辑判断当前节点是否可复用,用didReceiveUpdate来标记,若可复用则走attemptEarlyBailoutIfNoScheduledUpdate。调用栈如下

attemptEarlyBailoutIfNoScheduledUpdate =>
bailoutOnAlreadyFinishedWork=>
cloneChildFibers

顾名思义,会直接克隆一个fiber节点并返回。

beginWork第二阶段的逻辑是mount与update共用的,当节点无法复用时会调用reconcileChildren生成子节点,其内部会根据current是否存在进入mountChildFibers(current为null)或reconcileChildFibers(current不为null),我们已经知道这两者的逻辑基本是相同的,只是reconcileChildFibers会为当前fiber打上 flags,它代表当前dom需要执行的操作(插入,更新,删除等),以插入(Placement)为例,他会在placeSingleChild方法中为当前节点打上Placement的标记。

function placeSingleChild(newFiber: Fiber): Fiber {
   // shouldTrackSideEffects代表需要追踪副作用,update时会将其标记为true
   // 当前fiber不存在dom实例时,才可标记Placement
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags |= Placement;
    }
    return newFiber;
  }

另外,由于mount时current不存在,因此reconcileChildFibers不会有对比更新的逻辑,直接创建节点,而update时则会将current与当前的ReactElement做对比生成WIP,也就是diff算法,具体实现细节这里不展开。Ï

complete阶段

我们已经知道,mount时completeWork会直接创建dom实例,而update会调用updateHostComponent,我们来分析其实现逻辑

updateHostComponent = function(
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    const oldProps = current.memoizedProps;
    if (oldProps === newProps) {
      return;
    }
    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();
    // 对比props 生成updatePayload
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );
    workInProgress.updateQueue = (updatePayload: any);
    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

在新旧props不同时调用prepareUpdate,他会对比新旧props并生成updatePayload,其调用栈如下

prepareUpdate =>
diffProperties

我们来分析下diffProperties的主要逻辑

function diffProperties(
  domElement: Element,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
  rootContainerElement: Element | Document,
): null | Array<mixed> {
  let updatePayload: null | Array<any> = null;
  let lastProps: Object;
  let nextProps: Object;
  //处理新旧props 针对表单标签做特殊处理
  switch (tag) {
    case 'input':
      lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
      nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
      updatePayload = [];
      break;
    case 'select':
      lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
      nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
      updatePayload = [];
      break;
    case 'textarea':
      lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
      nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
      updatePayload = [];
      break;
    default:
      lastProps = lastRawProps;
      nextProps = nextRawProps;
      if (
        typeof lastProps.onClick !== 'function' &&
        typeof nextProps.onClick === 'function'
      ) {
        // TODO: This cast may not be sound for SVG, MathML or custom elements.
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      break;
  }

  assertValidProps(tag, nextProps);

  let propKey;
  let styleName;
  let styleUpdates = null;
  for (propKey in lastProps) {
    if (
      nextProps.hasOwnProperty(propKey) ||
      !lastProps.hasOwnProperty(propKey) ||
      lastProps[propKey] == null
    ) {
      continue;
    }
    // 新无旧有时进入一下逻辑,即属性被删除
    if (propKey === STYLE) {
      const lastStyle = lastProps[propKey];
      // 将对应style属性置空
      for (styleName in lastStyle) {
        if (lastStyle.hasOwnProperty(styleName)) {
          if (!styleUpdates) {
            styleUpdates = {};
          }
          styleUpdates[styleName] = '';
        }
      }
    } else {
      // 将对应key及value(null)推入更新队列,
      (updatePayload = updatePayload || []).push(propKey, null);
    }
  }
  for (propKey in nextProps) {
    const nextProp = nextProps[propKey];
    const lastProp = lastProps != null ? lastProps[propKey] : undefined;
    if (
      !nextProps.hasOwnProperty(propKey) ||
      nextProp === lastProp ||
      (nextProp == null && lastProp == null)
    ) {
      continue;
    }
    // 新有旧无或新旧都有切不相等时进入以下逻辑

    // style属性特殊处理 总结如下
    // 新有旧无 推入style队列
    // 新旧都有 用新的
    // 新无旧有 将对应属性置空
    if (propKey === STYLE) {
      if (lastProp) {
        for (styleName in lastProp) {
          if (
            lastProp.hasOwnProperty(styleName) &&
            (!nextProp || !nextProp.hasOwnProperty(styleName))
          ) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = '';
          }
        }
        for (styleName in nextProp) {
          if (
            nextProp.hasOwnProperty(styleName) &&
            lastProp[styleName] !== nextProp[styleName]
          ) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = nextProp[styleName];
          }
        }
      } else {
        if (!styleUpdates) {
          if (!updatePayload) {
            updatePayload = [];
          }
          updatePayload.push(propKey, styleUpdates);
        }
        styleUpdates = nextProp;
      }
    } else {
      // 将对应key及value推入更新队列
      (updatePayload = updatePayload || []).push(propKey, nextProp);
    }
  }
  if (styleUpdates) {
    (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
  }
  return updatePayload;
}

diffProperties内部主逻辑是对props进行两轮循环,分别处理属性删除与属性新增的情况,最终返回updatePayload,这是一个数组,第i项和第i+1项分别是更新后的的key和value。他会在mutation阶段被用于更新节点属性。updateHostComponent的最后调用进行markUpdate,赋值更新的flags(Update)。

至此mount与update流程的协调器与渲染器的职责与工作细节已经梳理完了,但到目前为止还有些问题没有解决例如:

  • 从触发更新到render阶段之前的逻辑
  • 根fiber节点的的创建
  • 更新优先级调度的逻辑
  • update对象的创建
    ......
    这些问题将在后续对react优先级与异步可中断更新的讨论中一一分析。

参考
https://juejin.cn/post/6994624240771153927#heading-4
https://react.iamkasong.com/process/completeWork.html#%E5%A4%84%E7%90%86hostcomponent

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容