1、completeWork主要逻辑

completeWork的工作就是将Fiber节点映射为 DOM 节点;

performUnitOfWork 到 completeWork,中间会经过一个这样的调用链路:
image.png

performUnitOfWork源码:

  1. function performUnitOfWork(unitOfWork) {
  2. ......
  3. // 获取入参节点对应的 current 节点
  4. var current = unitOfWork.alternate;
  5. var next;
  6. if (xxx) {
  7. ...
  8. // 创建当前节点的子节点
  9. next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
  10. ...
  11. } else {
  12. // 创建当前节点的子节点
  13. next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
  14. }
  15. ......
  16. if (next === null) {
  17. // 调用 completeUnitOfWork
  18. completeUnitOfWork(unitOfWork);
  19. } else {
  20. // 将当前节点更新为新创建出的 Fiber 节点
  21. workInProgress = next;
  22. }
  23. ......
  24. }

performUnitOfWork 每次会尝试调用 beginWork 来创建当前节点的子节点,若创建出的子节点为空(也就意味着当前节点不存在子 Fiber 节点),则说明当前节点是一个叶子节点。按照深度优先遍历的原则,当遍历到叶子节点时,“递”阶段就结束了,随之而来的是“归”的过程。因此这种情况下,就会调用 completeUnitOfWork,执行当前节点对应的 completeWork 逻辑。

2、completeWork 的工作原理

completeWork源码:

  1. function completeWork(current, workInProgress, renderLanes) {
  2. // 取出 Fiber 节点的属性值,存储在 newProps 里
  3. var newProps = workInProgress.pendingProps;
  4. // 根据 workInProgress 节点的 tag 属性的不同,决定要进入哪段逻辑
  5. switch (workInProgress.tag) {
  6. case ......:
  7. return null;
  8. case ClassComponent:
  9. {
  10. .....
  11. }
  12. case HostRoot:
  13. {
  14. ......
  15. }
  16. case HostComponent:
  17. {
  18. popHostContext(workInProgress);
  19. var rootContainerInstance = getRootHostContainer();
  20. var type = workInProgress.type;
  21. // 判断 current 节点是否存在,因为目前是挂载阶段,因此 current 节点是不存在的
  22. if (current !== null && workInProgress.stateNode != null) {
  23. updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
  24. if (current.ref !== workInProgress.ref) {
  25. markRef$1(workInProgress);
  26. }
  27. } else {
  28. // 这里首先是针对异常情况进行 return 处理
  29. if (!newProps) {
  30. if (!(workInProgress.stateNode !== null)) {
  31. {
  32. throw Error("We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.");
  33. }
  34. }
  35. return null;
  36. }
  37. // 接下来就为 DOM 节点的创建做准备了
  38. var currentHostContext = getHostContext();
  39. // _wasHydrated 是一个与服务端渲染有关的值,这里不用关注
  40. var _wasHydrated = popHydrationState(workInProgress);
  41. // 判断是否是服务端渲染
  42. if (_wasHydrated) {
  43. // 这里不用关注,请你关注 else 里面的逻辑
  44. if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
  45. markUpdate(workInProgress);
  46. }
  47. } else {
  48. // 这一步很关键, createInstance 的作用是创建 DOM 节点
  49. var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
  50. // appendAllChildren 会尝试把上一步创建好的 DOM 节点挂载到 DOM 树上去
  51. appendAllChildren(instance, workInProgress, false, false);
  52. // stateNode 用于存储当前 Fiber 节点对应的 DOM 节点
  53. workInProgress.stateNode = instance;
  54. // finalizeInitialChildren 用来为 DOM 节点设置属性
  55. if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
  56. markUpdate(workInProgress);
  57. }
  58. }
  59. ......
  60. }
  61. return null;
  62. }
  63. case HostText:
  64. {
  65. ......
  66. }
  67. case SuspenseComponent:
  68. {
  69. ......
  70. }
  71. case HostPortal:
  72. ......
  73. return null;
  74. case ContextProvider:
  75. ......
  76. return null;
  77. ......
  78. }
  79. {
  80. {
  81. throw Error("Unknown unit of work tag (" + workInProgress.tag + "). This error is likely caused by a bug in React. Please file an issue.");
  82. }
  83. }
  84. }
  1. completeWork 的核心逻辑是一段体量巨大的 switch 语句,在这段 switch 语句中,completeWork 将根据 workInProgress 节点的 tag 属性的不同,进入不同的 DOM 节点的创建、处理逻辑。
  2. 本次源码的重点是看节点的h1的tag属性对应的类型应该是 HostComponent,也就是“原生 DOM 元素类型”。
  3. completeWork 中的 current、 workInProgress 分别对应的是下图中左右两棵 Fiber 树上的节点:

image.png
其中 workInProgress 树代表的是“当前正在 render 中的树”,而 current 树则代表“已经存在的树”。

