关于react.js:reactSuspense工作原理分析

37次阅读

共计 13301 个字符,预计需要花费 34 分钟才能阅读完成。

Suspense 根本利用
Suspense 目前在 react 中个别配合 lazy 应用,当有一些组件须要动静加载(例如各种插件) 时能够利用 lazy 办法来实现。其中 lazy 承受类型为 Promise<() => {default: ReactComponet}> 的参数,并将其包装为 react 组件。ReactComponet 能够是类组件函数组件或其余类型的组件,例如:
const Lazy = React.lazy(() => import(“./LazyComponent”))
<Suspense fallback={“loading”}>

    <Lazy/> // lazy 包装的组件

</Suspense>
因为 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 (

  <Suspense fallback={"loading"}>
    <Lazy/>
  </Suspense>

)
}
这段代码定义了一个须要动静加载的 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<any, any> = 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<T>(
ctor: () => Thenable<{default: T, …}>,
): LazyComponent<T, Payload<T>> {
const payload: Payload<T> = {

// We use these fields to store the result.
_status: Uninitialized,
_result: ctor,

};

const lazyType: LazyComponent<T, Payload<T>> = {

$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer,

};
这里的 lazyType 实际上就是下面的 lazyComponent。那么这里的 _init 实际上来自于另一个函数 lazyInitializer:
function lazyInitializer<T>(payload: Payload<T>): 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<T> = (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 实战视频解说:进入学习
源码解读 – 异样捕捉
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<Wakeable> | 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);

    if (enableUpdaterTracking) {if (isDevToolsPresent) {if (inProgressLanes !== null && inProgressRoot !== null) {
          // If we have pending work still, associate the original updaters with it.
          restorePendingUpdaters(inProgressRoot, inProgressLanes);
        } else {
          throw Error('Expected finished root and lanes to be set. This is a bug in React.',);
        }
      }
    }
    // 将 retry 绑定 promise 的 then 回调
    wakeable.then(retry, retry);
  }
});

}
}
attachSuspenseRetryListeners 整个逻辑就是绑定 promise 回调,并将绑定后的 promise 放入缓存,免得反复绑定。这里绑定的回调为 resolveRetryWakeable.bind(null, finishedWork, wakeable),在这个办法中又调用了 retryTimedOutBoundary 办法:
if (retryLane === NoLane) {

// TODO: Assign this to `suspenseState.retryLane`? to avoid
// unnecessary entanglement?
retryLane = requestRetryLane(boundaryFiber);

}
// TODO: Special case idle priority?
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
if (root !== null) {

markRootUpdated(root, retryLane, eventTime);
ensureRootIsScheduled(root, eventTime);

}
看到 markUpdateLaneFromFiberToRoot 逻辑就比拟清晰了,即在 Suspense 的组件上调度一次更新。也就是说,当动静组件的申请实现后,会执行 resolveRetryWakeable -> retryTimedOutBoundary,并且最终让 Suspense 进行一次更新。
源码解读 -Suspense
之所以是将 Suspense 放在最初来剖析,是因为对 Suspense 的解决波及到多个状态,这些状态在之前的步骤中或者会被批改,因而在理解其余步骤之后再来看 Suspense 或者更容易了解。对于 Suspense 来说,在 workLoop 中可能会有 3 种不同的解决形式。每一次 beginWork Suspense 又会被拜访两次,在源码中称为 first pass 和 second pass。这两次会依据在 Suspense 的 flags 上是否存在 DidCapture 来进行不同操作。整个解决逻辑都在 updateSuspenseComponent 中。
首次渲染
beginWork – first pass,此时 DidCapture 不存在,Suspense 将 primary 组件作为子节点,拜访子节点后会抛出异样。catch 时会设置 DidCapture 到 flags 上。对应的函数为 mountSuspensePrimaryChildren:
function mountSuspensePrimaryChildren(
workInProgress, primaryChildren, renderLanes,
) {
const mode = workInProgress.mode;
const primaryChildProps: OffscreenProps = {

mode: 'visible',
children: primaryChildren,

};
const primaryChildFragment = mountWorkInProgressOffscreenFiber(

primaryChildProps,
mode,
renderLanes,

);
primaryChildFragment.return = workInProgress;
workInProgress.child = primaryChildFragment; // 子节点为 primaryChildFragment,下一次拜访会抛出异样
return primaryChildFragment;
}
beginWork – second pass,因为此时 DidCapture 存在,会将 primary 组件作为子节点,并将 fallback 组件作为 primary 组件的兄弟节点。然而间接返回 primary 组件,跳过 fallback 组件。对应的函数为 mountSuspenseFallbackChildren:
function mountSuspenseFallbackChildren(
workInProgress, primaryChildren, fallbackChildren, renderLanes,
) {
const mode = workInProgress.mode;
const progressedPrimaryFragment: Fiber | null = workInProgress.child;

const primaryChildProps: OffscreenProps = {

mode: 'hidden',
children: primaryChildren,

};

let primaryChildFragment;
let fallbackChildFragment;

primaryChildFragment.return = workInProgress;
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment; // 留神这里的子节点是 primaryChildFragment
return fallbackChildFragment; // 但返回的却是 fallbackChildFragment,目标是为了跳过 primaryChild 的遍历
}
commit: 将挂载到 updateQueue 上的 promise 绑定回调
primary 组件加载实现前的渲染
在首次渲染以及 primary 组件加载实现的期间,还可能会有其余组件更新而触发触发渲染,其逻辑为:
beginWork – first pass – DidCapture 不存在: 将 primary 组件作为子节点,如果 fallback 组件存在,则将其增加到 Suspense 组件的 deletions 中。拜访子节点后会抛出异样。catch 时会设置 DidCapture 到 flags 上。对应的函数为 updateSuspensePrimaryChildren:
function updateSuspensePrimaryChildren(
current, workInProgress, primaryChildren, renderLanes,
) {
const currentPrimaryChildFragment: Fiber = (current.child: any);
const currentFallbackChildFragment: Fiber | null =

currentPrimaryChildFragment.sibling;

const primaryChildFragment = updateWorkInProgressOffscreenFiber(

currentPrimaryChildFragment,
{
  mode: 'visible',
  children: primaryChildren,
},

);
if ((workInProgress.mode & ConcurrentMode) === NoMode) {

primaryChildFragment.lanes = renderLanes;

}
primaryChildFragment.return = workInProgress;
primaryChildFragment.sibling = null;
// 如果 currentFallbackChildFragment 存在,须要增加到 deletions 中
if (currentFallbackChildFragment !== null) {

const deletions = workInProgress.deletions;
if (deletions === null) {workInProgress.deletions = [currentFallbackChildFragment];
  workInProgress.flags |= ChildDeletion;
} else {deletions.push(currentFallbackChildFragment);
}

}

workInProgress.child = primaryChildFragment;
return primaryChildFragment;
}
beginWork – second pass – DidCapture 存在: 将 primary 组件作为子节点,将 fallback 组件作为 primary 组件的兄弟节点。并且革除 deletions。因为此时 primary 组件还未加载实现,所以须要确保 fallback 组件不会被删除。对于的函数为:
function updateSuspenseFallbackChildren(
current, workInProgress, primaryChildren, fallbackChildren, renderLanes,
) {
const progressedPrimaryFragment: Fiber = (workInProgress.child: any);

primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childLanes = NoLanes;
primaryChildFragment.pendingProps = primaryChildProps;

if (enableProfilerTimer && workInProgress.mode & ProfileMode) {

  primaryChildFragment.actualDuration = 0;
  primaryChildFragment.actualStartTime = -1;
  primaryChildFragment.selfBaseDuration =
    currentPrimaryChildFragment.selfBaseDuration;
  primaryChildFragment.treeBaseDuration =
    currentPrimaryChildFragment.treeBaseDuration;
}

// 革除 deletions,确保 fallback 能够展现
workInProgress.deletions = null;
let fallbackChildFragment;
if (currentFallbackChildFragment !== null) {
fallbackChildFragment = createWorkInProgress(
  currentFallbackChildFragment,
  fallbackChildren,
);

} else {

fallbackChildFragment = createFiberFromFragment(
  fallbackChildren,
  mode,
  renderLanes,
  null,
);
fallbackChildFragment.flags |= Placement;

}

fallbackChildFragment.return = workInProgress;
primaryChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment; // 同样的操作,workInProgress.child 为 primaryChildFragment
return fallbackChildFragment; // 然而返回 fallbackChildFragment
}
commit: 革除 DidCapture。
primary 组件加载实现时的渲染
加载实现之后会触发 Suspense 的更新,此时为:
beginWork – first pass – DidCapture 不存在: 将 primary 组件作为子节点,如果 fallback 组件存在,则将其增加到 Suspense 组件的 deletions 中。因为此时 primary 组件加载实现,拜访子节点不会抛出异样。解决的函数同样为 updateSuspensePrimaryChildren,这里就不再贴出来。
能够看出,primary 组件加载实现后就不会抛出异样,因而不会进入到 second pass,那么就不会有革除 deletions 的操作,因而本次实现后 fallback 依然在删除列表中,最终会被删除。达到了切换到 primary 组件的目标。整体流程为:

