Skip to content
目录

render阶段

Reconciler 工作阶段称为 render 阶段,本身是一个"DFS递归“的过程

主要分为两个子阶段

  • beginWork

    beginWork过程又称为render阶段的"递"过程

    主要任务是:

    • 挂载fiberNodewip.child上,在某些情况下也会为fiberNode标记flag
  • completeWork

    completeWork过程又称为render阶段的"归"过程

    主要任务是:

    • 创建或标记元素更新

    • flags冒泡

beginWork

beginWork阶段会针对不同的wip.tag进入不同组件的处理逻辑,比较重要的一点是如果此时是Update行为,会进行是否可以复用的优化判断,如果没有命中优化策略,则会进入reconcileChildren函数调用

reconcileChildren函数首先会判断本次操作时Mount行为还是Update行为(非常容易!你只需要判断current === null),针对不同的行为,我们可以进行一些优化

首先介绍一个工厂函数:

createChildFibers( shouldTrackSideEffects: boolean ): ( ...props ) => Fiber

shouldTrackSideEffects含义为 是否追踪副作用/是否标记flags

mountChildFiber = createChildFibers(false)

reconcileChildFiber = createChildFibers(true)

  • Mount行为

    wip.child = mountChildFibers()

  • Update行为

    wip.child = reconcileChildFibers()

Mount时不追踪副作用是一个优化策略,这一点在completeWork之后解释

Question-1:

Mount是不追踪副作用的理由是什么?优化策略如何体现?

reconcileChildFibers方法标记了fiberNode的"插入、删除、移动"行为(flags)

至此,beginWork阶段创建了wip的子fiberNode并将他们连接成fiberTree

completeWork

我们先解释一下flags冒泡,在reconcile阶段我们可能通过reconcileChildFibers方法为fiberNode进行了一些flag标记,这些flags行为会在Renderer的工作阶段——commit阶段进行兑现

我们希望的是Renderer能够快速的、准确的获取到需要跟踪flagsfiberNode,这一点可以通过flags冒泡来实现

ts
let subtreeFlags = NoFlags
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
completedWork.subtreFlags |= subtreeFlags;
1
2
3
4

20230824-142831

  • Mount行为

    • createInstance方法创建fiberNode对应的DOM元素

    • appendAllChildren将下一层DOM元素插入"createInstance"方法创建的DOM元素中

      具体逻辑为:

      1. 从当前fiberNode向下遍历,将遍历到的第一层DOM元素(HostComponent、HostText)类型通过appendChild方法插入parent末尾
      2. 对兄弟fiberNode执行步骤1
      3. 如果没有兄弟fiberNode,则对父fiberNode执行步骤1
      4. 遍历流程回到最初执行步骤1所在层或者parent所在层时终止
    • finalizeIntailChildren设置DOM元素属性

过程不难理解,但我们还是走一遍全流程

fiberNode.stateNode属性指向其对应的真实DOM元素

html
<!-- dom结构大概是这样 -->
<App>
  <div>
    "Hello"
    <span></span>
  </div>
</App>
1
2
3
4
5
6
7

1.1

mount 阶段执行流程如下:

  1. HostRootFiber beginWork

    创建 App fiberNode

  2. App fiberNode beginWork

    创建 div fiberNode

  3. div fiberNode beginWork

    创建"Hello" fiberNode、span fiberNode

  4. "Hello" fiberNode beginWork

    叶子元素

  5. "Hello" fiberNode completeWork

    向下遍历无fiberNode

    tsx
    fiberNode.stateNode = instance = "Hello"
    
    1
  6. span fiberNode beginWork

    叶子元素

  7. span fiberNode completeWork

    tsx
    fiberNode.stateNode = instance = <span></span>
    
    1
  8. div fiberNode completeWork

    向下遍历到span fiberNode"Hello" fiberNode,调用appendInitialChild(instance, wip)方法插入div fiberNode通过createInstance创建的instance

    tsx
    appendInitialChild(instance, workInProgress.child.stateNode, ...props);
    /**
    实际上就是instance.appendChild(workInProgress.child.stateNode)
    */
    fiberNode.stateNode = instance = <div>"Hello"<span></span></div>
    
    1
    2
    3
    4
    5
  9. App fiberNode completeWork

    与8同理,但由于App组件并无其他内容

    tsx
    fiberNode.stateNode = instance = <div>"Hello"<span></span></div>
    
    1
  10. HostRootFiber completeWork

Answer-1:

观察appendAllChildren行为,你会发现在completeWork阶段每一个fiberNode上都存在了一个stateNode属性,他是包含了children fiberNode节点stateNode的一个真实DOM元素(也就是一个离屏DOM)

事实上在mount时构建wip fiberTree时并非所有fiberNode都没有alternate属性,比如HostRootFiber就存在alternate属性的(HostRootFiber.current !== null)

也就是说对于HostRootFiber而言他将会执行的时reconcileChildFiber(),它本身是被标记了flags的

HostRootFiber在经过completeWork阶段后,获得了一颗完整的离屏DOM Tree(HostRootFiber.stateNode),最终一次插入DOM中,更节省性能。如果为每一个mount的fiberNode都标记flags,那么就会进行多次appendChild或insertBefore操作,性能开销更大

  • Update行为

    Update行为的逻辑主要在函数diffProperties()中,他包含两次遍历

    • 第一次遍历,标记删除更新前有,更新后无的属性
    • 第二次遍历,标记更新Update流程前后发生改变的属性

    diffProperties方法返回一个被标记的变化属性数组,相邻两项作为属性的(key, value)

    所有的变化都会保存到fiberNode.updateQueue中,并且fiberNode会被标记Update flag

Question-2:

采用相邻两项作为属性的(key, value)对比更符合语义化的Array<{key:string, value:any}>有什么优势?

Answer-2:(我也没看出啥名堂,听gpt怎么说)

  • 性能:开辟对象空间占用更高,这种方法对内存占用更少
  • 高效:遍历以及对内容处理更高效
  • 顺序保障:保障数据顺序的一致性

没太理解2,3点

Released under the MIT License.