Skip to content

React.lazy()

组件对象

jsx
const lazyType: LazyComponent<T, Payload<T>> = {
  // 组件的类型
  $$typeof: REACT_LAZY_TYPE,
  _payload: {
    // Lazy 组件的状态
    _status: Uninitialized,
    // 组件的构造函数
    _result: ctor,
  },
  _init: lazyInitializer,
};

其中 lazyInitializer 方法会在组件渲染时执行,根据传入的 _payload 中的数据去加载组件。

js
function lazyInitializer<T>(payload: Payload<T>): T {
  // 组件未初始化状态
  if (payload._status === Uninitialized) {
    // 获取组件的构造函数 () => import('./MyComponent')
    const ctor = payload._result;
    // 获取组件的构造函数的执行结果
    const thenable = ctor();
    thenable.then(
      // 组件加载成功
      (moduleObject) => {
        // 将组件的加载状态修改成加载成功,并将组件结果保存到 payload._result 中
        if (payload._status === Pending || payload._status === Uninitialized) {
          // Transition to the next state.
          const resolved: ResolvedPayload<T> = (payload: any);
          resolved._status = Resolved;
          resolved._result = moduleObject;
        }
      },
      // 组件加载失败
      (error) => {
        // 将组件的加载状态修改成加载失败,并将错误信息保存到 payload._result 中
        if (payload._status === Pending || payload._status === Uninitialized) {
          // Transition to the next state.
          const rejected: RejectedPayload = (payload: any);
          rejected._status = Rejected;
          rejected._result = error;
        }
      }
    );
    // 将组件的加载状态修改成加载中, 并将Promise对象保存到 payload._result 中
    if (payload._status === Uninitialized) {
      // In case, we're still uninitialized, then we're waiting for the thenable
      // to resolve. Set it as pending in the meantime.
      const pending: PendingPayload = (payload: any);
      pending._status = Pending;
      pending._result = thenable;
    }
  }
  // 组件加载完成状态
  if (payload._status === Resolved) {
    // 获取组件的结果
    const moduleObject = payload._result;

    // 返回 _result.default 字段,即组件的构造函数, 这样 React 就可以渲染组件了
    return moduleObject.default;
  } else {
    throw payload._result;
  }
}
  1. 状态切换

其借助于 Promise.then() 方法来实现异步加载组件的功能。并将组件加载的状态保存到 _payload 中。具体如下

  • Uninitialized => 组件未初始化状态
  • Pending => 组件加载中状态
  • Resolved => 组件加载完成状态
  • Rejected => 组件加载失败状态

这样就可以根据 payload._status 去判断组件的加载状态,从而渲染组件。

  1. 回调函数格式要求

因为其主要是针对于 esm 模块中组件的异步加载,所以其对于React.lazy( () => import('./MyComponent') ) 的回调函数具有格式要求。

  • 必须是一个函数,且返回值是一个 Promise 对象。
  • Promise 对象的 resolve 函数的参数必须是一个对象,且 对象中必须有 default 属性,且 default 属性的值必须是一个组件。
  1. 返回值

lazyInitializer 方法的返回值非常特殊,只有在组件加载完成时才会通过 return 返回组件的构造函数。对于组件初始化、加载中、加载失败状态都是返回的一个throw payload._result 异常,这是为什么?带着这个问题,我们去看 Lazy 组件的渲染流程。

Fiber 对象

jsx
{
  tag: LazyComponent , // 11
  type : lazyType 对象 ,  // { $$typeof: REACT_LAZY_TYPE, _payload: { _status: Uninitialized, _result: ctor }, _init: lazyInitializer }
}

render 阶段

对于 Lazy 组件的渲染流程,其仍然是在 beginWork 阶段进行的, 通过 case : LazyComponent 判断当前组件为 Lazy 组件,然后进入其 mountLazyComponent流程。

我们先简单看一部分源码

js
function mountLazyComponent(
  _current,
  workInProgress,
  elementType,
  renderLanes
) {
  resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);

  const props = workInProgress.pendingProps;
  const lazyComponent: LazyComponentType<any, any> = elementType;
  // 获取 lazy 组件的 payload 和 init 方法
  const payload = lazyComponent._payload;
  const init = lazyComponent._init;
  // 初始化 lazy 组件
  // 重点: 在调用的时候,如果是初始化或者加载中状态的时候 lazyInitializer 返回的是一个 throw 异常,
  // 那么下面的代码就不会执行了, 而是会执行 workLoopConcurrent 的 catch 方法,然后进入 completeWork 阶段
  let Component = init(payload);
  // --------------------------------------
  //  执行下面的前置条件就是  lazyInitializer 方法加载成功
  //     没有成功(加载之前)的时候都通过 throw 异常,跳出了
  // --------------------------------------
  // Store the unwrapped component in the type.
  // 将 lazy 组件的构造函数 存储在 type 上
  workInProgress.type = Component;
  // 获取 lazy 组件的 tag
  const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
  //
  const resolvedProps = resolveDefaultProps(Component, props);
}

