上一章最后一节我们介绍了,commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参。

  1. commitRoot(root);

rootFiber.firstEffect上保存了一条需要执行副作用Fiber节点的单向链表effectList,这些Fiber节点updateQueue中保存了变化的props

这些副作用对应的DOM操作commit阶段执行。

除此之外,一些生命周期钩子(比如componentDidXXX)、hook(比如useEffect)需要在commit阶段执行。

commit阶段的主要工作(即Renderer的工作流程)分为三部分:

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

  • mutation阶段(执行DOM操作)

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

你可以从这里流程概览 - 图1 (opens new window)看到commit阶段的完整代码

before mutation阶段之前和layout阶段之后还有一些额外工作,涉及到比如useEffect的触发、优先级相关的重置、ref的绑定/解绑。

这些对我们当前属于超纲内容,为了内容完整性,在这节简单介绍。

before mutation之前

commitRootImpl方法中直到第一句if (firstEffect !== null)之前属于before mutation之前。

我们大体看下他做的工作,现在你还不需要理解他们:

  1. do {
  2. // 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
  3. flushPassiveEffects();
  4. } while (rootWithPendingPassiveEffects !== null);
  5. // root指 fiberRootNode
  6. // root.finishedWork指当前应用的rootFiber
  7. const finishedWork = root.finishedWork;
  8. // 凡是变量名带lane的都是优先级相关
  9. const lanes = root.finishedLanes;
  10. if (finishedWork === null) {
  11. return null;
  12. }
  13. root.finishedWork = null;
  14. root.finishedLanes = NoLanes;
  15. // 重置Scheduler绑定的回调函数
  16. root.callbackNode = null;
  17. root.callbackId = NoLanes;
  18. let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  19. // 重置优先级相关变量
  20. markRootFinished(root, remainingLanes);
  21. // 清除已完成的discrete updates,例如:用户鼠标点击触发的更新。
  22. if (rootsWithPendingDiscreteUpdates !== null) {
  23. if (
  24. !hasDiscreteLanes(remainingLanes) &&
  25. rootsWithPendingDiscreteUpdates.has(root)
  26. ) {
  27. rootsWithPendingDiscreteUpdates.delete(root);
  28. }
  29. }
  30. // 重置全局变量
  31. if (root === workInProgressRoot) {
  32. workInProgressRoot = null;
  33. workInProgress = null;
  34. workInProgressRootRenderLanes = NoLanes;
  35. } else {
  36. }
  37. // 将effectList赋值给firstEffect
  38. // 由于每个fiber的effectList只包含他的子孙节点
  39. // 所以根节点如果有effectTag则不会被包含进来
  40. // 所以这里将有effectTag的根节点插入到effectList尾部
  41. // 这样才能保证有effect的fiber都在effectList中
  42. let firstEffect;
  43. if (finishedWork.effectTag > PerformedWork) {
  44. if (finishedWork.lastEffect !== null) {
  45. finishedWork.lastEffect.nextEffect = finishedWork;
  46. firstEffect = finishedWork.firstEffect;
  47. } else {
  48. firstEffect = finishedWork;
  49. }
  50. } else {
  51. // 根节点没有effectTag
  52. firstEffect = finishedWork.firstEffect;
  53. }

可以看到,before mutation之前主要做一些变量赋值,状态重置的工作。

这一长串代码我们只需要关注最后赋值的firstEffect,在commit的三个子阶段都会用到他。

layout之后

接下来让我们简单看下layout阶段执行完后的代码,现在你还不需要理解他们:

  1. const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
  2. // useEffect相关
  3. if (rootDoesHavePassiveEffects) {
  4. rootDoesHavePassiveEffects = false;
  5. rootWithPendingPassiveEffects = root;
  6. pendingPassiveEffectsLanes = lanes;
  7. pendingPassiveEffectsRenderPriority = renderPriorityLevel;
  8. } else {}
  9. // 性能优化相关
  10. if (remainingLanes !== NoLanes) {
  11. if (enableSchedulerTracing) {
  12. // ...
  13. }
  14. } else {
  15. // ...
  16. }
  17. // 性能优化相关
  18. if (enableSchedulerTracing) {
  19. if (!rootDidHavePassiveEffects) {
  20. // ...
  21. }
  22. }
  23. // ...检测无限循环的同步任务
  24. if (remainingLanes === SyncLane) {
  25. // ...
  26. }
  27. // 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
  28. ensureRootIsScheduled(root, now());
  29. // ...处理未捕获错误及老版本遗留的边界问题
  30. // 执行同步任务,这样同步任务不需要等到下次事件循环再执行
  31. // 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
  32. // 或useLayoutEffect
  33. flushSyncCallbackQueue();
  34. return null;

你可以在这里流程概览 - 图2 (opens new window)看到这段代码

主要包括三点内容:

  1. useEffect相关的处理。

我们会在讲解layout阶段时讲解。

  1. 性能追踪相关。

源码里有很多和interaction相关的变量。他们都和追踪React渲染时间、性能相关,在Profiler API流程概览 - 图3 (opens new window)DevTools流程概览 - 图4 (opens new window)中使用。

你可以在这里看到interaction的定义流程概览 - 图5 (opens new window)

  1. commit阶段会触发一些生命周期钩子(如 componentDidXXX)和hook(如useLayoutEffectuseEffect)。

在这些回调方法中可能触发新的更新,新的更新会开启新的render-commit流程。考虑如下Demo:

useLayoutEffect Demo

在该Demo中我们点击页面中的数字,状态会先变为0,再在useLayoutEffect回调中变为随机数。但在页面上数字不会变为0,而是直接变为新的随机数。

这是因为useLayoutEffect会在layout阶段同步执行回调。回调中我们触发了状态更新setCount(randomNum),这会重新调度一个同步任务。

该任务会在在如上commitRoot倒数第二行代码处被同步执行。

  1. flushSyncCallbackQueue();

所以我们看不到页面中元素先变为0。

如果换成useEffect多点击几次就能看到区别。

关注公众号,后台回复908获得在线Demo地址