什么是 `Effect List`?React 16 与 React 18 在收集副作用逻辑上的演进差异

React 副作用管理机制的演进:从 effectTagFlags 的蜕变

各位技术爱好者,大家好!

今天,我们将深入探讨 React 内部一个至关重要的机制:副作用(Side Effects)的管理。React 作为一个声明式 UI 库,其核心使命是将状态变化映射到 UI 视图。然而,在实际应用中,我们不可避免地需要与外部系统进行交互,例如操作 DOM、发起网络请求、订阅外部事件、设置定时器等。这些与渲染本身无关,但又必须发生的行为,我们统称为副作用。

React 为了维护其声明式和纯粹的渲染理念,提供了一套机制来“安全”地执行这些副作用。其中,Effect List(效果列表)是贯穿整个副作用管理流程的核心数据结构。我们将以一次深度技术讲座的形式,从 React 16 的实现出发,逐步剖析其副作用收集逻辑,再过渡到 React 18 中引入的重大演进,探究这些变化背后的设计哲学与对并发模式的支撑。

副作用:前端世界的必然之恶与 React 的驯服

在函数式编程范式中,纯函数是理想的:给定相同的输入,总是返回相同的输出,并且不会产生任何可观察的副作用。React 的渲染阶段(render phase)正是努力遵循这一原则,它应该是一个纯粹的计算过程,仅仅根据 props 和 state 计算出新的 UI 树。

然而,真实的应用程序离不开副作用:

  • DOM 操作:除了 React 自身管理的部分,我们可能需要手动调整某些 DOM 属性,或者集成第三方非 React 库。
  • 数据获取:组件挂载后通常需要从服务器获取数据。
  • 订阅与取消订阅:监听事件、WebSocket 连接、全局状态管理库的订阅。
  • 定时器setTimeoutsetInterval
  • 日志记录:发送分析数据。

如果这些副作用在渲染过程中随意发生,将会带来巨大的不确定性和难以调试的问题。例如,如果一个副作用在渲染过程中修改了状态,可能会导致无限循环渲染;如果它在渲染尚未完成时操作了 DOM,可能会看到闪烁或不一致的 UI。

为此,React 引入了“生命周期方法”和后来的 HooksuseEffectuseLayoutEffect),将副作用的执行延迟到渲染阶段之后,即所谓的“提交阶段”(Commit Phase)。这意味着,React 会先计算出完整的 UI 树(即 Fiber 树),然后一次性地将所有必要的 DOM 更新应用到真实 DOM 上,最后才执行组件中定义的副作用逻辑。

Effect List,正是 React 用来高效收集和执行这些副作用的关键内部数据结构。

Effect List 的核心概念:副作用的“待办事项”清单

在 React 内部,Effect List 是一个存储着所有需要执行副作用的 Fiber 节点的单向链表。当 React 完成了对组件树的调和(Reconciliation),并确定了哪些 Fiber 节点需要进行 DOM 更新、触发生命周期方法或执行 useEffect 回调时,它会将这些 Fiber 节点添加到这个特殊的链表中。

为什么需要 Effect List

如果没有 Effect List,React 在提交阶段执行副作用时,将不得不遍历整个 Fiber 树,检查每一个 Fiber 节点是否需要执行副作用。这在大型应用中效率极低,因为大部分 Fiber 节点在每次更新时可能都没有副作用。

通过构建 Effect List,React 只需要遍历这个链表,就能精确地找到所有“有待办事项”的 Fiber 节点,从而大幅提升了提交阶段的效率。这个链表是从根 Fiber 节点开始,通过 nextEffect 指针将所有带有副作用的 Fiber 节点串联起来的。

理解 Effect List 的构建和遍历机制,是深入理解 React 渲染流程和副作用管理的关键。接下来,我们将分别探讨 React 16 和 React 18 如何构建和利用这个列表。

React 16 中的 Effect ListeffectTag:统一的标记系统

React 16 引入了 Fiber 架构,这是一个对调和算法的彻底重写,旨在实现并发渲染。在 Fiber 架构中,每个 React 元素都对应一个 Fiber 节点,这些节点构成了 Fiber 树。

Fiber 节点的核心属性与 effectTag