其中 init(payload) 方法就是执行 lazyInitializer 方法去加载组件,那么这应该是一个异步的过程,那为什么通过同步的方式去执行的时候,就直接在下面获取组件的构造函数、类型等信息了,组件不是应该还没有加载完成吗?

这就涉及到上面 lazyInitializer 方法的返回值了。对于除了 Resolved 状态之外的其他状态,都是通过 throw payload._result 抛出异常的,这样就不会执行下面的代码了,而是会进入 workLoopConcurrent 的 catch 方法,然后进入 completeWork 阶段。

具体如下

js
do {
  try {
    workLoopConcurrent();
    break;
  } catch (thrownValue) {
    // 捕获异常
    // 如 1. 用户组件渲染阶段的异常
    //  2. lazy 组件加载阶段的异常
    handleError(root, thrownValue);
  }
} while (true);

这时候进入第一次的异常捕获阶段,即 handleError的流程

异常捕获

js
function handleError(root, thrownValue): void {
  do {
    let erroredWork = workInProgress;
    try {
      throwException(
        root,
        erroredWork.return,
        erroredWork,
        thrownValue,
        workInProgressRootRenderLanes
      );
      completeUnitOfWork(erroredWork);
    } catch (yetAnotherThrownValue) {
      continue;
    }
    return;
  } while (true);
}

其核心就是 两个流程

throwException的流程

在这个流程中通过对于捕获异常值的类型(value !== null && typeof value === 'object' &&typeof value.then === 'function') 从而判断出来当前的错误其实不是真正的异常,而是 Lazy 组件非加载成功状态的处理流程。

那么在这个流程里面其主要做了什么?

React.lazy()一般是需要和 Suspense组件配合使用的,那么这时候就通过node.return向上遍历的方式去找到 第一个可以接受状态的Suspense组件, 这有两个条件 1. 祖先节点中的第一个 2. 可以接受状态 。 所以下面就分为两种状态:

存在Suspense组件

js
if (suspenseBoundary !== null) {
  suspenseBoundary.flags &= ~ForceClientRender;
  // 核心就是将 捕获异常的 Suspense 组件的副作用中标记 shouldCapture
  markSuspenseBoundaryShouldCapture(
    suspenseBoundary,
    returnFiber,
    sourceFiber,
    root,
    rootRenderLanes
  );
  // We only attach ping listeners in concurrent mode. Legacy Suspense always
  // commits fallbacks synchronously, so there are no pings.
  // 对于 并发模式: 使用的是 Ping监听器,即在异步状态完成的时候回调 协调流程
  if (suspenseBoundary.mode & ConcurrentMode) {
    attachPingListener(root, wakeable, rootRenderLanes);
  }
  attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes);
  return;
}

这时候一方面通过 suspenseBoundary.flags &= ~ForceClientRender 清除 Suspense 组件可以渲染 children 的标记,并通过 markSuspenseBoundaryShouldCaptureSuspense组件的副作用添加 ShouldCapture 标记,这样在 completeUnitOfWork对于Suspense组件的 unwindWork流程会根据 ShouldCapture 标记 切换成 DidCapture标记。

另外在 Concurrent 模式中,Suspense 能够在数据获取时显示回退(fallback)内容,等数据加载完成后再渲染组件。

重点

  1. fiberNode.flags != ShouldCapture
  2. 通过 Promise.then 方法在 Lazy 组件加载完成后,触发 Promise.then 方法的回调,从而在完成时触发新的协调流程。

不存在 Suspense组件

js
// 如果当前不是一个同步更新,这是可以的。
if (!includesSyncLane(rootRenderLanes)) {
  // 监听 wakeable 的 then的回调,从而在完成时触发新的协调流程
  attachPingListener(root, wakeable, rootRenderLanes);
  // 标记 workInProgressRootExitStatus 和 缓存当前渲染lanes 到 root.suspendLanes 中
  renderDidSuspendDelayIfPossible();
  return;
}

completeUnitOfWork的流程

虽然 Lazy 组件存在未完成的工作,且抛出了错误,但是其本身的 completeUnitOfWork流程是需要执行的。

