跳到主要内容

React Fiber 可等待对象

一、作用

二、导出的常量

1. 挂起异常

// An error that is thrown (e.g. by `use`) to trigger Suspense. If we
// detect this is caught by userspace, we'll log a warning in development.
//
// 一个被抛出的错误(例如通过 `use`),用于触发 Suspense。如果我们检测到它被用户代码
// 捕获,在开发环境下我们会记录一个警告。
export const SuspenseException: mixed = new Error(
"Suspense Exception: This is not a real error! It's an implementation " +
'detail of `use` to interrupt the current render. You must either ' +
'rethrow it immediately, or move the `use` call outside of the ' +
'`try/catch` block. Capturing without rethrowing will lead to ' +
'unexpected behavior.\n\n' +
'To handle async errors, wrap your component in an error boundary, or ' +
"call the promise's `.catch` method and pass the result to `use`.",
);

2. 挂起提交异常

export const SuspenseyCommitException: mixed = new Error(
'Suspense Exception: This is not a real error, and should not leak into ' +
"userspace. If you're seeing this, it's likely a bug in React.",
);

3. 挂起动作异常

export const SuspenseActionException: mixed = new Error(
"Suspense Exception: This is not a real error! It's an implementation " +
'detail of `useActionState` to interrupt the current render. You must either ' +
'rethrow it immediately, or move the `useActionState` call outside of the ' +
'`try/catch` block. Capturing without rethrowing will lead to ' +
'unexpected behavior.\n\n' +
'To handle async errors, wrap your component in an error boundary.',
);

4. noopSuspensey提交可挂起对象

// This is a noop thenable that we use to trigger a fallback in throwException.
// TODO: It would be better to refactor throwException into multiple functions
// so we can trigger a fallback directly without having to check the type. But
// for now this will do.
//
// 这是一个 noop 可等待对象,我们用它来触发 throwException 中的回退。
// TODO: 最好将 throwException 重构为多个函数这样我们可以直接触发回退,而无需检查类型。
// 但目前这样也可以。
export const noopSuspenseyCommitThenable = {
then() {
if (__DEV__) {
console.error(
'Internal React error: A listener was unexpectedly attached to a ' +
'"noop" thenable. This is a bug in React. Please file an issue.',
);
}
},
};

三、创建可等待状态

export function createThenableState(): ThenableState {
// The ThenableState is created the first time a component suspends. If it
// suspends again, we'll reuse the same state.
//
// ThenableState 是在组件第一次挂起时创建的。如果它再次挂起,我们将重用相同的状态。
if (__DEV__) {
return {
didWarnAboutUncachedPromise: false,
thenables: [],
};
} else {
return [];
}
}

四、是否已解决

export function isThenableResolved(thenable: Thenable<mixed>): boolean {
const status = thenable.status;
return status === 'fulfilled' || status === 'rejected';
}

五、跟踪已使用的 Thenable