在 React 16 的 Fiber 节点中,除了 typepropsstateNode(指向真实 DOM 或组件实例)等基本属性外,有几个对副作用管理至关重要的属性:

  • child:指向第一个子 Fiber 节点。
  • sibling:指向下一个兄弟 Fiber 节点。
  • return:指向父 Fiber 节点。
  • memoizedState:存储 Hooks 链表(如果组件是函数组件)。
  • memoizedProps:上次渲染使用的 props。
  • pendingProps:下一次渲染将使用的 props。
  • flags(在 React 16 中通常称为 effectTag):这是 React 16 中用来标记 Fiber 节点所带副作用类型的核心属性。它是一个位掩码(bitmask),可以组合多种副作用类型。
  • firstEffect:指向当前 Fiber 节点的子树中第一个带有副作用的 Fiber 节点。
  • lastEffect:指向当前 Fiber 节点的子树中最后一个带有副作用的 Fiber 节点。
  • nextEffect:指向当前 Fiber 节点在整个 Effect List 中的下一个带有副作用的 Fiber 节点。

effectTag 的作用

在 React 16 中,effectTag 是一个非常通用的标记,它承载了所有类型的副作用信息,包括:

  • DOM 操作
    • Placement (1): 表示节点需要被插入到 DOM 中。
    • Update (2): 表示节点需要被更新(例如 props 变化)。
    • Deletion (4): 表示节点需要从 DOM 中删除。
  • 生命周期/Hooks 相关的副作用
    • Layout (8): 对应 componentDidMountcomponentDidUpdateuseLayoutEffect。这些副作用在 DOM 更新后同步执行。
    • Passive (16): 对应 useEffect。这些副作用在 DOM 更新和浏览器绘制完成后异步执行。
  • 其他
    • Ref (64): 处理 ref 更新。
    • Snapshot (128): 对应 getSnapshotBeforeUpdate

这些标签可以组合使用,例如一个 Fiber 节点可能同时具有 Placement | Layout 标记。

Effect List 的构建过程(React 16)

Effect List 的构建主要发生在调和阶段的 completeWork 阶段。调和阶段分为两个子阶段:

  1. 渲染阶段 (Render Phase):从根节点开始,自顶向下遍历 Fiber 树,执行 beginWork 函数。在这个阶段,React 会比较旧的 Fiber 节点和新的 React 元素,生成新的 Fiber 节点,并计算出它们的 effectTag。这个阶段是可中断的。
  2. 提交阶段 (Commit Phase):当渲染阶段完成,或者被中断后又重新恢复并完成时,React 会进入提交阶段。这个阶段是不可中断的,它负责将渲染阶段计算出的所有变更实际应用到 DOM 上,并执行副作用。

completeWork 中的 Effect List 构建

completeWork 函数中,React 会自底向上地遍历 Fiber 树。当一个 Fiber 节点的 completeWork 完成时,它会检查自身是否带有 effectTag,以及它的子节点是否带有 effectTag

伪代码大致如下:

function completeWork(current, workInProgress) {
    // ... 执行当前Fiber节点的完成工作,例如处理props,生成DOM实例等

    if (workInProgress.flags !== NoFlags) { // 如果当前节点有副作用
        // 将当前节点添加到父节点的effect list中
        // 注意:这里是伪代码,实际逻辑会更复杂
        if (workInProgress.return.firstEffect === null) {
            workInProgress.return.firstEffect = workInProgress;
        } else {
            workInProgress.return.lastEffect.nextEffect = workInProgress;
        }
        workInProgress.return.lastEffect = workInProgress;
    }

    // 将子节点的 effect list 链入当前节点的 effect list
    // 这种链入机制确保了 effect list 是一个扁平化的链表
    if (workInProgress.firstEffect !== null) {
        if (workInProgress.return.firstEffect === null) {
            workInProgress.return.firstEffect = workInProgress.firstEffect;
        } else {
            workInProgress.return.lastEffect.nextEffect = workInProgress.firstEffect;
        }
        workInProgress.return.lastEffect = workInProgress.lastEffect;
    }

    // 核心:通过 nextEffect 指针将带有 effectTag 的 Fiber 节点串联起来
    // 根 Fiber 节点最终会有一个指向所有副作用的 effect list 的头指针
    // root.current.alternate.firstEffect
}

简而言之,当一个 Fiber 节点完成其 completeWork 时:

  1. 如果它自身有 effectTag,它会被添加到其父 Fiber 节点的 firstEffect/lastEffect 指向的链表中。
  2. 如果它的子节点有 firstEffectlastEffect,那么这些子节点的副作用链表也会被“冒泡”到当前节点,并最终合并到根 Fiber 节点的 firstEffect 链表中。

最终,rootFiber.current.alternate.firstEffect 将指向一个单向链表,其中包含了所有带有 effectTag 的 Fiber 节点。这个链表就是提交阶段需要遍历的 Effect List

