上一节我们了解到render阶段的工作可以分为“递”阶段和“归”阶段。其中“递”阶段会执行beginWork,“归”阶段会执行completeWork。这一节我们看看“递”阶段的beginWork方法究竟做了什么。

方法概览

可以从源码这里beginWork - 图1 (opens new window)看到beginWork的定义。整个方法大概有500行代码。

从上一节我们已经知道,beginWork的工作是传入当前Fiber节点,创建子Fiber节点,我们从传参来看看具体是如何做的。

从传参看方法执行

  1. function beginWork(
  2. current: Fiber | null,
  3. workInProgress: Fiber,
  4. renderLanes: Lanes,
  5. ): Fiber | null {
  6. // ...省略函数体
  7. }

其中传参:

  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级相关,在讲解Scheduler时再讲解

双缓存机制一节我们知道,除rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mountcurrent === null

组件update时,由于之前已经mount过,所以current !== null

所以我们可以通过current === null ?来区分组件是处于mount还是update

基于此原因,beginWork的工作可以分为两部分:

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child

  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点

  1. function beginWork(
  2. current: Fiber | null,
  3. workInProgress: Fiber,
  4. renderLanes: Lanes
  5. ): Fiber | null {
  6. // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  7. if (current !== null) {
  8. // ...省略
  9. // 复用current
  10. return bailoutOnAlreadyFinishedWork(
  11. current,
  12. workInProgress,
  13. renderLanes,
  14. );
  15. } else {
  16. didReceiveUpdate = false;
  17. }
  18. // mount时:根据tag不同,创建不同的子Fiber节点
  19. switch (workInProgress.tag) {
  20. case IndeterminateComponent:
  21. // ...省略
  22. case LazyComponent:
  23. // ...省略
  24. case FunctionComponent:
  25. // ...省略
  26. case ClassComponent:
  27. // ...省略
  28. case HostRoot:
  29. // ...省略
  30. case HostComponent:
  31. // ...省略
  32. case HostText:
  33. // ...省略
  34. // ...省略其他类型
  35. }
  36. }

update时

我们可以看到,满足如下情况时didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber

  1. oldProps === newProps && workInProgress.type === current.type,即propsfiber.type不变
  2. !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够,会在讲解Scheduler时介绍
  1. if (current !== null) {
  2. const oldProps = current.memoizedProps;
  3. const newProps = workInProgress.pendingProps;
  4. if (
  5. oldProps !== newProps ||
  6. hasLegacyContextChanged() ||
  7. (__DEV__ ? workInProgress.type !== current.type : false)
  8. ) {
  9. didReceiveUpdate = true;
  10. } else if (!includesSomeLane(renderLanes, updateLanes)) {
  11. didReceiveUpdate = false;
  12. switch (workInProgress.tag) {
  13. // 省略处理
  14. }
  15. return bailoutOnAlreadyFinishedWork(
  16. current,
  17. workInProgress,
  18. renderLanes,
  19. );
  20. } else {
  21. didReceiveUpdate = false;
  22. }
  23. } else {
  24. didReceiveUpdate = false;
  25. }

mount时

当不满足优化路径时,我们就进入第二部分,新建子Fiber

我们可以看到,根据fiber.tag不同,进入不同类型Fiber的创建逻辑。

可以从这里beginWork - 图2 (opens new window)看到tag对应的组件类型

  1. // mount时:根据tag不同,创建不同的Fiber节点
  2. switch (workInProgress.tag) {
  3. case IndeterminateComponent:
  4. // ...省略
  5. case LazyComponent:
  6. // ...省略
  7. case FunctionComponent:
  8. // ...省略
  9. case ClassComponent:
  10. // ...省略
  11. case HostRoot:
  12. // ...省略
  13. case HostComponent:
  14. // ...省略
  15. case HostText:
  16. // ...省略
  17. // ...省略其他类型
  18. }

对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildrenbeginWork - 图3 (opens new window)方法。

reconcileChildren

从该函数名就能看出这是Reconciler模块的核心部分。那么他究竟做了什么呢?

  • 对于mount的组件,他会创建新的子Fiber节点

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

  1. export function reconcileChildren(
  2. current: Fiber | null,
  3. workInProgress: Fiber,
  4. nextChildren: any,
  5. renderLanes: Lanes
  6. ) {
  7. if (current === null) {
  8. // 对于mount的组件
  9. workInProgress.child = mountChildFibers(
  10. workInProgress,
  11. null,
  12. nextChildren,
  13. renderLanes,
  14. );
  15. } else {
  16. // 对于update的组件
  17. workInProgress.child = reconcileChildFibers(
  18. workInProgress,
  19. current.child,
  20. nextChildren,
  21. renderLanes,
  22. );
  23. }
  24. }

从代码可以看出,和beginWork一样,他也是通过current === null ?区分mountupdate

不论走哪个逻辑,最终他会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值beginWork - 图4 (opens new window),并作为下次performUnitOfWork执行时workInProgress传参beginWork - 图5 (opens new window)

注意

值得一提的是,mountChildFibersreconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。

effectTag

我们知道,render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中。

你可以从这里beginWork - 图6 (opens new window)看到effectTag对应的DOM操作

比如:

  1. // DOM需要插入到页面中
  2. export const Placement = /* */ 0b00000000000010;
  3. // DOM需要更新
  4. export const Update = /* */ 0b00000000000100;
  5. // DOM需要插入到页面中并更新
  6. export const PlacementAndUpdate = /* */ 0b00000000000110;
  7. // DOM需要删除
  8. export const Deletion = /* */ 0b00000000001000;

通过二进制表示effectTag,可以方便的使用位操作为fiber.effectTag赋值多个effect

那么,如果要通知RendererFiber节点对应的DOM节点插入页面中,需要满足两个条件:

  1. fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点

  2. (fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag

我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?

针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。

第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。

为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。

根Fiber节点 Demo

借用上一节的Demo,第一个进入beginWork方法的Fiber节点就是rootFiber,他的alternate指向current rootFiber(即他存在current)。

为什么rootFiber节点存在current(即rootFiber.alternate),我们在双缓存机制一节mount时的第二步已经讲过

由于存在currentrootFiberreconcileChildren时会走reconcileChildFibers逻辑。

而之后通过beginWork创建的Fiber节点是不存在current的(即 fiber.alternate === null),会走mountChildFibers逻辑

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

参考资料

beginWork流程图

beginWork流程图