1、render阶段

performSyncWorkOnRoot 标志着 render 阶段的开始,finishSyncRender 标志着 render 阶段的结束。这中间包含了大量的 beginWork、completeWork 调用栈,正是 render 的工作内容。

beginWork、completeWork 这两个方法需要注意,它们串联起的是一个“模拟递归”的过程。

在 ReactDOM.render 触发的同步模式下,它是一个深度优先搜索的过程。

  • beginWork 将创建新的 Fiber 节点;
  • completeWork 则负责将 Fiber 节点映射为 DOM 节点;

2、workInProgress 节点的创建

performSyncWorkOnRoot 是 render 阶段的起点,这个函数调用了 renderRootSync。我们重点关注里面的createWorkInProgress方法。

  1. // 这里入参中的 current 传入的是现有树结构中的 rootFiber 对象
  2. function createWorkInProgress(current, pendingProps) {
  3. var workInProgress = current.alternate;
  4. // ReactDOM.render 触发的首屏渲染将进入这个逻辑
  5. if (workInProgress === null) {
  6. // 这是需要你关注的第一个点,workInProgress 是 createFiber 方法的返回值
  7. workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
  8. workInProgress.elementType = current.elementType;
  9. workInProgress.type = current.type;
  10. workInProgress.stateNode = current.stateNode;
  11. // 这是需要你关注的第二个点,workInProgress 的 alternate 将指向 current
  12. workInProgress.alternate = current;
  13. // 这是需要你关注的第三个点,current 的 alternate 将反过来指向 workInProgress
  14. current.alternate = workInProgress;
  15. } else {
  16. // else 的逻辑此处先不用关注
  17. }
  18. // 以下省略大量 workInProgress 对象的属性处理逻辑
  19. // 返回 workInProgress 节点
  20. return workInProgress;
  21. }

以上代码做了三件事:

  • createWorkInProgress 将调用 createFiber,workInProgress是 createFiber 方法的返回值;
  • workInProgress 的 alternate 将指向 current;
  • current 的 alternate 将反过来指向 workInProgress。

workInProgress 的本体到底是什么样的,也就是createFiber 到底会返回什么。

  1. var createFiber = function (tag, pendingProps, key, mode) {
  2. return new FiberNode(tag, pendingProps, key, mode);
  3. };

createFiber 将创建一个 FiberNode 实例。因此 workInProgress 就是一个 Fiber 节点。

  1. workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);

workInProgress 节点其实就是 current 节点(即 rootFiber)的副本。

再结合 current 指向 rootFiber 对象(同样是 FiberNode 实例),以及 current 和 workInProgress 通过 alternate 互相连接这些信息,我们可以分析出这波操作执行完之后,整棵树的结构应该如下图所示:
image.png

完成了这个任务之后,就会进入 workLoopSync 的逻辑。workLoopSync 做的事情就是通过 while 循环反复判断 workInProgress 是否为空,并在不为空的情况下针对它执行 performUnitOfWork 函数。

  1. function workLoopSync() {
  2. // 若 workInProgress 不为空
  3. while (workInProgress !== null) {
  4. // 针对它执行 performUnitOfWork 方法
  5. performUnitOfWork(workInProgress);
  6. }
  7. }

而 performUnitOfWork 函数将触发对 beginWork 的调用,进而实现对新 Fiber 节点的创建。若 beginWork 所创建的 Fiber 节点不为空,则 performUniOfWork 会用这个新的 Fiber 节点来更新 workInProgress 的值,为下一次循环做准备。

在这个过程中,每一个被创建出来的新 Fiber 节点,都会一个一个挂载为最初那个 workInProgress 节点的后代节点。而上述过程中构建出的这棵 Fiber 树,也正是大名鼎鼎的 workInProgress 树。 current 指针所指向的根节点所在的那棵树,叫“current 树”。

image.png

3、beginWork 开启 Fiber 节点创建过程