提交阶段(Commit Phase)的执行(React 16)

提交阶段是不可中断的,它分为三个子阶段,每个阶段都会遍历 Effect List

  1. commitBeforeMutationEffects (Pre-mutation)

    • 遍历 Effect List
    • 执行 getSnapshotBeforeUpdate 生命周期方法。
    • 处理 Ref 的解除引用。
    • 检查 Snapshot 类型的 effectTag
  2. commitMutationEffects (Mutation)

    • 再次遍历 Effect List
    • 执行所有实际的 DOM 操作:插入 (Placement)、更新 (Update)、删除 (Deletion)。
    • 例如,如果一个 Fiber 节点有 Placement 标记,React 会将其对应的 DOM 节点插入到正确的位置。
  3. commitLayoutEffects (Layout & Passive)

    • 第三次遍历 Effect List
    • 执行 componentDidMountcomponentDidUpdateuseLayoutEffect 回调(对应 Layout 类型的 effectTag)。这些是同步执行的,会阻塞浏览器绘制。
    • 调度 useEffect 回调:对于带有 Passive 类型的 effectTag 的 Fiber 节点,React 会将其 useEffect 回调(及其清理函数)收集起来,并调度到一个单独的异步任务中执行。这个任务通常会在浏览器绘制完成后,且不阻塞主线程的情况下执行。

React 16 commitRoot 伪代码概览

function commitRoot(root) {
    const finishedWork = root.finishedWork; // 渲染阶段完成的Fiber树

    // 1. Pre-mutation phase (before DOM mutations)
    // 遍历 finishedWork.firstEffect,执行 getSnapshotBeforeUpdate 等
    let effect = finishedWork.firstEffect;
    while (effect !== null) {
        // 检查 effect.flags & Snapshot 或 Ref
        // 执行相应的副作用
        effect = effect.nextEffect;
    }

    // 2. Mutation phase (DOM mutations)
    // 再次遍历 finishedWork.firstEffect,执行 DOM 插入、更新、删除
    effect = finishedWork.firstEffect;
    while (effect !== null) {
        // 检查 effect.flags & (Placement | Update | Deletion)
        // 执行 DOM 操作
        effect = effect.nextEffect;
    }

    // 3. Layout phase (after DOM mutations, before browser paint)
    // 第三次遍历 finishedWork.firstEffect,执行 componentDidMount/Update, useLayoutEffect
    effect = finishedWork.firstEffect;
    while (effect !== null) {
        // 检查 effect.flags & Layout
        // 执行同步副作用
        effect = effect.nextEffect;
    }

    // 4. Passive phase (after browser paint, asynchronously)
    // 收集所有 Passive 类型的 effect,调度一个异步任务
    // 实际的 useEffect 回调和清理函数会在稍后执行
    effect = finishedWork.firstEffect;
    while (effect !== null) {
        // 检查 effect.flags & Passive
        // 收集 effect 到一个单独的链表或数组
        effect = effect.nextEffect;
    }
    // 调度异步任务执行收集到的 Passive effects
}

React 16 的局限性

React 16 的 effectTag 机制虽然强大,但也存在一些问题和限制,尤其是在面对并发模式时:

  1. 多次遍历 Effect List:提交阶段需要完整遍历 Effect List 三次(甚至四次,如果算上 Passive 收集),每次都通过位运算检查 effectTag。这在理论上存在一定的性能开销。
  2. effectTag 的职责过重:一个 effectTag 需要同时编码 DOM 操作、同步生命周期、异步副作用等多种不同性质的变更。这使得代码逻辑变得复杂,也限制了对不同类型副作用的独立优化。
  3. 并发模式的挑战:React 16 对 useEffect 的调度相对简单。在并发模式下,useEffect 的执行时机和优先级控制需要更精细的粒度。将 Passive 类型的 effectTag 与其他 DOM 操作和同步副作用混在一起,使得 useEffect 的延迟执行和可中断性变得不够灵活。

正是为了解决这些问题,并更好地支撑 React 18 的并发特性,React 团队对副作用的收集和执行机制进行了重大改进。

React 18 的演进:细粒度的 Flags 与多列表优化

React 18 的核心是并发模式(Concurrent Mode),它允许 React 在后台准备新的 UI 树,而不会阻塞主线程。为了实现这一目标,副作用的管理必须更加精细化和可控。React 18 对 effectTag 进行了拆分和优化,引入了更细粒度的 Flags 系统,并优化了 Effect List 的构建和遍历方式。

