您现在的位置是:网站首页> 编程资料编程资料

react  Suspense工作原理解析_React_

2023-05-24 347人已围观

简介 react  Suspense工作原理解析_React_

Suspense 基本应用

Suspense 目前在 react 中一般配合 lazy 使用,当有一些组件需要动态加载(例如各种插件)时可以利用 lazy 方法来完成。其中 lazy 接受类型为 Promise<() => {default: ReactComponet}> 的参数,并将其包装为 react 组件。ReactComponet 可以是类组件函数组件或其他类型的组件,例如:

 const Lazy = React.lazy(() => import("./LazyComponent"))  // lazy 包装的组件 

由于 Lazy 往往是从远程加载,在加载完成之前 react 并不知道该如何渲染该组件。此时如果不显示任何内容,则会造成不好的用户体验。因此 Suspense 还有一个强制的参数为 fallback,表示 Lazy 组件加载的过程中应该显示什么内容。往往 fallback 会使用一个加载动画。当加载完成后,Suspense 就会将 fallback 切换为 Lazy 组件的内容。一个完整的例子如下:

function LazyComp(){ console.info("sus", "render lazy") return "i am a lazy man" } function delay(ms){ return new Promise((resolve, reject) => { setTimeout(resolve, ms) }) } // 模拟动态加载组件 const Lazy = lazy(() => delay(5000).then(x => ({"default": LazyComp}))) function App() { const context = useContext(Context) console.info("outer context") return (  ) } 

这段代码定义了一个需要动态加载的 LazyComp 函数式组件。会在一开始显示 fallback 中的内容 loading,5s 后显示 i am a lazy man。

Suspense 原理

虽然说 Suspense 往往会配合 lazy 使用,但是 Suspense 是否只能配合 lazy 使用?lazy 是否又必须配合Suspense? 要搞清楚这两个问题,首先要明白 Suspense 以及 lazy 是在整个过程中扮演的角色,这里先给出一个简单的结论:

  • Suspense: 可以看做是 react 提供用了加载数据的一个标准,当加载到某个组件时,如果该组件本身或者组件需要的数据是未知的,需要动态加载,此时就可以使用 Suspense。Suspense 提供了加载 -> 过渡 -> 完成后切换这样一个标准的业务流程。
  • lazy: lazy 是在 Suspense 的标准下,实现的一个动态加载的组件的工具方法。

从上面的描述即可以看出,Suspense 是一个加载数据的标准,lazy 只是该标准下实现的一个工具方法。那么说明 Suspense 除配合了 lazy 还可以有其他应用场景。而 lazy 是 Suspense 标准下的一个工具方法,因此无法脱离 Suspense 使用。接下来通过 lazy + Suspense 方式来给大家分析具体原理,搞懂了这部分,我们利用 Suspense 实现自己的数据加载也不是难事。

基本流程

在深入了解细节之前,我们先了解一下 lazy + Suspense 的基本原理。这里需要一些 react 渲染流程的基本知识。为了统一,在后续将动态加载的组件称为 primary 组件,fallback 传入的组件称为 fallback 组件,与源码保持一致。

  • 当 react 在 beginWork 的过程中遇到一个 Suspense 组件时,会首先将 primary 组件作为其子节点,根据 react 的遍历算法,下一个遍历的组件就是未加载完成的 primary 组件。
  • 当遍历到 primary 组件时,primary 组件会抛出一个异常。该异常内容为组件 promise,react 捕获到异常后,发现其是一个 promise,会将其 then 方法添加一个回调函数,该回调函数的作用是触发 Suspense 组件的更新。并且将下一个需要遍历的元素重新设置为 Suspense,因此在一次 beginWork 中,Suspense 会被访问两次。
  • 又一次遍历到 Suspense,本次会将 primary 以及 fallback 都生成,并且关系如下:

虽然 primary 作为 Suspense 的直接子节点,但是 Suspense 会在 beginWork 阶段直接返回 fallback。使得直接跳过 primary 的遍历。因此此时 primary 必定没有加载完成,所以也没必要再遍历一次。本次渲染结束后,屏幕上会展示 fallback 的内容

  • 当 primary 组件加载完成后,会触发步骤 2 中 then,使得在 Suspense 上调度一个更新,由于此时加载已经完成,Suspense 会直接渲染加载完成的 primary 组件,并删除 fallback 组件。

这 4 个步骤看起来还是比较复杂。相对于普通的组件主要有两个不同的流程:

  • primary 会组件抛出异常,react 捕获异常后继续 beginWork 阶段。
  • 整个 beginWork 节点,Suspense 会被访问两次

不过基本逻辑还是比较简单,即是:

  • 抛出异常
  • react 捕获,添加回调
  • 展示 fallback
  • 加载完成,执行回调
  • 展示加载完成后的组件

整个 beginWork 遍历顺序为:

 Suspense -> primary -> Suspense -> fallback

源码解读 - primary 组件

整个 Suspend 的逻辑相对于普通流程实际上是从 primary 组件开始的,因此我们也从 react 是如何处理 primary 组件开始探索。找到 react 在 beginWork 中处理处理 primary 组件的逻辑的方法 mountLazyComponent,这里我摘出一段关键的代码:

 const props = workInProgress.pendingProps; const lazyComponent: LazyComponentType = elementType; const payload = lazyComponent._payload; const init = lazyComponent._init; let Component = init(payload); // 如果未加载完成,则会抛出异常,否则会返回加载完成的组件 