这时候有一个比较重点的点就是: (completedWork.flags & Incomplete) === NoFlags 因为组件有未完成的工作,其不会进行 completeWork 流程,而是进入了 unwindWork 流程。

这边我们直接进入 unwindWork 流程的 LazyComponent 流程, 这个流程未 null ,没有什么操作,但是 Lazy 组件是配合 Suspense 组件配合使用的,所以会进入 unwindWork 流程的 SuspenseComponent 流程。

js
switch (workInProgress.tag) {
  // 进入 Suspense 组件
  case SuspenseComponent: {
    popSuspenseContext(workInProgress);

    const flags = workInProgress.flags;
    // Suspense 组件包含 lazy 等向上冒泡的异常副作用(ShouldCapture)
    if (flags & ShouldCapture) {
      // 清除 ShouldCapture 标记,并添加 DidCapture 标记
      workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
      return workInProgress;
    }
    return null;
  }
}
  • popSuspenseContext(workInProgress) 退出 Suspense 上下文
  • 存在 ShouldCapture 标记副作用的,清除 ShouldCapture 标记,并添加 DidCapture 标记(很重要)

render 阶段(加载中)

在初次渲染阶段,Lazy 组件进入 Uninitialized 流程并创建了 Promise 异步回调方法,并将状态修改在 Pending

js
// 将组件的加载状态修改成加载中, 并将Promise对象保存到 payload._result 中
if (payload._status === Uninitialized) {
  // In case, we're still uninitialized, then we're waiting for the thenable
  // to resolve. Set it as pending in the meantime.
  const pending: PendingPayload = (payload: any);
  pending._status = Pending;
  pending._result = thenable;
}

那么在再次渲染的时候,发现 payload._status === Pending ,那么再次执行 mountLazyComponent()的时候 init(payload) 不会走 Promise 的流程了,而是直接触发异常错误throw payload._result,进入 handleError 异常处理流程。

异常捕获

render 阶段(加载完成)

等组件加载完成,触发新的一轮协调渲染流程,这时候 resolved._status = Resolved; 阶段的组件加载状态变成 Resolved,所以再次执行 mountLazyComponent()的时候 init(payload) 不会走 Promise 的流程,而是直接走的 return payload._result.default,这样就保证虽然组件再次执行了,但是不会重新发送请求,而是直接获取组件的构造函数,并且因为没有报错,所以会走入下一步 的流程

jsx
function mountLazyComponent(
  _current,
  workInProgress,
  elementType,
  renderLanes
) {
  let Component = init(payload);
  // --------------------------------------
  //  执行下面的前置条件就是  lazyInitializer 方法加载成功
  //     没有成功(加载之前)的时候都通过 throw 异常,跳出了
  // --------------------------------------
  // Store the unwrapped component in the type.
  // 将 lazy 组件的构造函数 存储在 type 上
  workInProgress.type = Component;
  // 获取 lazy 组件的 tag
  const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
  // 处理组件的 props
  // 将挂载在Lazy组件上的 props,传给组件本身并进行默认值处理
  const resolvedProps = resolveDefaultProps(Component, props);
  // 按照组件的类型 分别执行各自的 beginWork 流程
  //  函数组件的 => updateFunctionComponent
  //  Class组件 => updateClassComponent
  // ....
  let child;
  switch (resolvedTag) {
    case FunctionComponent: {
      if (__DEV__) {
        validateFunctionComponentInDev(workInProgress, Component);
        workInProgress.type = Component =
          resolveFunctionForHotReloading(Component);
      }
      child = updateFunctionComponent(
        null,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes
      );
      return child;
    }
    case ClassComponent: {
      child = updateClassComponent(
        null,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes
      );
      return child;
    }
    case ForwardRef: {
      child = updateForwardRef(
        null,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes
      );
      return child;
    }
    case MemoComponent: {
      child = updateMemoComponent(
        null,
        workInProgress,
        Component,
        resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
        renderLanes
      );
      return child;
    }
  }
  let hint = "";
}

可以看出加载成功后主要做以下流程

  1. 将 lazy 组件的构造函数 存储在 type 上

  2. 获取 lazy 组件的 tag

  3. 处理组件的 props,将挂载在 Lazy 组件上的 props,传给组件本身并进行默认值处理

tsx
import { Suspense, lazy } from 'react'

const DemoA = lazy(() => import('./demo/a'))
//
<DemoA path="/demoA" name="1" />

如上面例子,因为 Lazy 组件使用的时候其 props 是存放在 Lazy 组件上的,而不是真正组件上的,所以这时候需要将 Lazy 组件上的 props 传递给真正的组件去初始化

  1. 根据组件的 tag 去调用各自的 beginWork方法 渲染