Suspense 的核心概念与 error boundaries 非常相似,error boundaries 在 React 16 中引入,允许在应用程序内的任何位置捕获未捕获的异常,然后在组件树中展示跟错误信息相关的组件。以同样的方式,Suspense 组件从其子节点捕获任何抛出的Promises,不同之处在于对于 Suspense 我们不必使自定义组件充当边界,Suspense 组件就是那个边界;而在 error boundary中,我们需要为边界组件定义(componentDidCatch)方法。--摘抄此处
使用的 suspence 组件就是 REACT_SUSPENSE_TYPE(Symbol.for('react.suspense')),就是指定的一种特殊节点类型,和 div 是一个意思。
在 createFiberFromTypeAndProps 方法创建节点类型时,找到 REACT_SUSPENSE_TYPE 类型,执行 createFiberFromSuspense 创建 suspence 类型的 fiber 节点:
export function createFiberFromSuspense(
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTime,
key: null | string,
) {
const fiber = createFiber(SuspenseComponent, pendingProps, key, mode);
fiber.type = REACT_SUSPENSE_TYPE;
fiber.elementType = REACT_SUSPENSE_TYPE;
fiber.expirationTime = expirationTime;
return fiber;
}
回顾:节点类型的创建,在初始只会创建出根节点的 fiber,后续的创建在 beginWork 入口,进入 reconcile 过程,会判断节点可复用性,然后不能复用的就通过 createFiberFromTypeAndProps 创建新节点。
suspense 组件类型执行大致过程如下:
beginWork
|
updateSuspenseComponent
|
...
mountChildFibers
...
|
reconcileChildFibers
在 completeWork 中针对 SuspenseComponent 组件,执行的操作主要是进行是否需要更新标记:
workInProgress.effectTag |= Update;
举例:
export default () => (
// 这个组件内一些逻辑会 throw promise
)
beginWork 中的 SuspenseComponent 分支在 fallback 数据渲染之前会执行两次,原因在下面 handleError 方法中提及。
第一次执行
workInProgress.child = mountChildFibers 就是单纯的创建出子节点
第二次
fallbackChildFragment -- fallback 对应的内容
primaryChildFragment -- suspense 节点创建的 return--> 同时 wip(suspense) 还保留着
创建关系
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
return fallbackChildFragment
两次流程之后,界面初始出现传入的 fallback 的内容,然后进入对子节点的处理过程,等待组件的数据加载成功后被重新调度,渲染新数据。
通过 suspense 包裹的组件中,如果这个子组件有 throw 的逻辑,则会进入 catch 的过程:
try {
return beginWork(current, unitOfWork, expirationTime);
} catch (originalError) {
if (originalError !== null
&& typeof originalError === 'object'
&& typeof originalError.then === 'function'
) {
throw originalError;
}
...
}
在这里抛出错误之后,回退到 performSyncWorkOnRoot 中:
function performSyncWorkOnRoot (){
...
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
...
}
继续执行 handleError :
function handleError(root, thrownValue) {
do {
try {
...
// 继续执行
throwException(...);
// 这里完成时 会将wip设置为自己的父节点 也就是 suspense 节点
workInProgress = completeUnitOfWork(workInProgress);
} catch (yetAnotherThrownValue) {
...
continue
}
// Return to the normal work loop.
return;
} while (true);
}
错误机制中的 completeUnitOfWork 会将 wip 设置为父节点,在这里的场景是回到 suspense 节点,所以在 beginWork 那里断点时会发现连续两次都是进入 SuspenseComponent 分支。
继续执行 throwException,这里会将抛出的 promise 放入子组件的 updateQueue:
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
renderExpirationTime: ExpirationTime,
) {
...
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
// This is a thenable.
const thenable: Thenable = (value: any);
...
do {
if (
workInProgress.tag === SuspenseComponent &&
shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
) {
// 一个 set 结构存储在 updateQueue
const thenables: Set = (workInProgress.updateQueue: any);
if (thenables === null) {
const updateQueue = (new Set(): any);
updateQueue.add(thenable);
// 第一次新增
workInProgress.updateQueue = updateQueue;
} else {
// 追加
thenables.add(thenable);
}
...
// 同步设置
sourceFiber.expirationTime = Sync;
return;
}
...
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
...
}
接下来就是如何执行这些更新。
commitWork 方法中进入 SuspenseComponent ,最终要重新调度:
commitWork
|
SuspenseComponent
|
attachSuspenseRetryListeners
|
thenables.forEach
|
retryTimedOutBoundary
|
retryTimedOutBoundary
|
ensureRootIsScheduled 开始调度
主要是在这个阶段拿到这些抛出的 promise 对象并执行:
function attachSuspenseRetryListeners(finishedWork: Fiber) {
...
const thenables: Set | null = (finishedWork.updateQueue: any);
if (thenables !== null) {
// 置空队列
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
// 缓存优化
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
thenables.forEach(thenable => {
// 拿到 调度的 回调,等待组件中 resolve 之后 重新调度
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
if (!retryCache.has(thenable)) {
retryCache.add(thenable);
// resolve 之后执行,传入调度的回调
thenable.then(retry, retry);
}
});
}
}
function retryTimedOutBoundary(...) {
...
ensureRootIsScheduled(root);
...
}
等待组件中数据加载成功重新发起调度,再次进入 beginWork--SuspenseComponent,此时直接做的事情就是 reconcileChildFibers 过程,并清空 memoizedState:
...
primaryChild = reconcileChildFibers()
workInProgress.memoizedState = null;
return workInProgress.child = primaryChild;
...
此时在 completeWork 中针对 SuspenseComponent 组件,会对之前渲染的 fallback 组件标记删除,对新的渲染数据标记更新。
对 suspense 的理解目前就这么多吧,其中的很多细节没有深入研究,其背后产生的原理依据 algebrac effects(代数效应)。
通过上面的理解,再来看 lazy 就很简单了。
export function lazy(ctor: () => Thenable): LazyComponent {
let lazyType = {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null,
};
return lazyType;
}
beginWork 中对应 mountLazyComponent:
let Component = readLazyComponentType(elementType);
// type 和 tag 一并修改掉
workInProgress.type = Component;
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
核心是 readLazyComponentType 方法来读取传入的组件状态是否加载完毕:
export function readLazyComponentType(lazyComponent: LazyComponent): T {
initializeLazyComponentType(lazyComponent);
// 没有加载完毕 直接抛出 和自己写抛出错误是一样的
if (lazyComponent._status !== Resolved) {
throw lazyComponent._result;
}
// 加载完毕就返回一个正确的结果
return lazyComponent._result;
}
看下这个 _result 怎么来的:
export function initializeLazyComponentType(
lazyComponent: LazyComponent,
): void {
if (lazyComponent._status === Uninitialized) {
lazyComponent._status = Pending;
// 这个就是传入的 () => import(...) 编译后就是一个thenable 对象
const ctor = lazyComponent._ctor;
const thenable = ctor();
// 赋值
lazyComponent._result = thenable;
thenable.then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
lazyComponent._status = Resolved;
// 成功
lazyComponent._result = defaultExport;
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
// 失败
lazyComponent._result = error;
}
},
);
}
}
lazy 原理和直接在组件中抛出一个 thenable 一样。
更多 react 源码请点击这里更多源码