在本节正式开始前,让我们复习下这一章到目前为止所学的。

Renderer工作的阶段被称为commit阶段。commit阶段可以分为三个子阶段:

  • before mutation阶段(执行DOM操作前)

  • mutation阶段(执行DOM操作)

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

本节我们看看before mutation阶段(执行DOM操作前)都做了什么。

概览

before mutation阶段的代码很短,整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。

这部分源码在这里before mutation阶段 - 图1 (opens new window)。为了增加可读性,示例代码中删除了不相关的逻辑

  1. // 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
  2. const previousLanePriority = getCurrentUpdateLanePriority();
  3. setCurrentUpdateLanePriority(SyncLanePriority);
  4. // 将当前上下文标记为CommitContext,作为commit阶段的标志
  5. const prevExecutionContext = executionContext;
  6. executionContext |= CommitContext;
  7. // 处理focus状态
  8. focusedInstanceHandle = prepareForCommit(root.containerInfo);
  9. shouldFireAfterActiveInstanceBlur = false;
  10. // beforeMutation阶段的主函数
  11. commitBeforeMutationEffects(finishedWork);
  12. focusedInstanceHandle = null;

我们重点关注beforeMutation阶段的主函数commitBeforeMutationEffects做了什么。

commitBeforeMutationEffects

大体代码逻辑:

  1. function commitBeforeMutationEffects() {
  2. while (nextEffect !== null) {
  3. const current = nextEffect.alternate;
  4. if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
  5. // ...focus blur相关
  6. }
  7. const effectTag = nextEffect.effectTag;
  8. // 调用getSnapshotBeforeUpdate
  9. if ((effectTag & Snapshot) !== NoEffect) {
  10. commitBeforeMutationEffectOnFiber(current, nextEffect);
  11. }
  12. // 调度useEffect
  13. if ((effectTag & Passive) !== NoEffect) {
  14. if (!rootDoesHavePassiveEffects) {
  15. rootDoesHavePassiveEffects = true;
  16. scheduleCallback(NormalSchedulerPriority, () => {
  17. flushPassiveEffects();
  18. return null;
  19. });
  20. }
  21. }
  22. nextEffect = nextEffect.nextEffect;
  23. }
  24. }

整体可以分为三部分:

  1. 处理DOM节点渲染/删除后的 autoFocusblur 逻辑。

  2. 调用getSnapshotBeforeUpdate生命周期钩子。

  3. 调度useEffect

我们讲解下2、3两点。

调用getSnapshotBeforeUpdate

commitBeforeMutationEffectOnFibercommitBeforeMutationLifeCycles的别名。

在该方法内会调用getSnapshotBeforeUpdate

你可以在这里before mutation阶段 - 图2 (opens new window)看到这段逻辑

Reactv16开始,componentWillXXX钩子前增加了UNSAFE_前缀。

究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。

这种行为和Reactv15不一致,所以标记为UNSAFE_

更详细的解释参照这里before mutation阶段 - 图3 (opens new window)

为此,React提供了替代的生命周期钩子getSnapshotBeforeUpdate

我们可以看见,getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题。

调度useEffect

在这几行代码内,scheduleCallback方法由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。

  1. // 调度useEffect
  2. if ((effectTag & Passive) !== NoEffect) {
  3. if (!rootDoesHavePassiveEffects) {
  4. rootDoesHavePassiveEffects = true;
  5. scheduleCallback(NormalSchedulerPriority, () => {
  6. // 触发useEffect
  7. flushPassiveEffects();
  8. return null;
  9. });
  10. }
  11. }

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

我们接下来讨论useEffect如何被异步调度,以及为什么要异步(而不是同步)调度。

如何异步调度

flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList

关于flushPassiveEffects的具体讲解参照useEffect与useLayoutEffect一节

completeWork一节我们讲到,effectList中保存了需要执行副作用的Fiber节点。其中副作用包括

  • 插入DOM节点(Placement)
  • 更新DOM节点(Update)
  • 删除DOM节点(Deletion)

除此外,当一个FunctionComponent含有useEffectuseLayoutEffect,他对应的Fiber节点也会被赋值effectTag

你可以从这里before mutation阶段 - 图4 (opens new window)看到hook相关的effectTag

flushPassiveEffects方法内部会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。

如果在此时直接执行,rootWithPendingPassiveEffects === null

那么rootWithPendingPassiveEffects会在何时赋值呢?

在上一节layout之后的代码片段中会根据rootDoesHavePassiveEffects === true?决定是否赋值rootWithPendingPassiveEffects

  1. const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
  2. if (rootDoesHavePassiveEffects) {
  3. rootDoesHavePassiveEffects = false;
  4. rootWithPendingPassiveEffects = root;
  5. pendingPassiveEffectsLanes = lanes;
  6. pendingPassiveEffectsRenderPriority = renderPriorityLevel;
  7. }

所以整个useEffect异步调用分为三步:

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

为什么需要异步调用

摘录自React文档effect 的执行时机before mutation阶段 - 图5 (opens new window)

与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

可见,useEffect异步执行的原因主要是防止同步执行时阻塞浏览器渲染。

总结

经过本节学习,我们知道了在before mutation阶段,会遍历effectList,依次执行:

  1. 处理DOM节点渲染/删除后的 autoFocusblur逻辑

  2. 调用getSnapshotBeforeUpdate生命周期钩子

  3. 调度useEffect