VueUse `until` 完全指南:用 Promise 一次性等待响应式状态变化的 Watch

发布时间:2026/10/4 19:55:05
VueUse `until` 完全指南:用 Promise 一次性等待响应式状态变化的 Watch 前端【免费下载链接】vueuseCollection of essential Vue Composition Utilities for Vue 3项目地址https://gitcode.com/gh_mirrors/vu/vueuse点击查看免费下载until是 VueUse 提供的一个「承诺化一次性 watch」工具函数位于 packages/shared/until/index.ts。它把 Vue 的响应式监听能力包装成 Promise API让你可以在async/await流程中优雅地「等待某个 ref / getter / 响应式源达到指定条件」再继续执行后续逻辑。读完本文你将掌握until的完整 API、全部断言方法、超时控制与类型推导细节并能把「等待异步数据就绪」「等待计数器达到阈值」这类场景写成简洁、可读、可测试的代码。一、until是什么从命令式 watch 到声明式 await在传统写法中等待一个响应式状态变化通常需要手动注册watch、记录stop函数、在条件满足时调用回调并清理监听const stop watch(count, (v) { if (v 7) { stop() // 手动停止 doSomething() } })这段代码的问题显而易见状态管理靠闭包变量、停止逻辑容易遗漏、多个条件叠加时回调嵌套迅速失控。until将这一整套流程收拢为「一个 Promise、一个断言方法、一行await」await until(count).toMatch(v v 7)其核心语义在源码注释中定义得非常明确Promised one-time watch for changes——一个承诺化的、只触发一次的变化监听index.ts。它接收任意WatchSourceTref、响应式对象、getter 函数或MaybeRefOrGetterT返回一个链式断言实例一旦内部 watch 命中条件Promise 立即 resolve 并自动停止监听。二、快速上手三种最典型的用法1. 等待异步数据就绪配合useAsyncState可以干净地等待异步请求完成后再消费数据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由useAsyncState内部维护初始为false请求完成后变为true。until(isReady).toBe(true)会立即注册一个同步flush的 watchimmediate: true一旦isReady为真就 resolve。若请求已经完成、值已满足条件Promise 会立刻被满足不会卡住流程。2. 等待自定义条件until的toMatch接受任意谓词函数适合表达「大于某阈值」「处于某区间」等复杂条件。官方文档推荐用invoke来执行这个顶层 async 函数避免顶层await的兼容性问题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!) })invoke只是简单地执行传入函数并返回结果general.ts在这里等价于(async () { ... })()。该场景在 demo.vue 中有完整的可运行示例页面提供「Increment / Decrement」按钮当计数恰好达到 7 时弹出 alert。3. 带超时地等待until的第三个参数对象支持timeout与throwOnTimeout两个关键配置import { until } from vueuse/core // 最多等 1000ms超时后 resolve 当前值不抛错 await until(ref).toBe(true, { timeout: 1000 }) // 最多等 1000ms超时后抛出异常 try { await until(ref).toBe(true, { timeout: 1000, throwOnTimeout: true }) // ref.value true } catch (e) { // timeout }三、完整 API 与断言方法until返回的实例根据被监听值是否为数组提供两套断言集合见 index.ts 的类型定义通用断言UntilBaseInstance方法说明toMatch(condition, options?)等待谓词(v: T) boolean返回true返回此时的值T。若谓词是类型守卫(v: T) v is UTS 会收窄返回类型为Uchanged(options?)等待值发生第一次变化changedTimes(1)的别名changedTimes(n, options?)等待值变化至少n次返回第n次变化后的值数值 / 标量断言UntilValueInstance方法说明toBe(value, options?)等待值严格相等value支持MaybeRefOrGetter即可以传入另一个 ref 做「等待两者相等」toBeTruthy(options?)等待值通过Boolean(v)判断为真toBeNull(options?)等待值为nulltoBeUndefined(options?)等待值为undefinedtoBeNaN(options?)等待值为NaN内部用Number.isNaN判断not反转实例not.toBe(x)、not.toBeTruthy()、not.toBeNull()等含义变为「等待值不再是 x / 不再为真 / 不再为 null」数组断言UntilArrayInstance方法说明toContains(value, options?)等待数组包含指定元素value同样支持 ref 形式配合{ deep: true }可监听数组内部增删not反转实例如not.toContains(...)等待元素被移除一个完整示例覆盖全部常用断言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()从源码结构看createUntil在构建实例时通过Array.isArray(toValue(r))判断值是否为数组从而返回数组版或标量版实例index.ts这也是为什么toContains只在数组实例上可用。四、深入底层until是如何工作的1. 核心机制immediatewatch Promise.racetoMatch的实现非常精巧index.tsfunction toMatch(condition, { flush sync, deep false, timeout, throwOnTimeout } {}) { let stop null const watcher new Promise((resolve) { stop watch( r, (v) { if (condition(v) ! isNot) { if (stop) stop() else nextTick(() stop?.()) resolve(v) } }, { flush, deep, immediate: true }, ) }) const promises [watcher] if (timeout ! null) { promises.push( promiseTimeout(timeout, throwOnTimeout) .then(() toValue(r)) .finally(() stop?.()), ) } return Promise.race(promises) }关键点immediate: truewatch 在注册时立刻执行一次回调因此若值已经满足条件Promise 立即 resolve无需等待下一次变化flush: sync默认值与原生watch的默认pre不同until采用同步刷新的配置见 types.ts 中ConfigurableFlushSync的注释说明保证断言尽快命中命中即停回调中调用stop()停止监听后 resolve若条件在 watch 同步注册阶段就已满足此时stop尚未赋值则退化为nextTick(() stop?.())兜底清理超时兜底promiseTimeout创建竞速 Promisegeneral.tsthrowOnTimeout: true时 reject 并抛出Timeout否则 resolve 并返回当前值最后.finally(() stop?.())确保监听被释放Promise.race保证「条件先满足」或「超时先到」时都只有一个结果胜出。2.toBe的两种路径toBe根据传入值是否为 ref 走了两条实现index.ts传入普通值直接委托给toMatch(v v value)传入 ref / getter改用watch([r, value], ...)同时监听两个源等待v1 v2。这让你可以表达「等待 A 等于 B」这类双向对齐的场景。3.changedTimes的计数技巧changedTimes内部用闭包计数器实现index.tsfunction changedTimes(n 1, options) { let count -1 // skip the immediate check return toMatch(() { count 1 return count n }, options) }注意count初始化为-1由于 watch 设置了immediate: true注册时的那次「初始值检查」会执行一次谓词计数器从-1出发、先自增到0恰好跳过初始检查保证只有真实发生的变化才会计数。测试用例验证了changed()在第 1 次变化后返回新值1changedTimes(3)在第 3 次变化后返回3见 index.test.ts。4.not反转不是新建逻辑而是反转判定not通过 getter 惰性创建新的实例index.tsget not() { return createUntil(r, !isNot) }createUntil用一个布尔isNot贯穿所有断言判定时统一做condition(v) ! isNot的比较因此not只是把条件反转并不复制监听逻辑。多次访问not会得到相互独立的实例——测试用例「should supportnotas separate instances」验证了这一点同一个until(r)的两次not.toBe(...)调用互不干扰index.test.ts。五、配置项一览UntilToMatchOptions所有断言方法都接受同一个选项对象完整定义见 index.ts选项类型默认值说明timeoutnumber0等待超时毫秒数0表示永不超时throwOnTimeoutbooleanfalse超时后是否 reject。为false时超时会 resolve 当前值为true时抛出Timeout异常flushWatchOptionFlushsync内部 watch 的刷新时机pre/post/sync与 Vuewatch的flush语义一致deepboolean \| deepfalse是否深度监听对象内部变化监听数组push/pop时需设为true示例组合// 深度监听数组等待元素出现 const r refnumber[]([1, 2, 3]) await until(r).toContains(4, { deep: true }) // 数组 push(4) 后 resolve返回 [1, 2, 3, 4] // 立即超时timeout: 0 表示永不超时不要与“立刻超时”混淆 await until(r).toBe(1, { timeout: 0 }) // 永不超时测试用例「should support array」和「should support array withnot」分别验证了toContains与not.toContains在deep: true下的行为index.test.ts「should immediately timeout」则验证了timeout: 0下不会被超时逻辑打断index.test.ts。六、类型层面的惊喜条件类型推导until的重载签名针对数组和标量分别声明index.tsexport function untilT extends unknown[](r: WatchSourceT | MaybeRefOrGetterT): UntilArrayInstanceT export function untilT(r: WatchSourceT | MaybeRefOrGetterT): UntilValueInstanceT配合复杂的条件类型await的结果会被精准收窄。测试文件末尾专门有一组类型级断言index.test.ts可归纳为以下几点await until(x).toBe(1)返回类型被推导为字面量1await until(x).toBeTruthy()返回排除 falsy 联合后的类型如x | undefined→xawait until(x).toBeUndefined()返回undefined而not.toBeUndefined()返回ExcludeT, undefined传入类型守卫is1: (x: number) x is 1时toMatch(is1)返回1not.toMatch(is1)返回剩余联合2 | 3。这意味着until不仅是运行时好帮手也是类型安全的好帮手写出的等待逻辑在编译期就能被验证减少「值类型与预期不符」这类隐性 bug。七、实用组合与注意事项组合 1等待多个条件同时成立until每次只监听一个源多个条件可拆成多个await顺序执行或用Promise.all并行等待await Promise.all([ until(a).toBe(1), until(b).toBe(2), ])组合 2带超时的数据加载保护try { await until(isLoading).toBe(false, { timeout: 5000, throwOnTimeout: true }) } catch { // 5 秒未完成走降级逻辑 }注意事项入口与导出until由 packages/shared/index.ts 导出同时被 packages/core/index.ts 的export * from vueuse/shared转出因此可以直接import { until } from vueuse/core一次性语义每个断言方法只 resolve 一次命中后内部监听即被释放。若需要反复等待请在每次使用前重新调用until(...)创建新实例默认flush: sync与 Vue 原生watch的默认pre不同同步刷新在「等待值在一次同步更新中被连续改写」时命中更快但也要注意避免在极高频更新场景下的重复求值开销超时不保证停止throwOnTimeout: false时超时只是 resolve 当前值条件本身可能在之后满足此时剩余 watcher 已被竞速丢弃不会再有副作用测试基础官方的 index.test.ts 使用vi.useFakeTimers()vi.advanceTimersByTime()驱动 ref 变化来验证所有断言与超时行为如果你的项目也要测试类似「等待条件」的逻辑可以参考这套模式用假定时器让时间可控。结语until以约 200 行源码index.ts实现了「promise 化的 watch」把响应式等待从回调地狱中解放出来toBe/toMatch/changedTimes覆盖了等值、谓词、变化次数三类最常见的等待诉求not反转、timeout超时、deep深度监听和类型收窄让它既能处理简单标量也能应对数组与复杂对象。在 VueUse 的 Watch 类工具category: Watch中它是把「异步流程」与「响应式状态」衔接得最自然的桥梁之一。赞分享前端【免费下载链接】vueuseCollection of essential Vue Composition Utilities for Vue 3项目地址https://gitcode.com/gh_mirrors/vu/vueuse点击查看免费下载相关推荐VueUse useKeyModifier 完全指南响应式追踪 CapsLock、Shift 等修饰键状态VueUse useKeyModifier 完全指南响应式追踪 CapsLock、Shift 等修饰键状态 useKeyModifier 是 VueUse 核前端VueUse useWindowFocus 完全指南响应式追踪窗口焦点状态VueUse useWindowFocus 完全指南响应式追踪窗口焦点状态 导读 useWindowFocus 是 VueUse 中用于 响应式追踪浏览器窗口前端VueUse 的 useDocumentVisibility响应式追踪页面可见性状态的完整实践指南VueUse 的 useDocumentVisibility响应式追踪页面可见性状态的完整实践指南 useDocumentVisibility 是 VueUs前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考