render阶段中最后调用的commitRoot是commit阶段的工作起点。fiberRootNode作为传参commitRoot(root)

commit

  • 在rootFiber.firstEffct上保存了一条需要执行副作用的Fiber节点的单向链表effectList,最近的一次更新中react官方使用了subtreeHasEffects和rootHasEffect获取副作用的Fiber节点
    ,这些Fiber节点的updateQueue中保存了变化的props
  • 这些副作用对应的DOM操作会在commit阶段执行
  • 部分生命周期钩子函数componentDid***,hook(useEffect)需要在commit阶段执行

commit阶段的主要工作(即Renderer的工作流程):

  1. before mutation阶段(执行DOM操作之前)
  2. mutation阶段(执行DOM操作)
  3. layout阶段(执行DOM操作之后)

commitRootImpl-->commitRootImpl
在before mutation阶段组件之前以及layout阶段之后还涉及到useEffect的触发,优先级重置,ref的绑定/解绑等

commit流程

before mutation 之前

commitRootImpl方法中一直到if (subtreeHasEffects || rootHasEffect)之前,属于before mutation之前

do {
    // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
    // means `flushPassiveEffects` will sometimes result in additional
    // passive effects. So we need to keep flushing in a loop until there are
    // no more pending effects.
    // TODO: Might be better if `flushPassiveEffects` did not automatically
    // flush synchronous work at the end, to avoid factoring hazards like this.
    // 触发useEffect回调与其他的同步任务,由于可能会引发新的渲染。所以这里需要遍历到最终没有任务为止
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
    // root : fiberRootNode
    // root.finishedLanes 指当前应用的rootFiber
  const finishedWork = root.finishedWork;
    // lanes 与优先级相关的变量
  const lanes = root.finishedLanes;

  if (enableSchedulingProfiler) {
    markCommitStarted(lanes);
  }

  if (finishedWork === null) {

    if (enableSchedulingProfiler) {
      markCommitStopped();
    }

    return null;
  }

  // 重置Scheduler绑定的回调
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  // commitRoot never returns a continuation; it always finishes synchronously.
  // So we can clear these now to allow a new callback to be scheduled.
  root.callbackNode = null;

  // Update the first and last pending times on this root. The new first
  // pending time is whatever is left on the root fiber.
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // 重置优先级变量
  markRootFinished(root, remainingLanes);

  // Clear already finished discrete updates in case that a later call of
  // `flushDiscreteUpdates` starts a useless render pass which may cancels
  // a scheduled timeout.
  // 清理已经完成的discrete updates,eg: 鼠标点击触发的更新
  if (rootsWithPendingDiscreteUpdates !== null) {
    if (
      !hasDiscreteLanes(remainingLanes) &&
      rootsWithPendingDiscreteUpdates.has(root)
    ) {
      rootsWithPendingDiscreteUpdates.delete(root);
    }
  }
    // 重置全局变量
  if (root === workInProgressRoot) {
    // We can reset these now that they are finished.
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  } else {
    // This indicates that the last root we worked on is not the same one that
    // we're committing now. This most commonly happens when a suspended root
    // times out.
  }
  // Check if there are any effects in the whole tree.
  // TODO: This is left over from the effect list implementation, where we had
  // to check for the existence of `firstEffect` to satsify Flow. I think the
  // only other reason this optimization exists is because it affects profiling.
  // Reconsider whether this is necessary.
  // 判断当前的树上面是否还有副作用的节点
  const subtreeHasEffects =
    (finishedWork.subtreeFlags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
  const rootHasEffect =
    (finishedWork.flags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
    if (subtreeHasEffects || rootHasEffect) {

    } else {
        // No effects.
        root.current = finishedWork;
        // Measure these anyway so the flamegraph explicitly shows that there were
        // no effects.
        // TODO: Maybe there's a better way to report this.
        if (enableProfilerTimer) {
            recordCommitTime();
        }
    }

before mutation之前的阶段都是在处理变量的赋值,以及状态的重置等操作

layout 之后

const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

// useEffect相关
if (rootDoesHavePassiveEffects) {
  rootDoesHavePassiveEffects = false;
  rootWithPendingPassiveEffects = root;
  pendingPassiveEffectsLanes = lanes;
  pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {}

// 性能优化相关
if (remainingLanes !== NoLanes) {
  if (enableSchedulerTracing) {
    // ...
  }
} else {
  // ...
}

// 性能优化相关
if (enableSchedulerTracing) {
  if (!rootDidHavePassiveEffects) {
    // ...
  }
}

// ...检测无限循环的同步任务
if (remainingLanes === SyncLane) {
  // ...
} 

// 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
ensureRootIsScheduled(root, now());

// ...处理未捕获错误及老版本遗留的边界问题
// 执行同步任务,这样同步任务不需要等到下次事件循环再执行
// 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
// 或useLayoutEffect
flushSyncCallbackQueue();
return null;

主要包含三个方面:

  1. useEffect的相关处理
  2. 性能追踪
  3. 在commit阶段会触发一些生命周期钩子(componentDid***)和hook(useLayoutEffect, useEffect)

在这些回调方法中可能会触发新的更新,新的更新会开启新的render-commit过程,例如:

  1. 触发点击事件,页面中令一个状态变为0
  2. 在useLayoutEffect中设置为随机数
  3. 页面上渲染的数字不是0,而是变成随机数

因为useEffectLayout在layout阶段同步执行回调,回调中触发状态更新,这个操作会重新调度一个新的任务:--->flushSyncCallbackQueue

// If layout work was scheduled, flush it now.
  flushSyncCallbackQueue();

所以看不到页面中的数字先变成0

before mutation

Renderer工作的阶段被称为commit阶段,commit阶段分为三个部分

  • before mutation(执行DOM操作前)

  • mutation阶段(执行DOM操作)

  • layout阶段(执行DOM操作之后)

    before mutation阶段

    // 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
    

const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);

// 将当前上下文标记为CommitContext,作为commit阶段的标志
const prevExecutionContext = executionContext;
executionContext |= CommitContext;

// 处理focus状态
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;

// beforeMutation阶段的主函数
commitBeforeMutationEffects(finishedWork);

focusedInstanceHandle = null;

这里需要关注主函数`commitBeforeMutationEffects`的作用

### commitBeforeMutationEffects

`commitBeforeMutationEffects` 直接去调度`commitBeforeMutationEffectsImpl`
代码逻辑:
``` javascript
function commitBeforeMutationEffects(firstChild: Fiber) {
let fiber = firstChild;
while (fiber !== null) {
  if (fiber.deletions !== null) {
    commitBeforeMutationEffectsDeletions(fiber.deletions);
  }
  if (fiber.child !== null) {
    const primarySubtreeFlags = fiber.subtreeFlags & BeforeMutationMask;
    if (primarySubtreeFlags !== NoFlags) {
      commitBeforeMutationEffects(fiber.child);
    }
  }

 try {
      commitBeforeMutationEffectsImpl(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }
  fiber = fiber.sibling;
}
}

commitBeforeMutationEffectsImpl:

function commitBeforeMutationEffectsImpl(fiber: Fiber) {
  const current = fiber.alternate;
  const flags = fiber.flags;
// focus相关
  if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
    // Check to see if the focused element was inside of a hidden (Suspense) subtree.
    // TODO: Move this out of the hot path using a dedicated effect tag.
    if (
      fiber.tag === SuspenseComponent &&
      isSuspenseBoundaryBeingHidden(current, fiber) &&
      doesFiberContain(fiber, focusedInstanceHandle)
    ) {
      shouldFireAfterActiveInstanceBlur = true;
      beforeActiveInstanceBlur();
    }
  }
  // 调用getSnapshotBeforeUpdate

  if ((flags & Snapshot) !== NoFlags) {
    setCurrentDebugFiberInDEV(fiber);
    commitBeforeMutationEffectOnFiber(current, fiber);
    resetCurrentDebugFiberInDEV();
  }
 // 调度useEffect
  if ((flags & Passive) !== NoFlags) {
    // If there are passive effects, schedule a callback to flush at
    // the earliest opportunity.
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }
}

整体分为三个部分:

  1. 处理DOM节点渲染/删除后的autoFocus,blur逻辑
  2. 调用getSnapshotBeforeUpdate生命周期钩子
  3. 调度useEffect

调用getSnapshotBeforeUpdate

commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名,
这个方法内部会调用getSnapshotBeforeUpdate:
commitBeforeMutationLifeCycles方法

const snapshot = instance.getSnapshotBeforeUpdate(
    finishedWork.elementType === finishedWork.type
        ? prevProps
        : resolveDefaultProps(finishedWork.type, prevProps),
    prevState,
);

getSnapshotBeforeUpdate在commit阶段内的before mutation阶段调用的,由于commit是同步进行的,所以不会出现多次调用的问题

调度useEffect

scheduleCallback方法就是由Scheduler模块提供,用于某个优先级异步调度一个回调函数

// 调度useEffect
if ((flags & Passive) !== NoFlags) {
    // If there are passive effects, schedule a callback to flush at
    // the earliest opportunity.
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

此处,被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects

如何异步调度

export function flushPassiveEffects(): boolean {
  // Returns whether passive effects were flushed.
  if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
    const priorityLevel =
      pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
        ? NormalSchedulerPriority
        : pendingPassiveEffectsRenderPriority;
    pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
    if (decoupleUpdatePriorityFromScheduler) {
      const previousLanePriority = getCurrentUpdateLanePriority();
      try {
        setCurrentUpdateLanePriority(
          schedulerPriorityToLanePriority(priorityLevel),
        );
        return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
      } finally {
        setCurrentUpdateLanePriority(previousLanePriority);
      }
    } else {
      return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
    }
  }
  return false;
}

通过之前的了解render阶段:Fiber节点更新过程中的副作用包括

  1. 插入DOM节点(Placement)
  2. 删除DOM节点(Deletion)
  3. 更新DOM节点(Update)
    除此之外,当一个FunctionComponent含有useEffect或者useLayoutEffect的时候,他对应的Fiber节点也会被赋值flags

在commitRootImpl方法中会循环遍历rootWithPendingPassiveEffects执行有副作用的回调函数

整个useEffect的异步调用分为三个部分:

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

useEffect: 在浏览器完成布局与绘制之后,传递给useEffect的函数会延迟调用

使用useEffect异步执行的原因主要是为了纺织同步执行时阻塞浏览器的渲染

总结

before mutation阶段会通过commitBeforeMutationEffects去遍历有effect的Fiber节点:

  1. 处理DOM节点渲染/删除后的Focus/onblur逻辑
  2. 调用getSnapshotBeforeUpdate生命周期
  3. 调度useEffect

mutation阶段

与before mutation阶段相似的是mutation阶段也会去遍历effect的Fiber节点,不同的是:mutation阶段调用的是commitBeforeMutationEffects方法

 // The next phase is the mutation phase, where we mutate the host tree.
    commitMutationEffects(finishedWork, root, renderPriorityLevel);

    if (shouldFireAfterActiveInstanceBlur) {
      afterActiveInstanceBlur();
    }
    resetAfterCommit(root.containerInfo);

    // The work-in-progress tree is now the current tree. This must come after
    // the mutation phase, so that the previous tree is still current during
    // componentWillUnmount, but before the layout phase, so that the finished
    // work is current during componentDidMount/Update.
    root.current = finishedWork;

总结

mutation阶段会遍历有effect的Fiber节点,主要方法在commitMutationEffects,根据flags状态的不同调用不同的处理方法去处理Fiber

layout阶段

  • 在当前阶段代码都在DOM渲染完成(mutation阶段)之后执行的
  • 当前阶段触发的生命周期钩子和hook可以直接访问到已经改变之后的DOM,当前阶段可以直接参与DOM layout的阶段