关于javascript:React源码-commit阶段详解

点击进入React源码调试仓库。

当render阶段实现后,意味着在内存中构建的workInProgress树所有更新工作曾经实现,这包含树中fiber节点的更新、diff、effectTag的标记、effectList的收集。此时workInProgress树的残缺状态如下:

和current树相比,它们的构造上诚然存在区别,变动的fiber节点也存在于workInProgress树,但要将这些节点利用到DOM上却不会循环整棵树,而是通过循环effectList这个链表来实现,这样保障了只针对有变动的节点做工作。

所以循环effectList链表去将有更新的fiber节点利用到页面上是commit阶段的次要工作。

commit阶段的入口是commitRoot函数,它会告知scheduler以立刻执行的优先级去调度commit阶段的工作。

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}

scheduler去调度的是commitRootImpl,它是commit阶段的外围实现,整个commit阶段被划分成三个局部。

commit流程概览

commit阶段次要是针对root上收集的effectList进行解决。在真正的工作开始之前,有一个筹备阶段,次要是变量的赋值,以及将root的effect退出到effectList中。随后开始针对effectList分三个阶段进行工作:

  • before mutation:读取组件变更前的状态,针对类组件,调用getSnapshotBeforeUpdate,让咱们能够在DOM变更前获取组件实例的信息;针对函数组件,异步调度useEffect。
  • mutation:针对HostComponent,进行相应的DOM操作;针对类组件,调用componentWillUnmount;针对函数组件,执行useLayoutEffect的销毁函数。
  • layout:在DOM操作实现后,读取组件的状态,针对类组件,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对函数组件填充useEffect 的 effect执行数组,并调度useEffect

before mutation和layout针对函数组件的useEffect调度是互斥的,只能发动一次调度

workInProgress 树切换到current树的机会是在mutation完结后,layout开始前。这样做的起因是在mutation阶段调用类组件的componentWillUnmount的时候,
还能够获取到卸载前的组件信息;在layout阶段调用componentDidMount/Update时,获取的组件信息更新后的。

function commitRootImpl(root, renderPriorityLevel) {

  // 进入commit阶段,先执行一次之前未执行的useEffect
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  // 筹备阶段-----------------------------------------------

  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  if (finishedWork === null) {
    return null;
  }
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  root.callbackNode = null;
  root.callbackId = NoLanes;

  // effectList的整顿,将root上的effect连到effectList的开端
  let firstEffect;
  if (finishedWork.effectTag > PerformedWork) {
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // There is no effect on the root.
    firstEffect = finishedWork.firstEffect;
  }

  // 筹备阶段完结,开始解决effectList
  if (firstEffect !== null) {

    ...

    // before mutation阶段--------------------------------
    nextEffect = firstEffect;
    do {...} while (nextEffect !== null);

    ...

    // mutation阶段---------------------------------------
    nextEffect = firstEffect;
    do {...} while (nextEffect !== null);

    // 将wprkInProgress树切换为current树
    root.current = finishedWork;

    // layout阶段-----------------------------------------
    nextEffect = firstEffect;
    do {...} while (nextEffect !== null);

    nextEffect = null;

    // 告诉浏览器去绘制
    requestPaint();

  } else {
    // 没有effectList,间接将wprkInProgress树切换为current树
    root.current = finishedWork;

  }

  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

  // 获取尚未解决的优先级,比方之前被跳过的工作的优先级
  remainingLanes = root.pendingLanes;
  // 将被跳过的优先级放到root上的pendingLanes(待处理的优先级)上
  markRootFinished(root, remainingLanes);

  /*
  * 每次commit阶段实现后,再执行一遍ensureRootIsScheduled,确保是否还有工作须要被调度。
  * 例如,高优先级插队的更新实现后,commit实现后,还会再执行一遍,保障之前跳过的低优先级工作
  * 从新调度
  *
  * */
  ensureRootIsScheduled(root, now());

  ...

  return null;
}

上面的局部,是对这三个阶段别离进行的具体解说。

