React | 2020-08-25 22:33:02 1136次 1次
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 创建新节点。
二、render阶段
suspense 组件类型执行大致过程如下:
beginWork | updateSuspenseComponent | ... mountChildFibers ... | reconcileChildFibers
在 completeWork 中针对 SuspenseComponent 组件,执行的操作主要是进行是否需要更新标记:
workInProgress.effectTag |= Update;
举例:
export default () => ( <Suspense maduation={1000} fallback="loading data"> <SuspenseComp /> // 这个组件内一些逻辑会 throw promise </Suspense> )
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 子节点处理
通过 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<Thenable> = (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); } ... }
接下来就是如何执行这些更新。
三、commit阶段
commitWork 方法中进入 SuspenseComponent ,最终要重新调度:
commitWork | SuspenseComponent | attachSuspenseRetryListeners | thenables.forEach | retryTimedOutBoundary | retryTimedOutBoundary | ensureRootIsScheduled 开始调度
主要是在这个阶段拿到这些抛出的 promise 对象并执行:
function attachSuspenseRetryListeners(finishedWork: Fiber) { ... const thenables: Set<Thenable> | 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原理
通过上面的理解,再来看 lazy 就很简单了。
声明
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> { 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<T>(lazyComponent: LazyComponent<T>): T { initializeLazyComponentType(lazyComponent); // 没有加载完毕 直接抛出 和自己写抛出错误是一样的 if (lazyComponent._status !== Resolved) { throw lazyComponent._result; } // 加载完毕就返回一个正确的结果 return lazyComponent._result; }
看下这个 _result 怎么来的:
export function initializeLazyComponentType( lazyComponent: LazyComponent<any>, ): 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 一样。
1人赞