共计 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: 参考 React 实战视频解说:进入学习
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 协调整个阶段都在 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