workInProgress 节点和 current 节点之间用 alternate 属性相互连接。在组件的挂载阶段,current 树只有一个 rootFiber 节点,并没有其他内容。因此 h1 这个 workInProgress 节点对应的 current 节点是 null。

completeWork的主要工作:

  1. 负责处理 Fiber 节点到 DOM 节点的映射逻辑。
  2. completeWork 内部有 3 个关键动作:
    • 创建DOM 节点(CreateInstance)
    • 将 DOM 节点插入到 DOM 树中(AppendAllChildren)
    • 为 DOM 节点设置属性(FinalizeInitialChildren)
  3. 创建好的 DOM 节点会被赋值给 workInProgress 节点的 stateNode 属性
  4. 将 DOM 节点插入到 DOM 树的操作是通过 appendAllChildren 函数来完成的,实际上是将子 Fiber 节点所对应的 DOM 节点挂载到其父 Fiber 节点所对应的 DOM 节点里去。

3、completeUnitOfWork工作原理

completeUnitOfWork开启收集 EffectList 的“大循环”;

  1. 针对传入的当前节点,调用 completeWork;
  2. 将当前节点的副作用链(EffectList)插入到其父节点对应的副作用链(EffectList)中;
  3. 以当前节点为起点,循环遍历其兄弟节点及其父节点。当遍历到兄弟节点时,将 return 掉当前调用,触发兄弟节点对应的 performUnitOfWork 逻辑;而遍历到父节点时,则会直接进入下一轮循环,也就是重复 1、2 的逻辑。

理解副作用链之前,首先要理解 completeUnitOfWork 开启下一轮循环的原则,也就是步骤 3。步骤 3 相关的源码如下所示:

  1. do {
  2. ......
  3. // 这里省略步骤 1 和步骤 2 的逻辑
  4. // 获取当前节点的兄弟节点
  5. var siblingFiber = completedWork.sibling;
  6. // 若兄弟节点存在
  7. if (siblingFiber !== null) {
  8. // 将 workInProgress 赋值为当前节点的兄弟节点
  9. workInProgress = siblingFiber;
  10. // 将正在进行的 completeUnitOfWork 逻辑 return 掉
  11. return;
  12. }
  13. // 若兄弟节点不存在,completeWork 会被赋值为 returnFiber,也就是当前节点的父节点
  14. completedWork = returnFiber;
  15. // 这一步与上一步是相辅相成的,上下文中要求 workInProgress 与 completedWork 保持一致
  16. workInProgress = completedWork;
  17. } while (completedWork !== null);

步骤 3 是整个循环体的收尾工作,它会在当前节点相关的各种工作都做完之后执行。

当前的 Fiber 节点之所以会进入 completeWork,是因为“递无可递”了,才会进入“归”的逻辑,这就意味着当前 Fiber 要么没有 child 节点、要么 child 节点的 completeWork 早就执行过了。因此 child 节点不会是下次循环需要考虑的对象,下次循环只需要考虑兄弟节点(siblingFiber)和父节点(returnFiber)。

4、render阶段的做工作目标

render 阶段的工作目标是找出界面中需要处理的更新

更新阶段与挂载阶段的主要区别在于更新阶段的 current 树不为空;
image.png

假如说我的某一次操作,仅仅对 p 节点产生了影响,那么对于渲染器来说,它理应只关注 p 节点这一处的更新。这时候问题就来了:怎样做才能让渲染器又快又好地定位到那些真正需要更新的节点呢?

答案是:副作用链effectList

副作用链(effectList) 可以理解为 render 阶段“工作成果”的一个集合:每个 Fiber 节点都维护着一个属于它自己的 effectList,effectList 在数据结构上以链表的形式存在,链表内的每一个元素都是一个 Fiber 节点。这些 Fiber 节点需要满足两个共性:

  1. 都是当前 Fiber 节点的后代节点
  2. 都有待处理的副作用

Fiber 节点的 effectList 里记录的并非它自身的更新,而是其需要更新的后代节点

“completeWork 是自底向上执行的”,子节点的 completeWork 总是比父节点先执行。

把所有需要更新的 Fiber 节点单独串成一串链表,方便后续有针对性地对它们进行更新,这就是所谓的“收集副作用”的过程

  1. nextEffect nextEffect
  2. rootFiber.firstEffect -----------> fiber -----------> fiber

总结一下effectList 的创建过程:

  1. App FiberNode 的 flags 属性为 3,大于 PerformedWork,因此会进入 effectList 的创建逻辑;
  2. 创建 effectList 时,并不是为当前 Fiber 节点创建,而是为它的父节点创建,App 节点的父节点是 rootFiber,rootFiber 的 effectList 此时为空;
  3. rootFiber 的 firstEffect 和 lastEffect 指针都会指向 App 节点,App 节点由此成为 effectList 中的唯一一个 FiberNode,如下图所示。

image.png