备注
export function trackUsedThenable<T>(
thenableState: ThenableState,
thenable: Thenable<T>,
index: number,
): T {
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
ReactSharedInternals.didUsePromise = true;
}
const trackedThenables = getThenablesFromState(thenableState);
const previous = trackedThenables[index];
if (previous === undefined) {
trackedThenables.push(thenable);
} else {
if (previous !== thenable) {
// Reuse the previous thenable, and drop the new one. We can assume
// they represent the same value, because components are idempotent.
//
// 重用之前的 thenable,丢弃新的。我们可以假设它们表示相同的值,因为组件是幂等的。

if (__DEV__) {
const thenableStateDev: ThenableStateDev = thenableState as any;
if (!thenableStateDev.didWarnAboutUncachedPromise) {
// We should only warn the first time an uncached thenable is
// discovered per component, because if there are multiple, the
// subsequent ones are likely derived from the first.
//
// 我们应该仅在第一次发现未缓存的 thenable 时发出警告,因为如果有多个,后续的
// 很可能是由第一个派生出来的。
//
// We track this on the thenableState instead of deduping using the
// component name like we usually do, because in the case of a
// promise-as-React-node, the owner component is likely different from
// the parent that's currently being reconciled. We'd have to track
// the owner using state, which we're trying to move away from. Though
// since this is dev-only, maybe that'd be OK.
//
// 我们在 thenableState 上跟踪这个,而不是像通常那样使用组件名称去去重,
// 因为在 promise 作为 React 节点的情况下,拥有它的组件很可能与当前正在调和的父组件不同。
// 我们本来需要使用 state 来跟踪拥有者,但我们正试图减少对此的依赖。
// 不过因为这只是开发环境下的操作,也许这样做是可以的。
//
// However, another benefit of doing it this way is we might
// eventually have a thenableState per memo/Forget boundary instead
// of per component, so this would allow us to have more
// granular warnings.
//
// 不过,用这种方式做的另一个好处是,我们最终可能会为每个 memo/Forget 边界拥有
// 一个 thenableState,而不是每个组件一个,
// 这样可以让我们有更细粒度的警告。
thenableStateDev.didWarnAboutUncachedPromise = true;

// TODO: This warning should link to a corresponding docs page.
// 待办:此警告应链接到相应的文档页面。
console.error(
'A component was suspended by an uncached promise. Creating ' +
'promises inside a Client Component or hook is not yet ' +
'supported, except via a Suspense-compatible library or framework.',
);
}
}

// Avoid an unhandled rejection errors for the Promises that we'll
// intentionally ignore.
//
// 避免未处理的拒绝错误,针对我们将故意忽略的 Promise。
thenable.then(noop, noop);
thenable = previous;
}
}

if (__DEV__ && enableAsyncDebugInfo && thenable._debugInfo === undefined) {
// In DEV mode if the thenable that we observed had no debug info, then we add
// an inferred debug info so that we're able to track its potential I/O uniquely.
// We don't know the real start time since the I/O could have started much
// earlier and this could even be a cached Promise. Could be misleading.
//
// 在开发模式下,如果我们观察的 thenable 没有调试信息,我们会添加一个推断的调试信息,以便我们
// 能够唯一地跟踪其潜在的 I/O。
// 我们不知道真实的开始时间,因为 I/O 可能早已开始,这甚至可能是一个缓存的 Promise。
// 可能会产生误导。
const startTime = performance.now();
const displayName = thenable.displayName;
const ioInfo: ReactIOInfo = {
name: typeof displayName === 'string' ? displayName : 'Promise',
start: startTime,
end: startTime,
value: thenable as any,
// We don't know the requesting owner nor stack.
// 我们不知道请求的所有者,也不知道调用栈。
};
// We can infer the await owner/stack lazily from where this promise ends up
// used. It can be used in more than one place so we can't assign it here.
//
// 我们可以根据这个 promise 最终被使用的位置来推断 await 的所有者/堆栈
// 它可能在多个地方使用,所以我们不能在这里直接分配。
thenable._debugInfo = [{ awaited: ioInfo }];
// Track when we resolved the Promise as the approximate end time.
// 跟踪我们解决 Promise 的时间作为大致结束时间。
if (thenable.status !== 'fulfilled' && thenable.status !== 'rejected') {
const trackEndTime = () => {
ioInfo.end = performance.now();
};
thenable.then(trackEndTime, trackEndTime);
}
}

