VueUse until 完全指南:用 Promise 优雅等待响应式状态变化的编程模式

发布时间:2026/9/10 3:56:08
VueUse until 完全指南:用 Promise 优雅等待响应式状态变化的编程模式 VueUse until 完全指南用 Promise 优雅等待响应式状态变化的编程模式【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airiuntil是 VueUse 中一个一次性 Promise 化 watch工具它把 Vue 的响应式状态监听包装成可await的 Promise让你在ref、computed、getter 函数乃至数组发生变化时精准续接异步流程无需手写watch 回调 手动 resolve 的样板代码。本指南以 airi 仓库中的 until 参考文档 为核心结合仓库内 Web 端、桌面端、Live2D/VRM 渲染、语音转写等真实源码调用场景系统讲解其 API、选项、类型系统与实战模式。为什么需要 until从回调式 watch 到 Promise 式等待在 Vue 3 中watch的核心模型是事件回调你注册一个回调状态变化时由框架调用它。但当你的业务逻辑是先做 A等某个状态达到条件再做 B这种线性流程时回调模型会把代码拆成碎片还要手动管理标志位和resolve// 回调式写法需要额外的 promise 与标志位代码分散 let resolveReady: (v: any) void const ready new Promise(res { resolveReady res }) watch(source, (v) { if (v true) resolveReady(v) }, { immediate: true }) await readyuntil把上述模式收敛为一个 awaitable 的链式 API内部对目标源建立一个一次性 watch条件满足即 resolve并自动清理监听await until(source).toBe(true)从源码结构看见下方类型声明一节until接收一个WatchSourceT | MaybeRefOrGetterT参数兼容ref、reactive对象、getter 函数等所有可被watch追踪的源并返回带有toBe、toMatch、changed、changedTimes等方法的实例支持not取反链式调用。它在 airi 仓库中被归类为 Watch 类别下的核心工具约定调用级别为AUTO——即编写 Vue 业务代码时凡是等待某个状态满足后再继续的需求都应优先考虑它。基础用法等待异步数据就绪until最常见的场景是等待异步数据加载完成。参考文档给出的经典示例是与useAsyncState配合先发起一个异步请求再用until等待其isReady标志翻转。import { until, useAsyncState } from vueuse/core const { state, isReady } useAsyncState( fetch(https://jsonplaceholder.typicode.com/todos/1).then(t t.json()), {}, ) ;(async () { await until(isReady).toBe(true) console.log(state) // state is now ready! })()关键点在于isReady是一个响应式refbooleanuntil(isReady).toBe(true)会阻塞后续代码直到请求完成当数据真正到达后state才被读取避免拿到初始空值。airi 仓库里与此完全同构的例子是 display-models.tsstore 用displayModelsFromIndexedDBLoading标志表示 IndexedDB 中自定义模型Live2D / VRM / Spine / MMD / Tachie的异步加载状态而loadDisplayModelsFromIndexedDB、getDisplayModel、addDisplayModel、renameDisplayModel、removeDisplayModel五个异步方法都统一以同一行代码开头await until(displayModelsFromIndexedDBLoading).toBe(false)这里有两层含义一是等待加载流程彻底结束false二是串行化并发访问——无论调用方是模型选择器还是设置页任何操作都要先排队等加载完成避免在 IndexedDB 读写过程中插入竞态。这是until在真实 store 中承担异步互斥门闩gate职责的典型体现。自定义条件匹配toMatch 与 getter 源参考文档第二个示例展示了toMatch——当你需要的不只是等于某个值而是满足某个谓词时使用。配合invoke可以在任何地方启动异步流程import { invoke, until, useCounter } from vueuse/core const { count } useCounter() invoke(async () { await until(count).toMatch(v v 7) alert(Counter is now larger than 7!) })toMatch的谓词签名与Array.prototype.filter类似返回true时 Promise 立即 resolve并携带当时的快照值。它甚至可以充当类型守卫类型声明中toMatch有一个重载当谓词返回类型谓词v is U时resolve 出的类型会被收窄为U见 until.md。除直接传入ref外until的入参还接受getter 函数。airi 仓库大量使用这种形式等待多个条件同时成立OrbitControls.vueawait until(() cameraTres.value renderer.domElement).toBeTruthy()等待 three.js 相机与渲染器 DOM 均就绪后再初始化轨道控制SkyBox.vueawait until(() !!renderer !!renderer.domElement).toBeTruthy()保证天空盒挂载时 WebGL 渲染器已存在VRMModel.vueawait until(() scene.value).toBeTruthy()等待 3D 场景创建完成后再加载 VRM 模型。getter 写法把多源就绪条件折叠成一个布尔表达式比逐个await until(refA).toBeTruthy()更紧凑也天然支持任意复杂的复合条件。超时控制防止无限等待真实业务中等待的状态可能永远不出现设备未授权、请求失败、资源加载异常。until通过options提供两个超时相关选项参考文档完整给出了两种语义import { until } from vueuse/core // 直到 ref.value true 或 1000ms 超时静默 resolve不抛错 await until(ref).toBe(true, { timeout: 1000 }) // 超时则抛错需要 try/catch 捕获 try { await until(ref).toBe(true, { timeout: 1000, throwOnTimeout: true }) // ref.value true } catch (e) { // timeout }选项的默认值在类型声明中有明确标注until.md选项类型默认值作用timeoutnumber0永不超时Promise 等待的毫秒上限0表示不限时throwOnTimeoutbooleanfalse超时时是否 reject为false时静默 resolvedeepWatchOptions[deep]false是否深度监听内部变化直接透传给内部watchairi 仓库对超时选项的使用非常考究尤其是throwOnTimeout与try/catch的组合。语音转写链路是最典型的例子use-transcriptions.ts 在启动麦克风流时这样写await askPermission() // If still no stream, try starting it manually if (!stream.value hearingEnabled.value) { startStream() // Wait for the stream to become available with a timeout. try { await until(stream).toBeTruthy({ timeout: 3000, throwOnTimeout: true }) } catch { console.error(Timed out waiting for audio stream. Stopping transcription., { source: useTranscriptions }) isListening.value false return } }同样的模式也出现在 browser-web-speech-api.vue用户拒绝麦克风授权或系统迟迟不返回流时3 秒后抛出、停止监听并给出日志而不是让异步流程永久挂起。这是可失败的等待的标准姿势先给足时间再优雅降级。另一个短超时场景在 Live2D 模型加载中Model.vue 在画布重建期间用await until(() !!pixiApp.value !!pixiApp.value.stage).toBeTruthy({ timeout: 1500 })短暂等待新 stage 出现超时则回退到mounted状态避免白屏。链式断言全家桶toBe 系列与 not 取反参考文档More Examples一节汇总了until的全部断言方法直接覆盖了日常 90% 的需求import { until } from vueuse/core await until(ref).toBe(true) await until(ref).toMatch(v v 10 v 100) await until(ref).changed() await until(ref).changedTimes(10) await until(ref).toBeTruthy() await until(ref).toBeNull() await until(ref).not.toBeNull() await until(ref).not.toBeTruthy()各方法语义如下对应类型声明 UntilValueInstance方法等待条件备注toBe(value)与目标值严格相等value支持MaybeRefOrGetter可与任意响应式源比较toMatch(fn)谓词返回true支持类型守卫收窄changed()值发生任意变化等待首次变化changedTimes(n)值变化次数达到n从 1 开始计数toBeTruthy()/toBeFalsy()真值 / 假值判定等价于!!vtoBeNull()/toBeUndefined()/toBeNaN()对应空值判定not取反可表达非空not前缀上述所有断言取反类型层面也会翻转not是真正的类型级取反UntilValueInstanceT, Not中not会把Not泛型翻转因此until(ref).not.toBeNull()的返回值类型会被收窄为ExcludeT, null——取反不只是语义反转TypeScript 推导也跟着反转。airi 中的两个实战用法audio-context.ts 与 audio-recorder.ts 都用await until(mediaRef).toBeTruthy()等待麦克风MediaStream注入后再创建AudioContext/ 开始录音——保证音频管线初始化时流一定可用Model.vue 用await until(modelLoading).not.toBeTruthy()等待上一个模型加载结束后再获取互斥锁配合modelLoadMutex防止并发重载 Live2D 模型。数组实例toContains 与类型收窄当until接收的源是数组类型时返回的是UntilArrayInstanceTuntil.md除了继承toMatch、changed、changedTimes外额外提供toContainsexport interface UntilArrayInstanceT extends UntilBaseInstanceT { readonly not: UntilArrayInstanceT toContains: ( value: MaybeRefOrGetterElementOfShallowUnwrapRefT, options?: UntilToMatchOptions, ) PromiseT }toContains等待数组中包含指定元素value同样可以是ref或 getter。需要注意not在数组实例上的语义它返回的仍是UntilArrayInstanceT而非UntilValueInstance因此until(arr).not.toContains(x)表达等待数组不再包含 x。完整类型声明与内部实现要点until的完整类型声明见 until.md核心结构如下export interface UntilToMatchOptions extends ConfigurableFlushSync { timeout?: number // 默认 00 表示永不超时 throwOnTimeout?: boolean // 默认 false deep?: WatchOptions[deep] // 默认 false透传给内部 watch } export interface UntilBaseInstanceT, Not extends boolean false { toMatch: (...) changed: (options?: UntilToMatchOptions) PromiseT changedTimes: (n?: number, options?: UntilToMatchOptions) PromiseT } type Falsy false | void | null | undefined | 0 | 0n | export interface UntilValueInstanceT, Not extends boolean false extends UntilBaseInstanceT, Not { readonly not: UntilValueInstanceT, Not extends true ? false : true toBe: ... toBeTruthy: ... // Not 为 true 时返回 PromiseT Falsy toBeNull: ... // Not 为 true 时返回 PromiseExcludeT, null toBeUndefined: ... toBeNaN: ... }几个值得注意的实现细节源参数兼容性until(r: WatchSourceT | MaybeRefOrGetterT)WatchSource覆盖ref/reactive/ getter 函数这与 Vuewatch的源类型完全一致因此until本质上是一个建立在watch之上的 Promise 封装。toBeTruthy的类型收窄正常情况下返回PromiseExcludeT, Falsy去掉所有假值类型取反后返回PromiseT Falsy——Falsy联合类型在声明中直接列出false | void | null | undefined | 0 | 0n | 。一次性的本质每次调用只等待一次条件满足即 resolve不会持续监听监听器随即被清理天然避免内存泄漏。ConfigurableFlushSyncUntilToMatchOptions继承了 VueUse 的同步 flush 配置族保证在 flush 语义上与依赖方一致。在 airi 中的完整应用地图与最佳实践把仓库里的真实调用点串起来可以绘制出until在 airi 各端的使用全景应用层文件用法解决的问题Web/桌面端display-models.tstoBe(false)× 5IndexedDB 模型加载并发互斥Web/桌面端audio-context.ts、audio-recorder.tstoBeTruthy()等待麦克风流就绪再初始化音频Web/桌面端use-transcriptions.ts、browser-web-speech-api.vuetoBeTruthy({ timeout, throwOnTimeout })麦克风授权超时降级Web/桌面端chat-bubble-minimalism.vuetoBeTruthy()× 2等待气泡 DOM ref 挂载后再运行动画Live2DModel.vuenot.toBeTruthy()、toBeTruthy({ timeout })模型加载互斥锁 画布重建等待3D 场景OrbitControls.vue、SkyBox.vue、VRMModel.vuegetter toBeTruthy()等待相机/渲染器/场景多源就绪综合这些调用可以沉淀出几条适用于任何 Vue 3 / Nuxt 3 项目的最佳实践等待 DOM ref 用toBeTruthy()refHTMLDivElement()在模板渲染后才有值await until(elRef).toBeTruthy()比nextTick更稳健chat-bubble-minimalism.vue 正是这么做的。可失败的等待务必配timeoutthrowOnTimeouttry/catch凡是依赖用户授权、外部设备、网络请求的状态都必须设置超时兜底并给出降级路径。多个异步写操作共享一个 loading 标志时用until(x).toBe(false)做入口门闩display-models.ts 的五个方法统一前置等待是消除竞态的简洁方案。复合就绪条件优先用 getter 源until(() a.value b.value).toBeTruthy()比串行 await 更清晰、更快。对象/数组深层变化记得传deep: true默认浅监听若等待的是嵌套字段变化需显式开启对应UntilToMatchOptions.deep。结语until用最小的 API 面一个入口、一套断言、两个超时选项把 Vue 的响应式世界与异步流程编织在一起是Promise 化响应式状态的标准答案。在 airi 中它既承担着 IndexedDB 模型存储的并发门闩也守护着实时语音转写与 Live2D / VRM 渲染的初始化时序——从 Web 到桌面再到 3D 舞台同一套等待语义贯穿始终。下一次当你发现自己在手写watch 标志位 resolve时不妨先问一句until是不是已经替你写好了【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考