流程概览一节我们了解组件在render阶段会经历beginWorkcompleteWork

上一节我们讲解了组件执行beginWork后会创建子Fiber节点,节点上可能存在effectTag

这一节让我们看看completeWork会做什么工作。

你可以从这里completeWork - 图1 (opens new window)看到completeWork方法定义。

流程概览

类似beginWorkcompleteWork也是针对不同fiber.tag调用不同的处理逻辑。

  1. function completeWork(
  2. current: Fiber | null,
  3. workInProgress: Fiber,
  4. renderLanes: Lanes,
  5. ): Fiber | null {
  6. const newProps = workInProgress.pendingProps;
  7. switch (workInProgress.tag) {
  8. case IndeterminateComponent:
  9. case LazyComponent:
  10. case SimpleMemoComponent:
  11. case FunctionComponent:
  12. case ForwardRef:
  13. case Fragment:
  14. case Mode:
  15. case Profiler:
  16. case ContextConsumer:
  17. case MemoComponent:
  18. return null;
  19. case ClassComponent: {
  20. // ...省略
  21. return null;
  22. }
  23. case HostRoot: {
  24. // ...省略
  25. updateHostContainer(workInProgress);
  26. return null;
  27. }
  28. case HostComponent: {
  29. // ...省略
  30. return null;
  31. }
  32. // ...省略

我们重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点),其他类型Fiber的处理留在具体功能实现时讲解。

处理HostComponent

beginWork一样,我们根据current === null ?判断是mount还是update

同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点

  1. case HostComponent: {
  2. popHostContext(workInProgress);
  3. const rootContainerInstance = getRootHostContainer();
  4. const type = workInProgress.type;
  5. if (current !== null && workInProgress.stateNode != null) {
  6. // update的情况
  7. // ...省略
  8. } else {
  9. // mount的情况
  10. // ...省略
  11. }
  12. return null;
  13. }

update时

update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:

  • onClickonChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop

我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。

  1. if (current !== null && workInProgress.stateNode != null) {
  2. // update的情况
  3. updateHostComponent(
  4. current,
  5. workInProgress,
  6. type,
  7. newProps,
  8. rootContainerInstance,
  9. );
  10. }

你可以从这里completeWork - 图2 (opens new window)看到updateHostComponent方法定义。

updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

  1. workInProgress.updateQueue = (updatePayload: any);

其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value

具体渲染过程见mutation阶段一节

updatePayload属性 Demo

updateHostComponent方法内打印了Fiber节点对应的typeupdatePayload

你可以直观的感受updatePayload的数据结构

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

mount时

同样,我们省略了不相关的逻辑。可以看到,mount时的主要逻辑包括三个:

  • Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点
  • update逻辑中的updateHostComponent类似的处理props的过程
  1. // mount的情况
  2. // ...省略服务端渲染相关逻辑
  3. const currentHostContext = getHostContext();
  4. // 为fiber创建对应DOM节点
  5. const instance = createInstance(
  6. type,
  7. newProps,
  8. rootContainerInstance,
  9. currentHostContext,
  10. workInProgress,
  11. );
  12. // 将子孙DOM节点插入刚生成的DOM节点中
  13. appendAllChildren(instance, workInProgress, false, false);
  14. // DOM节点赋值给fiber.stateNode
  15. workInProgress.stateNode = instance;
  16. // 与update逻辑中的updateHostComponent类似的处理props的过程
  17. if (
  18. finalizeInitialChildren(
  19. instance,
  20. type,
  21. newProps,
  22. rootContainerInstance,
  23. currentHostContext,
  24. )
  25. ) {
  26. markUpdate(workInProgress);
  27. }

还记得上一节我们讲到:mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?

原因就在于completeWork中的appendAllChildren方法。

由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树

effectList

至此render阶段的绝大部分工作就完成了。

还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTagFiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== nullFiber节点么?

这显然是很低效的。

为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTagFiber节点会被保存在一条被称为effectList的单向链表中。

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect

类似appendAllChildren,在“归”阶段,所有有effectTagFiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。

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

这样,在commit阶段只需要遍历effectList就能执行所有effect了。

你可以在这里completeWork - 图3 (opens new window)看到这段代码逻辑。

借用React团队成员Dan Abramov的话:effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。

流程结尾

至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

  1. commitRoot(root);

代码见这里completeWork - 图4 (opens new window)

参考资料

completeWork流程图

completeWork流程图