beginWork关键代码如下:

  1. function beginWork(current, workInProgress, renderLanes) {
  2. ......
  3. // current 节点不为空的情况下,会加一道辨识,看看是否有更新逻辑要处理
  4. //
  5. if (current !== null) {
  6. // 获取新旧 props
  7. var oldProps = current.memoizedProps;
  8. var newProps = workInProgress.pendingProps;
  9. // 若 props 更新或者上下文改变,则认为需要"接受更新"
  10. if (oldProps !== newProps || hasContextChanged() || (
  11. workInProgress.type !== current.type )) {
  12. // 打个props更新标
  13. didReceiveUpdate = true;
  14. } else if (xxx) {
  15. // 不需要更新的情况 A
  16. return A
  17. } else {
  18. if (需要更新的情况 B) {
  19. didReceiveUpdate = true;
  20. } else {
  21. // 不需要更新的其他情况,这里我们的首次渲染就将执行到这一行的逻辑
  22. didReceiveUpdate = false;
  23. }
  24. }
  25. } else {
  26. didReceiveUpdate = false;
  27. }
  28. ......
  29. // 这坨 switch 是 beginWork 中的核心逻辑,原有的代码量相当大
  30. switch (workInProgress.tag) {
  31. ......
  32. // 这里省略掉大量形如"case: xxx"的逻辑
  33. // 根节点将进入这个逻辑
  34. case HostRoot:
  35. return updateHostRoot(current, workInProgress, renderLanes)
  36. // dom 标签对应的节点将进入这个逻辑
  37. case HostComponent:
  38. return updateHostComponent(current, workInProgress, renderLanes)
  39. // 文本节点将进入这个逻辑
  40. case HostText:
  41. return updateHostText(current, workInProgress)
  42. ......
  43. // 这里省略掉大量形如"case: xxx"的逻辑
  44. }
  45. // 这里是错误兜底,处理 switch 匹配不上的情况
  46. {
  47. {
  48. throw Error(
  49. "Unknown unit of work tag (" +
  50. workInProgress.tag +
  51. "). This error is likely caused by a bug in React. Please file an issue."
  52. )
  53. }
  54. }
  55. }
  1. 上半部分if (current !== null) {}里面的逻辑是做update操作
  2. 下半部分 switch (workInProgress.tag) {}逻辑是做mount操作

上面代码做了这两件事:

  1. beginWork 的入参是一对用 alternate 连接起来的 workInProgress 和 current 节点;
  2. beginWork 的核心逻辑是根据 fiber 节点(workInProgress)的 tag 属性的不同,调用不同的节点创建函数。

(FunctionComponent/ClassComponent/HostComponent)通过调用 reconcileChildren 方法,生成当前节点的子节点。

  • 对于mount的组件,他会创建新的子Fiber节点
  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点

注意:current为null处理的是mount,否则处理的是update;

  1. function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  2. // 判断 current 是否为 null
  3. // mount
  4. if (current === null) {
  5. // 若 current 为 null,则进入 mountChildFibers 的逻辑
  6. workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  7. } else {
  8. // update
  9. // 若 current 不为 null,则进入 reconcileChildFibers 的逻辑
  10. workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  11. }
  12. }

reconcileChildFibers 和 mountChildFibers都是 ChildReconciler 这个函数的返回值,仅仅存在入参上的区别。

  1. // update
  2. var reconcileChildFibers = ChildReconciler(true);
  3. // mount
  4. var mountChildFibers = ChildReconciler(false);

