• 【React源码】(十一)fiber 树渲染


    fiber 树渲染

    在正式分析fiber树渲染之前, 再次回顾一下reconciler 运作流程的 4 个阶段:

    1. 输入阶段: 衔接react-dom包, 承接fiber更新请求(参考React 应用的启动过程).
    2. 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调(参考React 调度原理(scheduler)).
    3. 执行任务回调: 在内存中构造出fiber树DOM对象(参考fiber 树构造(初次创建)和 fiber 树构造(对比更新)).
    4. 输出: 与渲染器(react-dom)交互, 渲染DOM节点.

    本节分析其中的第 4 阶段(输出), fiber树渲染处于reconciler 运作流程这一流水线的最后一环, 或者说前面的步骤都是为了最后一步服务, 所以其重要性不言而喻.

    前文已经介绍了fiber树构造, 现在分析fiber树渲染过程, 这个过程, 实际上是对fiber树的进一步处理.

    fiber 树特点

    通过前文fiber树构造的解读, 可以总结出fiber树的基本特点:

    • 无论是首次构造或者是对比更新, 最终都会在内存中生成一棵用于渲染页面的fiber树(即fiberRoot.finishedWork).
    • 这棵将要被渲染的fiber树有 2 个特点:
      1. 副作用队列挂载在根节点上(具体来讲是finishedWork.firstEffect)
      2. 代表最新页面的DOM对象挂载在fiber树中首个HostComponent类型的节点上(具体来讲DOM对象是挂载在fiber.stateNode属性上)

    这里再次回顾前文使用过的 2 棵 fiber 树, 可以验证上述特点:

    1. 初次构造

    1. 对比更新

    commitRoot

    整个渲染逻辑都在commitRoot 函数中:

    1. function commitRoot(root) {
    2. const renderPriorityLevel = getCurrentPriorityLevel();
    3. runWithPriority(
    4. ImmediateSchedulerPriority,
    5. commitRootImpl.bind(null, root, renderPriorityLevel),
    6. );
    7. return null;
    8. }

    commitRoot中同时使用到了渲染优先级调度优先级, 有关优先级的讨论, 在前文已经做出了说明(参考React 中的优先级管理fiber 树构造(基础准备)#优先级), 本节不再赘述. 最后的实现是通过commitRootImpl函数:

     
    
    1. // ... 省略部分无关代码
    2. function commitRootImpl(root, renderPriorityLevel) {
    3. // ============ 渲染前: 准备 ============
    4. const finishedWork = root.finishedWork;
    5. const lanes = root.finishedLanes;
    6. // 清空FiberRoot对象上的属性
    7. root.finishedWork = null;
    8. root.finishedLanes = NoLanes;
    9. root.callbackNode = null;
    10. if (root === workInProgressRoot) {
    11. // 重置全局变量
    12. workInProgressRoot = null;
    13. workInProgress = null;
    14. workInProgressRootRenderLanes = NoLanes;
    15. }
    16. // 再次更新副作用队列
    17. let firstEffect;
    18. if (finishedWork.flags > PerformedWork) {
    19. // 默认情况下fiber节点的副作用队列是不包括自身的
    20. // 如果根节点有副作用, 则将根节点添加到副作用队列的末尾
    21. if (finishedWork.lastEffect !== null) {
    22. finishedWork.lastEffect.nextEffect = finishedWork;
    23. firstEffect = finishedWork.firstEffect;
    24. } else {
    25. firstEffect = finishedWork;
    26. }
    27. } else {
    28. firstEffect = finishedWork.firstEffect;
    29. }
    30. // ============ 渲染 ============
    31. let firstEffect = finishedWork.firstEffect;
    32. if (firstEffect !== null) {
    33. const prevExecutionContext = executionContext;
    34. executionContext |= CommitContext;
    35. // 阶段1: dom突变之前
    36. nextEffect = firstEffect;
    37. do {
    38. commitBeforeMutationEffects();
    39. } while (nextEffect !== null);
    40. // 阶段2: dom突变, 界面发生改变
    41. nextEffect = firstEffect;
    42. do {
    43. commitMutationEffects(root, renderPriorityLevel);
    44. } while (nextEffect !== null);
    45. // 恢复界面状态
    46. resetAfterCommit(root.containerInfo);
    47. // 切换current指针
    48. root.current = finishedWork;
    49. // 阶段3: layout阶段, 调用生命周期componentDidUpdate和回调函数等
    50. nextEffect = firstEffect;
    51. do {
    52. commitLayoutEffects(root, lanes);
    53. } while (nextEffect !== null);
    54. nextEffect = null;
    55. executionContext = prevExecutionContext;
    56. }
    57. // ============ 渲染后: 重置与清理 ============
    58. if (rootDoesHavePassiveEffects) {
    59. // 有被动作用(使用useEffect), 保存一些全局变量
    60. } else {
    61. // 分解副作用队列链表, 辅助垃圾回收
    62. // 如果有被动作用(使用useEffect), 会把分解操作放在flushPassiveEffects函数中
    63. nextEffect = firstEffect;
    64. while (nextEffect !== null) {
    65. const nextNextEffect = nextEffect.nextEffect;
    66. nextEffect.nextEffect = null;
    67. if (nextEffect.flags & Deletion) {
    68. detachFiberAfterEffects(nextEffect);
    69. }
    70. nextEffect = nextNextEffect;
    71. }
    72. }
    73. // 重置一些全局变量(省略这部分代码)...
    74. // 下面代码用于检测是否有新的更新任务
    75. // 比如在componentDidMount函数中, 再次调用setState()
    76. // 1. 检测常规(异步)任务, 如果有则会发起异步调度(调度中心`scheduler`只能异步调用)
    77. ensureRootIsScheduled(root, now());
    78. // 2. 检测同步任务, 如果有则主动调用flushSyncCallbackQueue(无需再次等待scheduler调度), 再次进入fiber树构造循环
    79. flushSyncCallbackQueue();
    80. return null;
    81. }

    commitRootImpl函数中, 可以根据是否调用渲染, 把整个commitRootImpl分为 3 段(分别是渲染前渲染渲染后).

    渲染前

    为接下来正式渲染, 做一些准备工作. 主要包括:

    1. 设置全局状态(如: 更新fiberRoot上的属性)
    2. 重置全局变量(如: workInProgressRootworkInProgress等)
    3. 再次更新副作用队列: 只针对根节点fiberRoot.finishedWork
      • 默认情况下根节点的副作用队列是不包括自身的, 如果根节点有副作用, 则将根节点添加到副作用队列的末尾
      • 注意只是延长了副作用队列, 但是fiberRoot.lastEffect指针并没有改变. 比如首次构造时, 根节点拥有Snapshot标记:

    渲染

    commitRootImpl函数中, 渲染阶段的主要逻辑是处理副作用队列, 将最新的 DOM 节点(已经在内存中, 只是还没渲染)渲染到界面上.

    整个渲染过程被分为 3 个函数分布实现:

    1. commitBeforeMutationEffects
      • dom 变更之前, 处理副作用队列中带有Snapshot,Passive标记的fiber节点.
    2. commitMutationEffects
      • dom 变更, 界面得到更新. 处理副作用队列中带有PlacementUpdateDeletionHydrating标记的fiber节点.
    3. commitLayoutEffects
      • dom 变更后, 处理副作用队列中带有Update | Callback标记的fiber节点.

    通过上述源码分析, 可以把commitRootImpl的职责概括为 2 个方面:

    1. 处理副作用队列. (步骤 1,2,3 都会处理, 只是处理节点的标识fiber.flags不同).
    2. 调用渲染器, 输出最终结果. (在步骤 2: commitMutationEffects中执行).

    所以commitRootImpl是处理fiberRoot.finishedWork这棵即将被渲染的fiber树, 理论上无需关心这棵fiber树是如何产生的(可以是首次构造产生, 也可以是对比更新产生). 为了清晰简便, 在下文的所有图示都使用初次创建的fiber树结构来进行演示.

    这 3 个函数处理的对象是副作用队列DOM对象.

    所以无论fiber树结构有多么复杂, 到了commitRoot阶段, 实际起作用的只有 2 个节点:

    • 副作用队列所在节点: 根节点, 即HostRootFiber节点.
    • DOM对象所在节点: 从上至下首个HostComponent类型的fiber节点, 此节点 fiber.stateNode实际上指向最新的 DOM 树.

    下图为了清晰, 省略了一些无关引用, 只留下commitRoot阶段实际会用到的fiber节点:

    commitBeforeMutationEffects

    第一阶段: dom 变更之前, 处理副作用队列中带有Snapshot,Passive标记的fiber节点.

    1. // ... 省略部分无关代码
    2. function commitBeforeMutationEffects() {
    3. while (nextEffect !== null) {
    4. const current = nextEffect.alternate;
    5. const flags = nextEffect.flags;
    6. // 处理`Snapshot`标记
    7. if ((flags & Snapshot) !== NoFlags) {
    8. commitBeforeMutationEffectOnFiber(current, nextEffect);
    9. }
    10. // 处理`Passive`标记
    11. if ((flags & Passive) !== NoFlags) {
    12. // Passive标记只在使用了hook, useEffect会出现. 所以此处是针对hook对象的处理
    13. if (!rootDoesHavePassiveEffects) {
    14. rootDoesHavePassiveEffects = true;
    15. scheduleCallback(NormalSchedulerPriority, () => {
    16. flushPassiveEffects();
    17. return null;
    18. });
    19. }
    20. }
    21. nextEffect = nextEffect.nextEffect;
    22. }
    23. }

    注意:commitBeforeMutationEffectOnFiber实际上对应了commitBeforeMutationLifeCycles函数,在导入时进行了重命名

    1. 处理Snapshot标记
     
    
    1. function commitBeforeMutationLifeCycles(
    2. current: Fiber | null,
    3. finishedWork: Fiber,
    4. ): void {
    5. switch (finishedWork.tag) {
    6. case FunctionComponent:
    7. case ForwardRef:
    8. case SimpleMemoComponent:
    9. case Block: {
    10. return;
    11. }
    12. case ClassComponent: {
    13. if (finishedWork.flags & Snapshot) {
    14. if (current !== null) {
    15. const prevProps = current.memoizedProps;
    16. const prevState = current.memoizedState;
    17. const instance = finishedWork.stateNode;
    18. const snapshot = instance.getSnapshotBeforeUpdate(
    19. finishedWork.elementType === finishedWork.type
    20. ? prevProps
    21. : resolveDefaultProps(finishedWork.type, prevProps),
    22. prevState,
    23. );
    24. instance.__reactInternalSnapshotBeforeUpdate = snapshot;
    25. }
    26. }
    27. return;
    28. }
    29. case HostRoot: {
    30. if (supportsMutation) {
    31. if (finishedWork.flags & Snapshot) {
    32. const root = finishedWork.stateNode;
    33. clearContainer(root.containerInfo);
    34. }
    35. }
    36. return;
    37. }
    38. case HostComponent:
    39. case HostText:
    40. case HostPortal:
    41. case IncompleteClassComponent:
    42. return;
    43. }
    44. }

    从源码中可以看到, 与Snapshot标记相关的类型只有ClassComponentHostRoot.

    • 对于ClassComponent类型节点, 调用了instance.getSnapshotBeforeUpdate生命周期函数
    • 对于HostRoot类型节点, 调用clearContainer清空了容器节点(即div#root这个 dom 节点).
    1. 处理Passive标记

    Passive标记只会在使用了hook对象的function类型的节点上存在, 后续的执行过程在hook原理章节中详细说明. 此处我们需要了解在commitRoot的第一个阶段, 为了处理hook对象(如useEffect), 通过scheduleCallback单独注册了一个调度任务task, 等待调度中心scheduler处理.

    注意: 通过调度中心scheduler调度的任务task均是通过MessageChannel触发, 都是异步执行(可参考React 调度原理(scheduler)).

    小测试:

    1. // 以下示例代码中的输出顺序为 1, 3, 4, 2
    2. function Test() {
    3. console.log(1);
    4. useEffect(() => {
    5. console.log(2);
    6. });
    7. console.log(3);
    8. Promise.resolve(() => {
    9. console.log(4);
    10. });
    11. return <div>testdiv>;
    12. }

    commitMutationEffects

    第二阶段: dom 变更, 界面得到更新. 处理副作用队列中带有ContentResetRefPlacementUpdateDeletionHydrating标记的fiber节点.

     
    
    1. // ...省略部分无关代码
    2. function commitMutationEffects(
    3. root: FiberRoot,
    4. renderPriorityLevel: ReactPriorityLevel,
    5. ) {
    6. // 处理Ref
    7. if (flags & Ref) {
    8. const current = nextEffect.alternate;
    9. if (current !== null) {
    10. // 先清空ref, 在commitRoot的第三阶段(dom变更后), 再重新赋值
    11. commitDetachRef(current);
    12. }
    13. }
    14. // 处理DOM突变
    15. while (nextEffect !== null) {
    16. const flags = nextEffect.flags;
    17. const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    18. switch (primaryFlags) {
    19. case Placement: {
    20. // 新增节点
    21. commitPlacement(nextEffect);
    22. nextEffect.flags &= ~Placement; // 注意Placement标记会被清除
    23. break;
    24. }
    25. case PlacementAndUpdate: {
    26. // Placement
    27. commitPlacement(nextEffect);
    28. nextEffect.flags &= ~Placement;
    29. // Update
    30. const current = nextEffect.alternate;
    31. commitWork(current, nextEffect);
    32. break;
    33. }
    34. case Update: {
    35. // 更新节点
    36. const current = nextEffect.alternate;
    37. commitWork(current, nextEffect);
    38. break;
    39. }
    40. case Deletion: {
    41. // 删除节点
    42. commitDeletion(root, nextEffect, renderPriorityLevel);
    43. break;
    44. }
    45. }
    46. nextEffect = nextEffect.nextEffect;
    47. }
    48. }

    处理 DOM 突变:

    1. 新增: 函数调用栈 commitPlacement -> insertOrAppendPlacementNode -> appendChild
    2. 更新: 函数调用栈 commitWork -> commitUpdate
    3. 删除: 函数调用栈 commitDeletion -> removeChild

    最终会调用appendChild, commitUpdate, removeChild这些react-dom包中的函数. 它们是HostConfig协议(源码在 ReactDOMHostConfig.js 中)中规定的标准函数, 在渲染器react-dom包中进行实现. 这些函数就是直接操作 DOM, 所以执行之后, 界面也会得到更新.

    注意: commitMutationEffects执行之后, 在commitRootImpl函数中切换当前fiber树(root.current = finishedWork),保证fiberRoot.current指向代表当前界面的fiber树.

    commitLayoutEffects

    第三阶段: dom 变更后, 处理副作用队列中带有Update, Callback, Ref标记的fiber节点.

     
    
    1. // ...省略部分无关代码
    2. function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
    3. while (nextEffect !== null) {
    4. const flags = nextEffect.flags;
    5. // 处理 Update和Callback标记
    6. if (flags & (Update | Callback)) {
    7. const current = nextEffect.alternate;
    8. commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    9. }
    10. if (flags & Ref) {
    11. // 重新设置ref
    12. commitAttachRef(nextEffect);
    13. }
    14. nextEffect = nextEffect.nextEffect;
    15. }
    16. }
    17. 核心逻辑都在commitLayoutEffectOnFiber->commitLifeCycles函数中.
    18. // ...省略部分无关代码
    19. function commitLifeCycles(
    20. finishedRoot: FiberRoot,
    21. current: Fiber | null,
    22. finishedWork: Fiber,
    23. committedLanes: Lanes,
    24. ): void {
    25. switch (finishedWork.tag) {
    26. case ClassComponent: {
    27. const instance = finishedWork.stateNode;
    28. if (finishedWork.flags & Update) {
    29. if (current === null) {
    30. // 初次渲染: 调用 componentDidMount
    31. instance.componentDidMount();
    32. } else {
    33. const prevProps =
    34. finishedWork.elementType === finishedWork.type
    35. ? current.memoizedProps
    36. : resolveDefaultProps(finishedWork.type, current.memoizedProps);
    37. const prevState = current.memoizedState;
    38. // 更新阶段: 调用 componentDidUpdate
    39. instance.componentDidUpdate(
    40. prevProps,
    41. prevState,
    42. instance.__reactInternalSnapshotBeforeUpdate,
    43. );
    44. }
    45. }
    46. const updateQueue: UpdateQueue<
    47. *,
    48. > | null = (finishedWork.updateQueue: any);
    49. if (updateQueue !== null) {
    50. // 处理update回调函数 如: this.setState({}, callback)
    51. commitUpdateQueue(finishedWork, updateQueue, instance);
    52. }
    53. return;
    54. }
    55. case HostComponent: {
    56. const instance: Instance = finishedWork.stateNode;
    57. if (current === null && finishedWork.flags & Update) {
    58. const type = finishedWork.type;
    59. const props = finishedWork.memoizedProps;
    60. // 设置focus等原生状态
    61. commitMount(instance, type, props, finishedWork);
    62. }
    63. return;
    64. }
    65. }
    66. }

    commitLifeCycles函数中:

    • 对于ClassComponent节点, 调用生命周期函数componentDidMountcomponentDidUpdate, 调用update.callback回调函数.
    • 对于HostComponent节点, 如有Update标记, 需要设置一些原生状态(如: focus等)

    渲染后

    执行完上述步骤之后, 本次渲染任务就已经完成了. 在渲染完成后, 需要做一些重置和清理工作:

    1. 清除副作用队列

      • 由于副作用队列是一个链表, 由于单个fiber对象的引用关系, 无法被gc回收.
      • 将链表全部拆开, 当fiber对象不再使用的时候, 可以被gc回收.

    1. 检测更新
      • 在整个渲染过程中, 有可能产生新的update(比如在componentDidMount函数中, 再次调用setState()).
      • 如果是常规(异步)任务, 不用特殊处理, 调用ensureRootIsScheduled确保任务已经注册到调度中心即可.
      • 如果是同步任务, 则主动调用flushSyncCallbackQueue(无需再次等待 scheduler 调度), 再次进入 fiber 树构造循环
     
    
    1. // 清除副作用队列
    2. if (rootDoesHavePassiveEffects) {
    3. // 有被动作用(使用useEffect), 保存一些全局变量
    4. } else {
    5. // 分解副作用队列链表, 辅助垃圾回收.
    6. // 如果有被动作用(使用useEffect), 会把分解操作放在flushPassiveEffects函数中
    7. nextEffect = firstEffect;
    8. while (nextEffect !== null) {
    9. const nextNextEffect = nextEffect.nextEffect;
    10. nextEffect.nextEffect = null;
    11. if (nextEffect.flags & Deletion) {
    12. detachFiberAfterEffects(nextEffect);
    13. }
    14. nextEffect = nextNextEffect;
    15. }
    16. }
    17. // 重置一些全局变量(省略这部分代码)...
    18. // 下面代码用于检测是否有新的更新任务
    19. // 比如在componentDidMount函数中, 再次调用setState()
    20. // 1. 检测常规(异步)任务, 如果有则会发起异步调度(调度中心`scheduler`只能异步调用)

    ensureRootIsScheduled(root, now());

    // 2. 检测同步任务, 如果有则主动调用flushSyncCallbackQueue(无需再次等待scheduler调度), 再次进入fiber树构造循环

    flushSyncCallbackQueue();

    总结

    本节分析了fiber 树渲染的处理过程, 从宏观上看fiber 树渲染位于reconciler 运作流程中的输出阶段, 是整个reconciler 运作流程的链路中最后一环(从输入到输出). 本节根据源码, 具体从渲染前, 渲染, 渲染后三个方面分解了commitRootImpl函数. 其中最核心的渲染逻辑又分为了 3 个函数, 这 3 个函数共同处理了有副作用fiber节点, 并通过渲染器react-dom把最新的 DOM 对象渲染到界面上.

  • 相关阅读:
    Java.lang.Class类 isEnum()方法有什么功能呢?
    PHP刷leetcode第一弹: 两数之和
    Gitlab SSRF 漏洞复现 CVE-2021-22214
    ZZ308 物联网应用与服务赛题第G套
    2022年Java秋招面试必看的 | ZooKeeper面试题
    JS创建一个数组(字面量)
    代码随想录算法训练营第48天 | ● 198.打家劫舍 ● 213.打家劫舍II ● 337.打家劫舍III
    2.22每日一题(含绝对值的定积分+极值+凹凸区间+单调区间)
    MySQL FROM_UNIXTIME时间戳转换函数
    供应化学试剂BHQ-1 氨基|BHQ-1 amine|1308657-79-5
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/126525335