Vue中的Suspense原理讲解

📅 2026/7/30 14:35:42
Vue中的Suspense原理讲解
文章目录一、 核心原理解析1. 状态机3 种状态与 2 个插槽2. 异步依赖收集机制巧用“Promise 抛出与捕获”① async setup() 的原理② 依赖收集流程3. 双 VNode 树与离屏渲染Off-screen / Buffer二、 简易版源码逻辑模拟三、 与 defineAsyncComponent 的协同原理四、 总结一览在 Vue 3 中Suspense是一个内置的控制流组件。它的核心使命是协调Coordinate管理其组件树深处所有异步依赖异步组件、async setup()的加载状态避免页面出现“打地鼠式”的多次闪烁实现平滑的一次性整体渲染。要理解它的底层原理可以拆解为状态机切换、异步依赖收集Promise 抛出与捕获以及双 VNode 树离屏渲染三个维度。一、 核心原理解析1. 状态机3 种状态与 2 个插槽Suspense内部维护了一个简单的状态机[ Pending (加载中) ] / \ / \ (所有异步依赖 resolve) (出现错误) \ ▼ ▼ [ Rejected ] [ Resolved (挂载完成) ]#default插槽包含真正的业务组件树可能嵌套了深层的async setup()或异步组件。#fallback插槽加载中Pending 状态时展示的骨架屏或 Loading 占位组件。2. 异步依赖收集机制巧用“Promise 抛出与捕获”Suspense是如何知道它子孙组件里有哪些东西是异步的①async setup()的原理在 Vue 3 中如果一个组件的setup()是async的或者使用了顶级await它在执行时本质上会返回一个 Promise// 编译后的 setup 函数setup(){// 当组件内部有 await 时 setup 会返回一个 PromisereturndoAsyncStuff().then((){return()h(div,Data loaded)})}② 依赖收集流程当 Vue 渲染组件树遇到一个返回 Promise 的组件或开启了suspensible的异步组件时当前组件的挂载会被暂停。该 Promise 会被向上冒泡Bubble up直到被最近的父级Suspense组件捕获Captured。Suspense内部有一个数组/计数器用来追踪所有捕获到的异步 Promise// 源码核心思路示意suspense.deps// 发现一个未完成的异步依赖计数器 1promise.then((){suspense.deps--// 完成一个计数器 -1if(suspense.deps0){suspense.resolve()// 全部完成触发状态切换}})3. 双 VNode 树与离屏渲染Off-screen / Buffer这是Suspense实现“无缝平滑切换”最关键的物理机制。如果你在已经渲染好的页面上因为某些响应式参数变化重新触发了子组件的异步加载Suspense不会立刻把当前旧页面撕掉变白离屏 DOM 树Subtree Buffer当状态变为 Pending 时Suspense会在内存中继续渲染新的#default树离屏 DOM。继续展示旧视图 / 展示 Fallback如果是首次加载直接在真实 DOM 上展示#fallback骨架屏。如果是更新加载真实 DOM 依然展示旧的#default内容新页面的渲染全在内存中进行。原子化替换Atomic Swap只有当内存中新的#default树里**所有的异步 Promise 全部resolve**之后Vue 才会执行一次 DOM 替换把内存中的 DOM 树一次性挂载到真实 DOM 上并销毁旧视图。效果用户要么看到完整的旧页面要么看到完整的新页面绝对不会看到半加载状态的残缺页面。二、 简易版源码逻辑模拟为了让你更直观地理解我们可以用一个简化版的组件来模拟Suspense的控制逻辑import{defineComponent,ref,h}fromvueexportconstMySuspensedefineComponent({name:MySuspense,setup(props,{slots}){constisResolvedref(false)constpendingDepsref(0)// 提供给子孙组件调用的注册接口实际源码中通过 internal instance 边界捕获constregisterDep(promise:Promiseany){pendingDeps.valueisResolved.valuefalsepromise.then((){pendingDeps.value--if(pendingDeps.value0){// 所有异步依赖已全部解决isResolved.valuetrue}}).catch((err){console.error(Suspense caught error:,err)})}return(){// 如果全部完成渲染 #default 插槽否则渲染 #fallback 插槽if(isResolved.value){returnslots.default?slots.default():null}else{returnslots.fallback?slots.fallback():h(div,Loading...)}}}})三、 与defineAsyncComponent的协同原理前面提到过defineAsyncComponent配置项中有suspensible: true默认值[ Async Component (defineAsyncComponent) ] │ (检查父级是否有 Suspense) │ ┌─────────┴─────────┐ ▼ ▼ [ 有 Suspense ] [ 无 Suspense ] │ │ 把加载 Promise 自己接管状态 向上抛给 Suspense 渲染自己的 loadingComponent当存在Suspense时defineAsyncComponent产生的组件在执行loader()时会把返回的 Promise 注册给上层的 Suspense 节点自己不再独立渲染loadingComponent。当不存在Suspense时它退化为独立的代理组件通过内部的ref(loaded)自行切换loadingComponent和目标组件。四、 总结一览Suspense的原理可以概括为 10 个字“捕获 Promise离屏渲染 DOM”。依赖收集利用组件setup返回 Promise 或异步组件抛出 Promise 的特性在父级拦截并统计未完成的异步任务数量。状态等待在所有 Promise 完成前维持#fallback骨架屏或保持旧视图不跳变。一次性替换通过内存离屏 DOM 树在所有依赖准备就绪后以原子操作的方式一次性挂载新组件树。 感谢阅读想了解更多 我的博客网站 | 记录思考分享干货 我的个人主页 | 关于我、开源项目