Appearance
Suspense:处理组件的异步依赖
引言
Vue 3 允许 setup 函数是一个 async 函数,这意味着组件可以在 setup 内部等待异步操作(如 await fetchData())完成后再渲染。
vue
<script setup>
const userData = await fetchUser(props.userId) // "暂停"执行
</script>
<template>
<div>{{ userData.name }}</div>
</template>但这带来一个问题:在 fetchUser 完成之前,组件该渲染什么?
<Suspense> 是 Vue 为此提供的“异步边界”解决方案。它允许你“捕获”子孙组件中的异步依赖,并在“等待”期间,显示一个 fallback(回退)内容。
核心:async setup 如何“通知” Suspense?
Suspense 的机制围绕一个“通知与回调”的流程。
流程的起点在“子组件”:
- 当一个带
async setup的子组件(如UserProfile)被创建时,setup函数会返回一个Promise。这个Promise被存储在instance.asyncDep上。 - 子组件开始挂载(
mountComponent)。渲染器发现instance.asyncDep存在。 - 此时,子组件会向上查找组件树,寻找离它最近的
parentSuspense(即<Suspense>组件创建的“边界”)。 - 如果找到了,它会立即“注册依赖”:
parentSuspense.registerDep(instance, setupRenderEffect)。
这个 registerDep 调用,就是子组件在向 <Suspense> 发起通知:“我有一个异步依赖(asyncDep)还没完成,请你等待我。”
子组件的“依赖登记处”:registerDep
registerDep (packages/runtime-core/src/components/Suspense.ts) 是 <Suspense> 边界对象上的一个方法。它负责监听子组件的 Promise:
typescript
// SuspenseBoundary.registerDep (简化)
registerDep(instance: ComponentInternalInstance, setupRenderEffect) {
const suspense = this // 'this' 是 Suspense 边界
// 1. 【依赖计数】
// 告诉 Suspense:“你现在有 1 个依赖正在等待”
suspense.deps++
// 2. 【监听 Promise】
// 监听子组件的 async setup Promise
instance.asyncDep!
.catch(err => {
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
})
.then(asyncSetupResult => {
// 3. 【Promise 完成了】
// 检查:如果 Suspense 已经切换了(pendingId 不匹配)
// 或组件已卸载,就什么都不做
if (instance.isUnmounted || suspense.isUnmounted ||
suspense.pendingId !== instance.suspenseId) {
return
}
// 4. 【依赖递减】
// 告诉 Suspense:“我准备好了”
suspense.deps--
// 5. 【子组件收尾】
// 子组件现在可以完成自己的渲染了
instance.asyncResolved = true
handleSetupResult(instance, asyncSetupResult, false) // 处理 setup 结果
setupRenderEffect(instance, ...) // 执行子组件的真实渲染
// 6. 【检查是否全部完成】
// 如果我是最后一个完成的 (deps === 0)
if (suspense.deps === 0) {
// 通知 Suspense:“所有人都准备好了,你可以切换了!”
suspense.resolve()
}
})
}“Suspense”的“分流”策略:mountSuspense
Suspense 组件自己在 mount 时,会通过 mountSuspense 函数处理其渲染逻辑。
typescript
// core/packages/runtime-core/src/renderer.ts
function mountSuspense(
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
// ...
) {
const { p: patch, o: { createElement } } = rendererInternals
// 1. 【创建“隐藏容器”】
// 这是一个离屏的 div,用于“等待室”
const hiddenContainer = createElement('div')
// 2. 【创建 Suspense 边界】
// 这是核心控制器,上面有 deps, registerDep, resolve 等
const suspense = (vnode.suspense = createSuspenseBoundary(
vnode,
// ...
hiddenContainer,
// ...
))
// 3. 【尝试在“隐藏容器”中渲染默认插槽】
// 'pendingBranch' (等待分支) 指的是默认插槽内容
patch(
null,
(suspense.pendingBranch = vnode.ssContent!), // ssContent 是 #default 插槽
hiddenContainer, // 【关键】渲染到“隐藏容器”中
null,
parentComponent,
suspense, // 将“自己”传递下去,供子组件 registerDep
)
// 4. 【检查依赖】
// 在 patch 过程中,子组件的 async setup 已经被触发
// 如果子组件调用了 registerDep, 那么 suspense.deps > 0
if (suspense.deps > 0) {
// 4a. 【有异步依赖】:
// 渲染被“暂停”了
// 触发 onPending, onFallback 事件
triggerEvent(vnode, 'onPending')
triggerEvent(vnode, 'onFallback')
// 【渲染 Fallback】
// 在“真实 DOM”中渲染 #fallback 插槽
patch(
null,
vnode.ssFallback!, // ssFallback 是 #fallback 插槽
container, // 【关键】渲染到“真实容器”
anchor,
)
// 标记“激活分支”为 fallback
setActiveBranch(suspense, vnode.ssFallback!)
} else {
// 4b. 【无异步依赖】:
// 子组件是同步的,deps 仍然是 0
// 直接“解析”,将“隐藏容器”的内容移到“真实 DOM”
suspense.resolve(false, true)
}
}“切换”策略:resolve
当 deps 计数器归零时,suspense.resolve() 被调用。它的工作是完成内容的“切换”:
typescript
// SuspenseBoundary.resolve (简化)
resolve() {
const {
vnode,
activeBranch, // 当前激活的分支 (即 fallback VNode)
pendingBranch, // 等待的分支 (即 default VNode,已在隐藏容器中)
container, // 真实的 DOM 容器
effects // 挂起的回调 (如子组件的 onMounted)
} = suspense
// 1. 【卸载 Fallback】
// 卸载当前激活的“回退内容”
if (activeBranch) {
unmount(activeBranch, parentComponent, suspense, true)
}
// 2. 【移动 Default】
// 将“隐藏容器”中已经渲染好的“等待分支” (pendingBranch)
// 通过 DOM move 操作,“移动”到真实的 DOM 容器中
move(pendingBranch!, container, anchor, MoveType.ENTER)
// 3. 【激活 Default】
// 将“激活分支”设为 default VNode
setActiveBranch(suspense, pendingBranch!)
// 4. 清理状态
suspense.pendingBranch = null
suspense.isInFallback = false
// 5. 执行挂起的副作用 (如子组件的 onMounted)
queuePostFlushCb(effects)
// 触发 @resolve 事件
triggerEvent(vnode, 'onResolve')
}“嵌套与超时”
嵌套 Suspense (
suspensible: true): 默认情况下,子 Suspense 会独立工作。但如果设置了suspensible: true,子 Suspense 在创建时(createSuspenseBoundary),会把自己注册为父 Suspense 的一个dep。 这样,父 Suspense 就会等待子 Suspense 解析完毕后,才能一起resolve。超时 (
timeout): 如果在timeout时间内deps仍未归零,Suspense 会强制显示fallback。这是通过在mountSuspense中设置一个setTimeout来实现的。如果timeout触发,它会调用suspense.fallback(),其逻辑是:卸载pendingBranch(如果它还在的话),并挂载fallback。
总结
<Suspense> 是一个“异步协调器”,它解耦了“父组件的加载状态”和“子组件的异步逻辑”:
- “隐藏容器”:Suspense 在
mount时创建一个隐藏的div,用于尝试渲染#default内容(pendingBranch)。 registerDep(依赖注册):async setup子组件在mount时会“暂停”,并向上调用parentSuspense.registerDep()。deps(依赖计数):registerDep使suspense.deps计数器+1,并监听Promise。- Fallback (回退):
mountSuspense检查deps > 0,如果为true,则只渲染#fallback内容(activeBranch)到真实 DOM 中。 resolve(解析):Promise完成后,在registerDep的.then()回调中deps--。当deps归零时,suspense.resolve()被调用。- “切换”:
resolve()负责卸载fallback(activeBranch),并将“隐藏容器”中已渲染完毕的#default(pendingBranch) 移动到真实 DOM 中。