// We use an expando to track the status and result of a thenable so that we
// can synchronously unwrap the value. Think of this as an extension of the
// Promise API, or a custom interface that is a superset of Thenable.
//
// 我们使用一个扩展对象来跟踪 thenable 的状态和结果,以便我们可以同步地展开其值。可以
// 把它看作是 Promise API 的扩展,或者是一个 Thenable 的超集的自定义接口。
//
// If the thenable doesn't have a status, set it to "pending" and attach
// a listener that will update its status and result when it resolves.
//
// 如果 thenable 没有状态,则将其设置为“pending”,并附加一个监听器,当其解析
// 时会更新其状态和结果。
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
default: {
if (typeof thenable.status === 'string') {
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
// Attach a dummy listener, to ensure that any lazy initialization can
// happen. Flight lazily parses JSON when the value is actually awaited.
//
// 只有在状态未定义时才对 thenable 进行检测。如果状态已定义但值未知,则假设它已被某些
// 自定义用户空间实现检测过。我们将其视为“挂起”。
// 附加一个虚拟监听器,以确保可以进行任何延迟初始化。Flight 在实际等待值时才会
// 延迟解析 JSON。
thenable.then(noop, noop);
} else {
// This is an uncached thenable that we haven't seen before.
// 这是一个我们以前未见过的未缓存可 then 对象。

// Detect infinite ping loops caused by uncached promises.
// 检测由未缓存的 Promise 引起的无限 ping 循环。
const root = getWorkInProgressRoot();
if (root !== null && root.shellSuspendCounter > 100) {
// This root has suspended repeatedly in the shell without making any
// progress (i.e. committing something). This is highly suggestive of
// an infinite ping loop, often caused by an accidental Async Client
// Component.
//
// 这个根节点在 Shell 中多次挂起但没有取得任何进展(即没有提交任何内容)。这很可能
// 是一个无限 Ping 循环,通常是由意外的异步客户端组件引起的。
//
// During a transition, we can suspend the work loop until the promise
// to resolve, but this is a sync render, so that's not an option. We
// also can't show a fallback, because none was provided. So our last
// resort is to throw an error.
//
// 在过渡期间,我们可以挂起工作循环,直到 promise 解析,但这是一次同步渲染,因此
// 这不是一个选项。我们也不能显示回退,因为没有提供任何回退方式。所以我们最后的办法
// 是抛出一个错误。
//
// TODO: Remove this error in a future release. Other ways of handling
// this case include forcing a concurrent render, or putting the whole
// root into offscreen mode.
//
// TODO:在未来版本中移除此错误。处理这种情况的其他方法包括强制并发渲染,或者
// 将整个根节点置于离线模式。
throw new Error(
'An unknown Component is an async Client Component. ' +
'Only Server Components can be async at the moment. ' +
'This error is often caused by accidentally ' +
"adding `'use client'` to a module that was originally written " +
'for the server.',
);
}

const pendingThenable: PendingThenable<T> = thenable as any;
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<T> = thenable as any;
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<T> = thenable as any;
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
}

// Check one more time in case the thenable resolved synchronously.
// 再检查一次,以防 thenable 同步解决。
switch ((thenable as Thenable<T>).status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = thenable as any;
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = thenable as any;
const rejectedError = rejectedThenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
}

// Suspend.
// 暂停。
//
// Throwing here is an implementation detail that allows us to unwind the
// call stack. But we shouldn't allow it to leak into userspace. Throw an
// opaque placeholder value instead of the actual thenable. If it doesn't
// get captured by the work loop, log a warning, because that means
// something in userspace must have caught it.
//
// 在这里抛出是一个实现细节,它允许我们展开调用栈。但我们不应该允许它泄漏到用户
// 空间。应抛出一个不透明的占位值,而不是实际的 thenable。如果它没有被工作循环
// 捕获,记录一个警告,因为这意味着用户空间中一定有东西捕获了它。
suspendedThenable = thenable;
if (__DEV__) {
needsToResetSuspendedThenableDEV = true;
}
throw SuspenseException;
}
}
}

六、挂起提交

export function suspendCommit(): void {
// This extra indirection only exists so it can handle passing
// noopSuspenseyCommitThenable through to throwException.
// TODO: Factor the thenable check out of throwException
//
// 这个额外的间接步骤存在的唯一原因是为了能够处理将
// noopSuspenseyCommitThenable 传递给 throwException。
// TODO: 将 thenable 检查从 throwException 中分离出来
suspendedThenable = noopSuspenseyCommitThenable;
throw SuspenseyCommitException;
}

七、延迟解析