effectTagFlags 的语义升级

在 React 18 中,effectTag 这个名称虽然在内部一些地方可能仍有痕迹,但其语义和使用方式已经彻底改变。现在我们更倾向于称之为 flags。最显著的变化是:

  • DOM 相关的 flagsPlacementUpdateDeletion 等仍然存在,用于标记需要进行 DOM 操作的 Fiber 节点。
  • 非 DOM 相关的 flagsLayoutPassive 等类型现在被提升为独立的 flags,不再仅仅是 effectTag 的一个位。这意味着它们与 DOM 操作的 flags 拥有了更清晰的界限。

更重要的是,React 18 引入了新的 Fiber 属性来更直接地管理 PassiveLayout 效果:

  • subtreeFlags:一个优化,用于快速判断子树中是否存在某些类型的副作用,避免不必要的遍历。
  • firstLayoutEffect / lastLayoutEffect:指向当前 Fiber 节点子树中第一个/最后一个 Layout 效果的 Fiber 节点。
  • firstPassiveEffect / lastPassiveEffect:指向当前 Fiber 节点子树中第一个/最后一个 Passive 效果的 Fiber 节点。
  • nextEffect:这个指针仍然存在,主要用于串联那些带有 DOM 操作 flags 的 Fiber 节点,形成一个通用的 Effect List

此外,Hooks 内部也引入了 HookFlags(例如 HookHasEffect),用于标记某个 Hook 是否需要执行副作用。

Effect List 的构建过程(React 18)

在 React 18 中,completeWork 阶段的逻辑变得更加精细。它不再仅仅构建一个扁平化的 Effect List,而是针对不同类型的副作用,在 Fiber 树的冒泡过程中,分别构建或收集到不同的地方:

  1. 通用 Effect List (for DOM Mutations)

    • 带有 PlacementUpdateDeletion 等 DOM 操作 flags 的 Fiber 节点,仍然会通过 nextEffect 指针被链入到父节点的 firstEffect/lastEffect 中,最终形成根 Fiber 节点的通用 Effect List。这与 React 16 的机制类似。
  2. LayoutEffect 链表 (收集到根节点)

    • 当一个 Fiber 节点被标记为 LayoutEffect (例如因为它包含 useLayoutEffect Hook 或 componentDidMount/Update) 时,它会被添加到根 Fiber 节点(或最近的 Host Root)的 firstLayoutEffectlastLayoutEffect 指向的链表中。
    • 这个过程也是在 completeWork 中自底向上进行的。子 Fiber 节点的 LayoutEffect 链表会合并到父 Fiber 节点的 LayoutEffect 链表,最终聚合到根节点。
  3. PassiveEffect 链表 (收集到根节点)

    • 同理,当一个 Fiber 节点被标记为 PassiveEffect (因为它包含 useEffect Hook) 时,它会被添加到根 Fiber 节点(或最近的 Host Root)的 firstPassiveEffectlastPassiveEffect 指向的链表中。
    • 这个链表的构建方式与 LayoutEffect 链表类似,也是通过冒泡机制聚合到根节点。

completeWork 中的 Effect List 构建(React 18 伪代码概览)

