Published on

lazy 内部机制

React.lazy() 是 React 框架提供的一个内置 API,用于实现组件的代码分割 (Code Splitting)。它允许开发者在不改变组件编写方式的前提下,以一种声明式的方式,按需加载组件代码。这不仅可以显著减小应用初始加载的包体积,优化首次内容绘制 (FCP),也使得管理异步加载状态变得更加优雅。

lazy() 的核心机制

返回一个特殊的 React 元素

React.lazy() 并非返回一个普通的函数组件,而是返回一个特殊的、内部数据结构经过特殊处理的对象。

React.lazy() 的返回类型

React.lazy() 返回一个带有 $$typeof: REACT_LAZY_TYPE 标识的特殊对象。这个对象不包含任何渲染逻辑,其内部的 _payload 属性包含了加载所需的一切信息,而 _init 方法则包含了实际的加载逻辑。

  • _payload: 存储了加载状态,其核心字段包括:
    • _status: 标记加载状态,如 0 (未初始化)、1 (加载中)、2 (已解析)、3 (已失败)。
    • _result: 存储了加载的产物,如模块的 Promise、解析后的组件或错误对象。
  • _init: 一个初始化方法。当它被调用时,会触发模块加载,并根据加载结果更新 _payload 中的 _status_result

异步加载的内部状态管理

React.lazy() 的机制巧妙地将加载过程的状态管理与 React 的渲染流程结合在一起。

协调过程中的特殊处理

React.lazy() 的设计并非仅仅是为了方便调用 import()。它与 Fiber 架构的深度集成,旨在实现类型安全和性能优化。

LazyComponent 的 Fiber 节点

当 React 在渲染阶段遇到 lazy() 返回的特殊对象时,它会通过 createFiberFromTypeAndProps 将其映射为一个专用的、tag 类型为 LazyComponent 的 Fiber 节点。

mountLazyComponent 的工作流

LazyComponent 类型的 Fiber 节点,由 mountLazyComponent() 函数专门处理。其工作流程如下:

  1. 从 Fiber 节点的 elementType(即 lazy() 返回的对象)中,取出 _payload_init
  2. 调用 _init(_payload) 触发模块加载。
    • 如果模块正在加载中,_init抛出一个 Promise,React 捕获这个 Promise 并触发 Suspense
    • 如果模块已加载,_init返回真实组件(例如,函数组件或类组件)。
  3. 一旦获得真实组件,mountLazyComponent 会将 Fiber 节点的 type 更新为该组件,并通过 resolveLazyComponentTag() 函数判定其类型(例如 FunctionComponent)。
  4. 最后,根据判定出的真实类型,调用相应的渲染逻辑(如 updateFunctionComponent),直接为真实组件创建其子 Fiber 节点。
性能优化:避免冗余的 Fiber 节点

这种“立即解析”的机制是原生 lazy() 的一项重要优化。一个自己封装的懒加载高阶组件,通常会创建一层额外的 Fiber 节点来管理状态,这增加了 Fiber 树的深度和渲染开销。而原生 lazy()LazyComponent 节点内部完成了所有的状态管理和加载,然后直接将该 Fiber 节点“变身”为真实组件的 Fiber 节点,避免了这层冗余,使得 Fiber 树更加紧凑高效。

优化与类型安全的考量

类型安全:确保组件是有效类型

React.lazy() 的内部逻辑会通过 resolveLazyComponentTag() 确保加载的模块导出了一个有效的组件类型(如函数组件、类组件、React.memoReact.forwardRef 包装的组件)。如果模块导出的是一个非法的类型(例如原始值),React 会抛出一个清晰的错误,这增强了应用的健壮性。

memo()forwardRef() 的特殊处理

React.lazy() 的内部机制能够识别出由 memo()forwardRef() 包装的特殊组件类型,并为其分配专门的 Fiber.tag。这种处理使得原生 lazy() 能够与 React 的核心优化和特性无缝协作,避免了开发者需要自行处理这些复杂情况。