利用 Suspense 本人实现数据加载
在咱们明确了 lazy + Suspense 的原理之后,能够本人利用 Suspense 来进行数据加载,其无非就是三种状态:

初始化:查问数据,抛出 promise
加载中: 间接抛出 promise
加载实现:设置 promise 返回的数据

依照这样的思路,设计一个简略的数据加载性能:
// 模仿申请 promise
function mockApi(){
return delay(5000).then(() => “data fetched”)
}

// 解决申请状态变更
function fetchData(){
let status = “uninit”
let data = null
let promise = null
return () => {

switch(status){
  // 初始状态,发出请求并抛出 promise
  case "uninit": {const p = mockApi()
      .then(x => {
        status = "resolved"
        data = x
      })
      status = "loading"
      promise = p
    throw promise
  };
  // 加载状态,间接抛出 promise
  case "loading": throw promise;
  // 如果加载实现间接返回数据
  case "resolved": return data;
  default: break;
}

}
}

const reader = fetchData()

function TestDataLoad(){
const data = reader()
return (

<p>{data}</p>

)
}

function App() {
const [count, setCount] = useState(1)
useEffect(() => {

setInterval(() => setCount(c => c > 100 ? c: c + 1), 1000)

}, [])
return (

 <>
    <Suspense fallback={"loading"}>
      <TestDataLoad/>
    </Suspense>
    <p>count: {count}</p>
 </>

)
}
后果为一开始显示 fallback 中的 loading,数据加载实现后显示 data fetched

正文完
 0