Appearance
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;
}
}- 状态切换
其借助于 Promise.then() 方法来实现异步加载组件的功能。并将组件加载的状态保存到 _payload 中。具体如下
- Uninitialized => 组件未初始化状态
- Pending => 组件加载中状态
- Resolved => 组件加载完成状态
- Rejected => 组件加载失败状态
这样就可以根据 payload._status 去判断组件的加载状态,从而渲染组件。
- 回调函数格式要求
因为其主要是针对于 esm 模块中组件的异步加载,所以其对于React.lazy( () => import('./MyComponent') ) 的回调函数具有格式要求。
- 必须是一个函数,且返回值是一个 Promise 对象。
- Promise 对象的 resolve 函数的参数必须是一个对象,且 对象中必须有 default 属性,且 default 属性的值必须是一个组件。
- 返回值
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 的标记,并通过 markSuspenseBoundaryShouldCapture将 Suspense组件的副作用添加 ShouldCapture 标记,这样在 completeUnitOfWork对于Suspense组件的 unwindWork流程会根据 ShouldCapture 标记 切换成 DidCapture标记。
另外在 Concurrent 模式中,Suspense 能够在数据获取时显示回退(fallback)内容,等数据加载完成后再渲染组件。
重点 :
fiberNode.flags != ShouldCapture。- 通过
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 = "";
}可以看出加载成功后主要做以下流程
将 lazy 组件的构造函数 存储在 type 上
获取 lazy 组件的 tag
处理组件的 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 传递给真正的组件去初始化
- 根据组件的 tag 去调用各自的
beginWork方法渲染