function completeWork(current, workInProgress) {
    // ... 执行当前Fiber节点的完成工作

    // 1. 处理子节点的副作用链表冒泡
    // 将子节点的通用 Effect List 冒泡到当前节点
    if (workInProgress.firstEffect !== null) {
        if (workInProgress.return.firstEffect === null) {
            workInProgress.return.firstEffect = workInProgress.firstEffect;
        } else {
            workInProgress.return.lastEffect.nextEffect = workInProgress.firstEffect;
        }
        workInProgress.return.lastEffect = workInProgress.lastEffect;
    }

    // 2. 将子节点的 LayoutEffect 链表冒泡到当前节点
    if (workInProgress.firstLayoutEffect !== null) {
        if (workInProgress.return.firstLayoutEffect === null) {
            workInProgress.return.firstLayoutEffect = workInProgress.firstLayoutEffect;
        } else {
            workInProgress.return.lastLayoutEffect.nextEffect = workInProgress.firstLayoutEffect;
        }
        workInProgress.return.lastLayoutEffect = workInProgress.lastLayoutEffect;
    }

    // 3. 将子节点的 PassiveEffect 链表冒泡到当前节点
    if (workInProgress.firstPassiveEffect !== null) {
        if (workInProgress.return.firstPassiveEffect === null) {
            workInProgress.return.firstPassiveEffect = workInProgress.firstPassiveEffect;
        } else {
            workInProgress.return.lastPassiveEffect.nextEffect = workInProgress.firstPassiveEffect;
        }
        workInProgress.return.lastPassiveEffect = workInProgress.lastPassiveEffect;
    }

    // 4. 处理当前节点自身的副作用
    if (workInProgress.flags !== NoFlags) { // 如果当前节点有任何 flags
        // 如果是 DOM 相关的 flags,加入通用 Effect List
        // 这是 nextEffect 的主要用途
        if (workInProgress.return.firstEffect === null) {
            workInProgress.return.firstEffect = workInProgress;
        } else {
            workInProgress.return.lastEffect.nextEffect = workInProgress;
        }
        workInProgress.return.lastEffect = workInProgress;
    }

    // 如果当前节点有 LayoutEffect flags,加入 LayoutEffect 链表
    if ((workInProgress.flags & LayoutMask) !== NoFlags) {
        if (workInProgress.return.firstLayoutEffect === null) {
            workInProgress.return.firstLayoutEffect = workInProgress;
        } else {
            workInProgress.return.lastLayoutEffect.nextEffect = workInProgress;
        }
        workInProgress.return.lastLayoutEffect = workInProgress;
    }

    // 如果当前节点有 PassiveEffect flags,加入 PassiveEffect 链表
    if ((workInProgress.flags & PassiveMask) !== NoFlags) {
        if (workInProgress.return.firstPassiveEffect === null) {
            workInProgress.return.firstPassiveEffect = workInProgress;
        } else {
            workInProgress.return.lastPassiveEffect.nextEffect = workInProgress;
        }
        workInProgress.return.lastPassiveEffect = workInProgress;
    }
}

通过这种方式,在调和阶段结束时,根 Fiber 节点(即 HostRootFiber)上会维护三条独立的链表头指针:

  • firstEffect (用于 DOM 操作等通用副作用)
  • firstLayoutEffect (专门用于 useLayoutEffectcomponentDidMount/Update)
  • firstPassiveEffect (专门用于 useEffect)

这种多链表结构是 React 18 副作用管理的核心优化。

提交阶段(Commit Phase)的执行(React 18)

React 18 的提交阶段结构与 React 16 类似,但其遍历方式发生了根本性变化,因为它不再需要多次遍历一个巨大的通用 Effect List 来筛选副作用类型。

  1. commitBeforeMutationEffects (Pre-mutation)

    • 遍历 root.firstEffect (通用 Effect List)。
    • 执行 getSnapshotBeforeUpdate,处理 Ref 解除引用等。
    • 这与 React 16 类似,因为 getSnapshotBeforeUpdateRef 仍然是与 DOM 变更紧密相关的同步操作。
  2. commitMutationEffects (Mutation)

    • 遍历 root.firstEffect (通用 Effect List)。
    • 执行所有实际的 DOM 操作:插入、更新、删除。
    • 这与 React 16 类似,主要处理 DOM 变更。
  3. commitLayoutEffects (Layout)

    • 直接遍历 root.firstLayoutEffect
    • 执行所有 useLayoutEffectcomponentDidMount/Update 的回调。
    • 这个阶段现在效率更高,因为它直接访问专门的 LayoutEffect 链表,无需进行额外的 flags 检查。
  4. commitPassiveEffects (Passive)

    • 直接遍历 root.firstPassiveEffect
    • 收集所有 useEffect 的清理函数和回调函数。
    • 然后,将这些收集到的 PassiveEffect 回调调度到一个由 Scheduler 模块管理的异步任务中执行。
    • 这个阶段也是效率更高,因为它直接访问专门的 PassiveEffect 链表。

React 18 commitRoot 伪代码概览