其中最关键的部分莫过于这个 init 方法,执行到这个方法时,如果没有加载完成就会抛出 Promise 的异常。如果加载完成就直接返回完成后的组件。我们可以看到这个 init 方法实际上是挂载到 lazyComponent._init 方法,lazyComponent 则就是 React.lazy() 返回的组件。我们找到 React.lazy() :

export function lazy( ctor: () => Thenable<{default: T, ...}>, ): LazyComponent> { const payload: Payload = { // We use these fields to store the result. _status: Uninitialized, _result: ctor, }; const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, _payload: payload, _init: lazyInitializer, }; 

这里的 lazyType 实际上就是上面的 lazyComponent。那么这里的 _init 实际上来自于另一个函数 lazyInitializer:

function lazyInitializer(payload: Payload): T { if (payload._status === Uninitialized) { console.info("sus", "payload status", "Uninitialized") const ctor = payload._result; const thenable = ctor(); // 这里的 ctor 就是我们返回 promise 的函数,执行之后得到一个加载组件的 promise // 加载完成后修改状态,并将结果挂载到 _result 上 thenable.then( moduleObject => { if (payload._status === Pending || payload._status === Uninitialized) { // Transition to the next state. const resolved: ResolvedPayload = (payload: any); resolved._status = Resolved; resolved._result = moduleObject; } }, error => { if (payload._status === Pending || payload._status === Uninitialized) { // Transition to the next state. const rejected: RejectedPayload = (payload: any); rejected._status = Rejected; rejected._result = error; } }, ); 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; console.info("sus", "get lazy resolved result") return moduleObject.default; // 注意这里返回的是 moduleObject.default 而不是直接返回 moduleObject } else { // 否则抛出异常 console.info("sus, raise a promise", payload._result) throw payload._result; } } 

因此执行这个方法大致可以分为两个状态:

  • 未加载完成时抛出异常
  • 加载完成后返回组件

到这里,整个 primary 的逻辑就搞清楚了。下一步则是搞清楚 react 是如何捕获并且处理异常的。

源码解读 - 异常捕获

react 协调整个阶段都在 workLoop 中执行,代码如下:

 do { try { workLoopSync(); break; } catch (thrownValue) { handleError(root, thrownValue); } } while (true); 

可以看到 catch 了 error 后,整个处理过程在 handleError 中完成。当然,如果是如果 primary 组件抛出的异常,这里的 thrownValue 就为一个 priomise。在 handleError 中有这样一段相关代码:

throwException( root, erroredWork.return, erroredWork, thrownValue, workInProgressRootRenderLanes, ); completeUnitOfWork(erroredWork); 

核心代码需要继续深入到 throwException:

// 首先判断是否是为 promise if ( value !== null && typeof value === 'object' && typeof value.then === 'function' ) { const wakeable: Wakeable = (value: any); resetSuspendedComponent(sourceFiber, rootRenderLanes); // 获取到 Suspens 父组件 const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber); if (suspenseBoundary !== null) { suspenseBoundary.flags &= ~ForceClientRender; // 给 Suspens 父组件 打上一些标记,让 Suspens 父组件知道已经有异常抛出,需要渲染 fallback 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. if (suspenseBoundary.mode & ConcurrentMode) { attachPingListener(root, wakeable, rootRenderLanes); } // 将抛出的 promise 放入Suspens 父组件的 updateQueue 中,后续会遍历这个 queue 进行回调绑定 attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes); return; } } 

可以看到 throwException 逻辑主要是判断抛出的异常是不是 promise,如果是的话,就给 Suspens 父组件打上 ShoulCapture 的 flags,具体用处下面会讲到。并且把抛出的 promise 放入 Suspens 父组件的 updateQueue 中。

throwException 完成后会执行一次 completeUnitOfWork,根据 ShoulCapture 打上 DidCapture 的 flags。 并将下一个需要遍历的节点设置为 Suspense,也就是下一次遍历的对象依然是 Suspense。这也是之前提到的 Suspens 在整个 beginWork 阶段会遍历两次

源码解读 - 添加 promise 回调

在 Suspense 的 update queue 中,在 commit 阶段会遍历这个 updateQueue 添加回调函数,该功能在 commitMutationEffectsOnFiber 中。找到关于 Suspense 的部分,会有以下代码:

 if (flags & Update) { try { commitSuspenseCallback(finishedWork); } catch (error) { captureCommitPhaseError(finishedWork, finishedWork.return, error); } attachSuspenseRetryListeners(finishedWork); } return; 

主要逻辑在 attachSuspenseRetryListeners 中:

function attachSuspenseRetryListeners(finishedWork: Fiber) { const wakeables: Set | null = (finishedWork.updateQueue: any); if (wakeables !== null) { finishedWork.updateQueue = null; let retryCache = finishedWork.stateNode; if (retryCache === null) { retryCache = finishedWork.stateNode = new PossiblyWeakSet(); } wakeables.forEach(wakeable => { // Memoize using the boundary fiber to prevent redundant listeners. const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); // 判断一下这个 promise 是否已经绑定过一次了,如果绑定过则可以忽略 if (!retryCache.has(wakeable)) { retryCache.add(wakeable); i
                
                

-六神源码网