备注
export function resolveLazy<T>(lazyType: LazyComponentType<T, any>): T {
try {
if (__DEV__) {
return callLazyInitInDEV(lazyType);
}
const payload = lazyType._payload;
const init = lazyType._init;
return init(payload);
} catch (x) {
if (x !== null && typeof x === 'object' && typeof x.then === 'function') {
// This lazy Suspended. Treat this as if we called use() to unwrap it.
// 这个懒惰的挂起对象。把它当作我们调用 use() 来展开它一样处理。
suspendedThenable = x;
if (__DEV__) {
needsToResetSuspendedThenableDEV = true;
}
throw SuspenseException;
}
throw x;
}
}

八、获取挂起的可等待对象

export function getSuspendedThenable(): Thenable<mixed> {
// This is called right after `use` suspends by throwing an exception. `use`
// throws an opaque value instead of the thenable itself so that it can't be
// caught in userspace. Then the work loop accesses the actual thenable using
// this function.
//
// 这是在 `use` 因抛出异常而挂起后立即调用的。`use`
// 抛出的是一个不透明的值,而不是 thenable 本身,以便它不能在用户空间被捕获。然后工作循环使用
// 此函数访问实际的 thenable。
if (suspendedThenable === null) {
throw new Error(
'Expected a suspended thenable. This is a bug in React. Please file ' +
'an issue.',
);
}
const thenable = suspendedThenable;
suspendedThenable = null;
if (__DEV__) {
needsToResetSuspendedThenableDEV = false;
}
return thenable;
}

九、检查是否使用 try-catch 包裹

export function checkIfUseWrappedInTryCatch(): boolean {
if (__DEV__) {
// This was set right before SuspenseException was thrown, and it should
// have been cleared when the exception was handled. If it wasn't,
// it must have been caught by userspace.
//
// 这是在抛出 SuspenseException 之前设置的,并且在异常被处理时应该已被清除。如果没有清除,
// 那一定是被用户空间捕获了。
if (needsToResetSuspendedThenableDEV) {
needsToResetSuspendedThenableDEV = false;
return true;
}
}
return false;
}

十、检查是否在异步捕获中使用

export function checkIfUseWrappedInAsyncCatch(rejectedReason: any) {
// This check runs in prod, too, because it prevents a more confusing
// downstream error, where SuspenseException is caught by a promise and
// thrown asynchronously.
//
// 这个检查也会在生产环境中运行,因为它可以防止一个更令人困惑的下游错误,
// 即 SuspenseException 被 promise 捕获并异步抛出。
//
// TODO: Another way to prevent SuspenseException from leaking into an async
// execution context is to check the dispatcher every time `use` is called,
// or some equivalent. That might be preferable for other reasons, too, since
// it matches how we prevent similar mistakes for other hooks.
//
// TODO: 防止 SuspenseException 泄漏到异步执行上下文的另一种方法是每次
// 调用 `use` 时检查分发器,或者使用某种等效方法。从其他角度来看,这可能也
// 是更可取的做法,因为它与我们预防其他 hooks 的类似错误的方式一致。
if (
rejectedReason === SuspenseException ||
rejectedReason === SuspenseActionException
) {
throw new Error(
'Hooks are not supported inside an async component. This ' +
"error is often caused by accidentally adding `'use client'` " +
'to a module that was originally written for the server.',
);
}
}

十一、变量

1. 已挂起的可等待对象

备注

在源码中的 318 - 322 行

// This is used to track the actual thenable that suspended so it can be
// passed to the rest of the Suspense implementation — which, for historical
// reasons, expects to receive a thenable.
//
// 这是用来追踪实际被挂起的 thenable,以便可以传递给 Suspense 实现的其余部分——出于
// 历史原因,它预期接收一个 thenable。
// 已挂起的可等待对象
let suspendedThenable: Thenable<any> | null = null;
// 需要重置已挂起的可等待对象DEV
let needsToResetSuspendedThenableDEV = false;

工具

1. 从状态获取可等待对象

function getThenablesFromState(state: ThenableState): Array<Thenable<any>> {
if (__DEV__) {
const devState: ThenableStateDev = state as any;
return devState.thenables;
} else {
const prodState = state as any;
return prodState;
}
}