function commitRoot(root) {
    const finishedWork = root.finishedWork;

    // 1. Pre-mutation phase
    // 遍历通用 Effect List (root.firstEffect)
    let effect = finishedWork.firstEffect;
    while (effect !== null) {
        // ... execute getSnapshotBeforeUpdate, detach refs etc.
        effect = effect.nextEffect;
    }

    // 2. Mutation phase
    // 遍历通用 Effect List (root.firstEffect)
    effect = finishedWork.firstEffect;
    while (effect !== null) {
        // ... execute DOM mutations (placement, update, deletion)
        effect = effect.nextEffect;
    }

    // 3. Layout phase
    // 直接遍历 Layout Effect List (root.firstLayoutEffect)
    effect = finishedWork.firstLayoutEffect; // 注意这里!
    while (effect !== null) {
        // ... execute useLayoutEffect, componentDidMount/Update
        effect = effect.nextEffect; // LayoutEffect 链表也使用 nextEffect
    }

    // 4. Passive phase (scheduled asynchronously)
    // 直接遍历 Passive Effect List (root.firstPassiveEffect)
    effect = finishedWork.firstPassiveEffect; // 注意这里!
    if (effect !== null) {
        // 收集所有 Passive effects 到一个单独的列表
        const passiveEffects = [];
        while (effect !== null) {
            passiveEffects.push(effect);
            effect = effect.nextEffect; // PassiveEffect 链表也使用 nextEffect
        }
        // 调度一个异步任务来执行这些 passive effects
        // scheduleCallback(NormalPriority, () => {
        //    executePassiveEffects(passiveEffects);
        // });
    }

    // 清理根节点上的效果列表指针
    finishedWork.firstEffect = finishedWork.lastEffect = null;
    finishedWork.firstLayoutEffect = finishedWork.lastLayoutEffect = null;
    finishedWork.firstPassiveEffect = finishedWork.lastPassiveEffect = null;
}

React 16 与 React 18 副作用收集机制对比表格

特性 / 版本 React 16 (Fiber) React 18 (Concurrent)
副作用标记 effectTag (位掩码) flags (位掩码,但语义更清晰)
Effect List 结构 单一的通用 Effect List (root.firstEffect) 三个独立的链表:
root.firstEffect (通用)
root.firstLayoutEffect (Layout)
root.firstPassiveEffect (Passive)
构建方式 completeWork 阶段,子节点 effectTag 冒泡合并到父节点 firstEffect/lastEffect completeWork 阶段,子节点 flags 分别冒泡合并到父节点对应的 firstEffect/firstLayoutEffect/firstPassiveEffect
提交阶段遍历 多次遍历 root.firstEffect,每次通过 effectTag 检查副作用类型。 分别遍历 root.firstEffectroot.firstLayoutEffectroot.firstPassiveEffect,针对性执行。
LayoutEffect 执行 遍历 root.firstEffect,检查 Layout flag。 直接遍历 root.firstLayoutEffect
PassiveEffect 执行 遍历 root.firstEffect,检查 Passive flag,收集后异步调度。 直接遍历 root.firstPassiveEffect,收集后异步调度。
效率 每次提交需要多次完整遍历通用 Effect List 针对不同类型副作用,直接遍历对应的专用链表,效率更高。
并发支持 异步 useEffect 调度相对简单。 更精细的异步调度,由 Scheduler 模块管理,更好地支持并发更新和时间切片。
设计哲学 统一标记,通过位运算区分。 职责分离,专用列表,优化执行路径。

React 18 改进的优势

  1. 性能提升:避免了在提交阶段对单个大型 Effect List 进行多次全量遍历和位运算筛选。通过直接访问特定类型的副作用链表,减少了不必要的检查,提升了提交阶段的执行效率。
  2. 更清晰的职责分离:将不同类型的副作用(DOM 操作、同步生命周期、异步副作用)在内部数据结构上进行了明确的分离。这使得 React 核心代码更加模块化和易于维护。
  3. 强化并发模式
    • PassiveEffect(即 useEffect)的清理和执行被完全解耦并推迟到浏览器绘制之后,且由 Scheduler 模块更智能地管理。这意味着即使在优先级较高的更新发生时,useEffect 也可以被中断、延迟或重新排序,而不会阻塞用户交互或关键渲染路径。
    • 这种分离使得 useEffect 成为一个“非阻塞”的副作用,它不会影响用户感知的性能。
  4. 更好的可维护性和扩展性:新的 flags 系统为未来引入更多类型的副作用或优化提供了更大的灵活性。

总结与展望

从 React 16 的 effectTag 到 React 18 的细粒度 Flags 和多链表结构,我们看到 React 在副作用管理机制上的持续演进。这一演进的核心驱动力是为了更好地支持并发模式,让 React 能够以更精细、更高效的方式处理各种副作用,从而为用户提供更流畅、响应更快的交互体验。

React 18 对 Effect List 的优化不仅仅是内部实现细节的改变,它反映了 React 团队在平衡性能、并发性和开发体验方面的深思熟虑。通过将不同类型的副作用组织到独立的链表中,React 显著提升了提交阶段的效率,并为并发渲染的复杂性奠定了坚实的基础。理解这些底层机制,有助于我们更深入地掌握 React 的工作原理,并在开发中做出更优的决策。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注