before Mutation

beforeMutation阶段的入口函数是commitBeforeMutationEffects

nextEffect = firstEffect;
do {
  try {
    commitBeforeMutationEffects();
  } catch (error) {
    ...
  }
} while (nextEffect !== null);

它的作用次要是调用类组件的getSnapshotBeforeUpdate,针对函数组件,异步调度useEffect。

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const current = nextEffect.alternate;

    ...

    const flags = nextEffect.flags;
    if ((flags & Snapshot) !== NoFlags) {
      // 通过commitBeforeMutationEffectOnFiber调用getSnapshotBeforeUpdate
      commitBeforeMutationEffectOnFiber(current, nextEffect);
    }

    if ((flags & Passive) !== NoFlags) {
      // 异步调度useEffect
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        scheduleCallback(NormalSchedulerPriority, () => {
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

commitBeforeMutationEffectOnFiber代码如下

function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber,
): void {
  switch (finishedWork.tag) {
    ...
    case ClassComponent: {
      if (finishedWork.flags & Snapshot) {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // 调用getSnapshotBeforeUpdate
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          // 将返回值存储在外部属性上,不便componentDidUpdate获取
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      return;
    }
    ...
  }

}

mutation

mutation阶段会真正操作DOM节点,波及到的操作有增、删、改。入口函数是commitMutationEffects

    nextEffect = firstEffect;
    do {
      try {
        commitMutationEffects(root, renderPriorityLevel);
      } catch (error) {
        ...
        nextEffect = nextEffect.nextEffect;
      }
    } while (nextEffect !== null);

因为过程较为简单,所以我写了三篇文章来阐明这三种DOM操作,如果想要理解细节,能够看一下。文章写于17还未正式公布的时候,所以外面的源码版本取自17.0.0-alpha0。

React和DOM的那些事-节点新增算法

React和DOM的那些事-节点删除算法

React和DOM的那些事-节点更新

layout阶段

layout阶段的入口函数是commitLayoutEffects

nextEffect = firstEffect;
do {
  try {
    commitLayoutEffects(root, lanes);
  } catch (error) {
    ...
    nextEffect = nextEffect.nextEffect;
  }
} while (nextEffect !== null);

咱们只关注classComponent和functionComponent。针对前者,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对后者,填充useEffect 的 effect执行数组,并调度useEffect(具体的原理在我的这篇文章:梳理useEffect和useLayoutEffect的原理与区别中有解说)。

function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      // 执行useLayoutEffect的创立
      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

      // 填充useEffect的effect执行数组
      schedulePassiveEffects(finishedWork);
      return;
    }
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (finishedWork.flags & Update) {
        if (current === null) {
          // 如果是初始挂载阶段,调用componentDidMount
          instance.componentDidMount();
        } else {
          // 如果是更新阶段,调用componentDidUpdate
          const prevProps =
            finishedWork.elementType === finishedWork.type
              ? current.memoizedProps
              : resolveDefaultProps(finishedWork.type, current.memoizedProps);
          const prevState = current.memoizedState;

          instance.componentDidUpdate(
            prevProps,
            prevState,
            // 将getSnapshotBeforeUpdate的后果传入
            instance.__reactInternalSnapshotBeforeUpdate,
          );
        }
      }

      // 调用setState的回调
      const updateQueue: UpdateQueue<
        *,
      > | null = (finishedWork.updateQueue: any);
      if (updateQueue !== null) {

        commitUpdateQueue(finishedWork, updateQueue, instance);
      }
      return;
    }

    ...

  }
}

总结

commit阶段将effectList的解决分成三个阶段保障了不同生命周期函数的适时调用。绝对于同步执行的useEffectLayout,useEffect的异步调度提供了一种不阻塞页面渲染的副作用操作入口。另外,标记root上还未解决的优先级和调用ensureRootIsScheduled使得被跳过的低优先级工作得以再次被调度。commit阶段的实现,也就意味着本次更新曾经完结。

欢送扫码关注公众号,发现更多技术文章

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理