ChildReconciler源码:

  1. function ChildReconciler(shouldTrackSideEffects) {
  2. // 删除节点的逻辑
  3. function deleteChild(returnFiber, childToDelete) {
  4. if (!shouldTrackSideEffects) {
  5. // Noop.
  6. return;
  7. }
  8. // 以下执行删除逻辑
  9. }
  10. ......
  11. // 单个节点的插入逻辑
  12. function placeSingleChild(newFiber) {
  13. if (shouldTrackSideEffects && newFiber.alternate === null) {
  14. newFiber.flags = Placement;
  15. }
  16. return newFiber;
  17. }
  18. // 插入节点的逻辑
  19. function placeChild(newFiber, lastPlacedIndex, newIndex) {
  20. newFiber.index = newIndex;
  21. if (!shouldTrackSideEffects) {
  22. // Noop.
  23. return lastPlacedIndex;
  24. }
  25. // 以下执行插入逻辑
  26. }
  27. ......
  28. // 此处省略一系列 updateXXX 的函数,它们用于处理 Fiber 节点的更新
  29. // 处理不止一个子节点的情况
  30. function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
  31. ......
  32. }
  33. // 此处省略一堆 reconcileXXXXX 形式的函数,它们负责处理具体的 reconcile 逻辑
  34. function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
  35. // 这是一个逻辑分发器,它读取入参后,会经过一系列的条件判断,调用上方所定义的负责具体节点操作的函数
  36. }
  37. // 将总的 reconcileChildFibers 函数返回
  38. return reconcileChildFibers;
  39. }

reconcileChildren流程主要总结为以下三点:

  1. 关键的入参 shouldTrackSideEffects,意为“是否需要追踪副作用”,因此 reconcileChildFibers 和 mountChildFibers 的不同,在于对副作用的处理不同;
  2. ChildReconciler 中定义了大量如 placeXXX、deleteXXX、updateXXX、reconcileXXX 等这样的函数,这些函数覆盖了对 Fiber 节点的创建、增加、删除、修改等动作,将直接或间接地被 reconcileChildFibers 所调用;
  3. ChildReconciler 的返回值是一个名为 reconcileChildFibers 的函数,这个函数是一个逻辑分发器,它将根据入参的不同,执行不同的 Fiber 节点操作,最终返回不同的目标 Fiber 节点。

针对单个节点插入展开说一下:

  1. function placeSingleChild(newFiber) {
  2. if (shouldTrackSideEffects && newFiber.alternate === null) {
  3. newFiber.flags = Placement;
  4. }
  5. return newFiber;
  6. }

可以看出,一旦判断 shouldTrackSideEffects 为 false,那么下面所有的逻辑都不执行了,其实就是给 Fiber 节点打上一个叫“flags”的标记;

beginWork整体逻辑链路:
image.png

4、flags 是什么

React17的源码已经更改为flags,之前版本叫effectTag;

Placement 这个 effectTag 的意义,是在渲染器执行时,也就是真实 DOM 渲染时,告诉渲染器:我这里需要新增 DOM 节点。 effectTag 记录的是副作用的类型,而所谓“副作用”,React 给出的定义是“数据获取、订阅或者修改 DOM”等动作。

Placement 副作用标识,以二进制常量的形式存在;
image.png

5、Fiber构建

节点的创建只不过是对 performUnitOfWork、 beginWork 和 ChildReconciler 等相关逻辑的重复。

不同的 Fiber 节点之间,将通过 child、return、sibling 这 3 个属性建立关系,其中 child、return 记录的是父子节点关系,而 sibling 记录的则是兄弟节点关系。

我们创建如下实例:

  1. function App() {
  2. return (
  3. <div className="App">
  4. <div className="container">
  5. <h1>我是标题</h1>
  6. <p>我是第一段话</p>
  7. <p>我是第二段话</p>
  8. </div>
  9. </div>
  10. );
  11. }

FiberNode 实例中,return 指向的是当前 Fiber 节点的父节点,而 sibling 指向的是当前节点的第 1 个兄弟节点。我们将fiber树建立如下展示形式:
image.png

以上便是 workInProgress Fiber 树的最终形态了,虽然我们习惯上仍然将眼前的这个产物称为“Fiber 树”,但它的数据结构本质其实已经从树变成了链表。