React18.2源码解析之commit阶段(muation&layout)

370 阅读4分钟

记录学习过程,如有错误欢迎指出

开始之前(必看)

React version 18.2.0

DEV代码可以忽略,下面正文我使用省略号就代表是dev相关的代码,包含hydrate字样的也请忽略,那是服务端渲染相关,望周知

我使用深度优先(🐶)的方式讲解:即遇到函数先进入函数,执行完函数后,又退回之前的函数.而不是在一个函数中讲完了再讲函数中执行的函数(希望能听懂我在说什么^v^)

因为使用了深度优先的方式讲解,耦合比较重,不建议跳着看

commitRootImpl(未完)

...
     /**
     * mutation阶段
     */

    //  mutation 阶段,可以在这个阶段 改变 host tree
    //  mutation 阶段并`不会`使用向下查找,向上归并的方式执行,而是直接执行commitMutationEffectsOnFiber函数
    commitMutationEffects(root, finishedWork, lanes);

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

commitMutationEffects(mutation阶段)

export function commitMutationEffects(
  root: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
) {
  // 优先级相关
  inProgressLanes = committedLanes;
  inProgressRoot = root;

  setCurrentDebugFiberInDEV(finishedWork);
  // 进入执行
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
  setCurrentDebugFiberInDEV(finishedWork);

  // 重置
  inProgressLanes = null;
  inProgressRoot = null;
}

commitMutationEffectsOnFiber

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  // The effect flag should be checked *after* we refine the type of fiber,
  // because the fiber tag is more specific. An exception is any flag related
  // to reconcilation, because those can be set on all fiber types.

  // 根据tag判断
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    
    
    //其余tag判断
    ....
    

commitRootImpl(未完)

...

     /**
     * 重点:
     *  root.current = finishedWork;
     *  交换页面指向的树
     */

    /*
      当 workInProgressFiber 树完成了渲染,就会将 current 指针
      从 current Fiber 树指向 workInProgress Fiber 树,也就是这行代码所做的工作
    */
    // 交换 workInProgress

    root.current = finishedWork;

    /**
     * 为什么要在 mutation 阶段结束后,layout 阶段之前执行呢?
     * 
     * 这是因为componentWillUnmount这个构造,绘制mutation时执行,可能会操作原来Fiber上的内容
     * **为了保证数据的可靠性不会修改current指向**
     * 
     * 而layout阶段会执行componentDidMount 和 componentDidUpdate钩子,此时需要获取到的 DOM 是更新后的
     */



     /**
     * layout阶段
     */

    // 执行 layout
    commitLayoutEffects(finishedWork, root, lanes);

    // 告诉Scheduler在该帧的末尾让步,这样浏览器就有绘画的机会
    requestPaint();

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

    // 将优先级重置为之前的 非同步优先级,表示effect执行完毕了
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  // 没有effect的情况
  } else {
    // 没有effect还是会将current指向workInProgress

    // 因为没有effect,所以也不会发生什么钩子的触发,所以这里改变current指向即可
    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();
    }
  }
...

commitLayoutEffects

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

  const current = finishedWork.alternate;
  //执行
  commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

commitLayoutEffectOnFiber

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {

  // 在更新这个函数时,也要更新reappearLayoutEffects,它的作用是
  // 把不在屏幕上显示的那颗树 从隐藏 => 可见时

  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:

commitRootImpl

....
  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;


  // 在前面rootDoesHavePassiveEffects被置为true,这里成立
  if (rootDoesHavePassiveEffects) {
    // This commit has passive effects. Stash a reference to them. But don't
    // schedule a callback until after flushing layout work.
    // 又置为false
    rootDoesHavePassiveEffects = false;
    //赋值rootWithPendingPassiveEffects,那么下次执行该函数时在该函数开头的do while就可以执行了
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsLanes = lanes;
  } else {
    // 没有任何被动的影响,所以我们可以立即释放这个缓存的池
    releaseRootPooledCache(root, remainingLanes);
  }



  // 再次读取一下pendingLanes,防止有effect在中途出现了
  remainingLanes = root.pendingLanes;
  
  if (remainingLanes === NoLanes) {
    // 如果没有剩余的工作,我们可以清除已经失败的一组错误的边界
    legacyErrorBoundariesThatAlreadyFailed = null;
  }

  onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel);


  // commit 阶段结尾,可能会在 commit 阶段产生新的更新,因此在 commit 阶段的结尾会重新调度一次
  ensureRootIsScheduled(root, now());

  // 判断recoverableErrors是否存在,如果存在说明出现了错误
  if (recoverableErrors !== null) {
    // 在这次渲染过程中出现了一些错误,但从这些错误中恢复过来,不需要把它浮现在用户界面上。我们在这里记录它们
    const onRecoverableError = root.onRecoverableError;
    for (let i = 0; i < recoverableErrors.length; i++) {
      const recoverableError = recoverableErrors[i];
      const componentStack = recoverableError.stack;
      const digest = recoverableError.digest;
      onRecoverableError(recoverableError.value, {componentStack, digest});
    }
  }

  if (hasUncaughtError) {
    hasUncaughtError = false;
    const error = firstUncaughtError;
    firstUncaughtError = null;
    throw error;
  }

  // 如果被动效果是离散渲染的结果,就在当前任务结束时同步冲刷它们,这样就可以立即观察到结果。
  // 否则(不是离散渲染),我们假设它们不受顺序(异步)的影响,不需要被外部系统观察到,所以我们可以等到绘制之后再进行
  if (
    includesSomeLane(pendingPassiveEffectsLanes, SyncLane) &&
    root.tag !== LegacyRoot
  ) {
    flushPassiveEffects();
  }

  // 再读一下这个,因为一个被动效果可能已经更新了
  remainingLanes = root.pendingLanes;
  if (includesSomeLane(remainingLanes, (SyncLane: Lane))) {
    if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
      markNestedUpdateScheduled();
    }


    if (root === rootWithNestedUpdates) {
      nestedUpdateCount++;
    } else {
      nestedUpdateCount = 0;
      rootWithNestedUpdates = root;
    }
  } else {
    nestedUpdateCount = 0;
  }

  // react 中会将同步任务放在 flushSync 队列中,执行这个函数会执行它里面的同步任务
  // 默认 react 中开启的是 legacy 模式,这种模式下的更新都是 同步的 更新,
  // 未来会开启 concurrent 模式(并发模式),会出现不同优先级的更新
  flushSyncCallbacks();

  return null;
}

总结

我们现在知道了commite时内部会执行三个子节点,并且都会对不同的节点进行相应处理,因为react源码内有很多都涉及到这种逻辑,所以我单独写一篇文章来讲解

记录学习过程,如有错误欢迎指出