airi 项目实战:VueUse whenever —— 只在“变真时”执行的 watch 简写
【免费下载链接】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-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
whenever是 VueUse(@vueuse/core)中 Watch 分类下的一个核心 composable,专门用来“监听某个响应式值变为真值(truthy)的那一刻”执行回调。airi 仓库在性能可视化工具的快捷键处理和跨应用登录标志位(needsLogin)的触发逻辑中都直接使用了它。读完本篇,你能掌握whenever的完整语义(回调参数、getter 源、选项与类型声明),并学会在 airi 这类 Vue 3 + Pinia 多端应用中用一行代码替代“watch + if 判真”的样板代码。
一、核心定位:watch + 真值过滤的等价简写
whenever的官方定义是一句话:Shorthand for watching value to be truthy(监听值变为真值的简写)。它等价于在watch回调中手动加if判断:
import { whenever } from '@vueuse/core' // 这个写法 whenever(ready, () => console.log(state)) // 等价于: watch(ready, (isReady) => { if (isReady) console.log(state) })这个等价关系揭示了它的完整语义(从源码结构看,即“带真值过滤的 watch”):
- 只响应“变真”的跃迁:由于
watch本身只在值变化时触发,whenever实际上只在源值从假值(false、0、''、null、undefined)变为真值时执行回调。值在真值之间互相变化(例如1变2)不会触发。 - 源值初始即为真时不触发:
whenever不默认immediate,如果注册监听时源值已经是真值,回调不会立即执行;需要立即检查初始状态时可通过选项传入{ immediate: true }(选项与watch完全一致,见下文第四节)。 - 返回值是
WatchHandle:与watch一样可用于手动停止监听,并在组件卸载时随 effect scope 自动清理,不产生泄漏。
在 airi 仓库的 AI 开发规范 SKILL.md 中,whenever被列在 Watch 分类下且 Invocation 标记为AUTO——即“凡是用它能实现的需求,优先使用它而不是手写 watch + if”,这正是它存在的工程价值:用一个语义化的名字表达“条件满足时执行一次副作用”。
二、完整用法:从文档示例到四种调用形态
以下内容完整继承自 airi 仓库内置的 VueUse 参考文档 whenever.md。
基本用法:与 useAsyncState 配合
典型场景是“异步数据就绪后才执行副作用”:
import { useAsyncState, whenever } from '@vueuse/core' const { state, isReady } = useAsyncState( fetch('https://jsonplaceholder.typicode.com/todos/1').then(t => t.json()), {}, ) whenever(isReady, () => console.log(state))isReady从false变为true的那一刻,state已可用,回调恰好在此时执行——无需手写if (isReady)。
回调参数:与 watch 完全一致
与watch相同,回调以cb(value, oldValue, onInvalidate)的签名被调用。在真值场景下,最有用的通常是前两个参数:value是新的真值,oldValue是变化前的旧值,可以据此计算增量:
import { whenever } from '@vueuse/core' whenever(height, (current, lastHeight) => { if (current > lastHeight) console.log(`Increasing height by ${current - lastHeight}`) })源可以是 getter 函数
与watch一致,第一个参数可以传一个 getter,每次变化时重新求值,从而监听“派生条件”而非某个具体 ref:
import { whenever } from '@vueuse/core' // 监听“counter 恰好等于 7”这个条件成立 whenever( () => counter.value === 7, () => console.log('counter is 7 now!'), )选项:与 watch 完全一致
第三个参数透传给watch的WatchOptions,例如 flush 时机:
whenever( () => counter.value === 7, () => console.log('counter is 7 now!'), { flush: 'sync' }, )flush可选'pre'(默认,组件更新前)、'post'(组件更新后)、'sync'(同步立即执行);{ immediate: true }、{ deep: true }等选项同样适用。
三、类型声明:Truthy 与 once 选项
官方类型声明完整如下(引自 whenever.md):
type Truthy<T> = T extends false | null | undefined ? never : T export interface WheneverOptions< Immediate = boolean, > extends WatchOptions<Immediate> { /** * Only trigger once when the condition is met * * Override the `once` option in `WatchOptions` * * @default false */ once?: boolean } /** * Shorthand for watching value to be truthy * * @see https://vueuse.org/whenever */ export declare function whenever<T>( source: WatchSource<T>, cb: WatchCallback<Truthy<T>, T | undefined>, options?: WheneverOptions<true>, ): WatchHandle export declare function whenever<T>( source: WatchSource<T>, cb: WatchCallback<Truthy<T>, T>, options?: WheneverOptions<false>, ): WatchHandle三个值得注意的点:
Truthy<T>收窄了回调参数的值类型。例如源是Ref<boolean>,普通watch的回调拿到value: boolean;而whenever的回调拿到的是Truthy<T>——在触发点保证为真值类型(false | null | undefined被排除为never),省去回调内的二次判空。Once选项:WheneverOptions.once(默认false)表示条件满足、回调触发一次后自动停止监听,语义上覆盖了WatchOptions中的同名once选项。- 两个重载区分 oldValue 是否可能为
undefined:WatchCallback<Truthy<T>, T | undefined>与WatchCallback<Truthy<T>, T>对应 immediate 语义下 oldValue 的有无,由编译器根据你传入的选项类型自动选中。
四、airi 仓库中的两处真实用法
airi 是一个 pnpm workspace 单体仓库,各应用(Web 舞台、Pocket、Electron 桌面等)与共享包packages/stage-ui统一通过 catalog 版本@vueuse/core: ^14.4.0(见 pnpm-workspace.yaml)引用 VueUse。仓库中至少有两处whenever的生产级用法,分别对应它最典型的两种场景。
场景一:快捷键“按下即触发” —— 性能可视化页
文件:performance-visualizer.vue
const magicKeys = useMagicKeys() whenever(magicKeys['ctrl+alt+l'], () => lagStore.toggleAll(true)) whenever(magicKeys['ctrl+alt+k'], () => lagStore.toggleAll(false))useMagicKeys()返回一个响应式的按键映射,magicKeys['ctrl+alt+l']在组合键按下时变为true、松开时变回false。如果直接用watch,每次回调都要写if (pressed) { ... }来忽略“松开”这一次变化;换成whenever后,回调天然只在“按下”(false → true)跃迁时执行一次,语义与意图完全对齐。这个页面是桌面端设置里用于可视化性能指标(帧率、内存等)的开发者工具,快捷键用于一键开/关全部指标的采集。
从源码结构看,这正是whenever相对watch的核心收益:把“按键释放导致值变假”这一次无意义的触发在类型与逻辑层面都过滤掉了。
场景二:跨应用登录标志位 —— needsLogin 事件旗标
文件:auth.ts
// Cross-app "user must log in" flag. Setting this to true triggers an // immediate OIDC redirect on web (mobile + desktop). Electron skips this // path because controls-island-auth-button listens for IPC and handles // sign-in in the main process. const needsLogin = ref(false) const { isMobile } = useBreakpoints() whenever(needsLogin, async () => { if (isStageTamagotchi()) return // Consume the request before opening an external browser. Pocket stays // mounted when the user cancels there, so leaving this true would make // the next button click a no-op instead of starting a new OIDC flow. needsLogin.value = false await triggerSignIn() }) // Reset the flag if the viewport class flips, so a stale needsLogin from a // previous breakpoint does not surface again on resize. watch(isMobile, () => needsLogin.value = false)这里needsLogin是一个“事件旗标”(event flag):欢迎页、优惠券弹窗、登录失败的auth-fetch等多处 UI 只需要把authStore.needsLogin = true(例如 step-welcome.vue 中的一行赋值),全局唯一的whenever监听器就会在 false → true 跃迁时拉起 OIDC 登录流程。两个工程细节值得借鉴:
- 消费式复位:回调内先
needsLogin.value = false再await triggerSignIn()。注释解释了原因——Pocket 页签在外部浏览器取消登录时仍然保持挂载,若标志位一直是true,下一次点击无法产生新的跃迁,登录按钮就成了 no-op。 - 测试兜底:auth.test.ts 中的用例 “allows sign-in to be requested again after an external flow is canceled” 显式验证了这一模式:置
true→ 断言triggerSignIn被调用且标志位复位为false→ 再次置true→ 断言第二次登录流程能正常触发。测试注释还点出了whenever依赖“false → true 跃迁”这一语义的根因(ROOT CAUSE)说明,是理解第三节“只响应变真跃迁”的绝佳实证。
五、选型:whenever vs watchOnce vs 普通 watch
| 需求 | 推荐写法 |
|---|---|
| 值变为真值时执行(可反复触发) | whenever(source, cb) |
| 条件满足后只执行一次即停止 | whenever(source, cb, { once: true }) |
| 值任意变化都执行 | watch(source, cb) |
| 值变为真值且只需一次、语义更直白 | watchOnce(source, () => { if (v) ... })或whenever(..., { once: true }) |
同属 Watch 分类的watchOnce(watchOnce.md)是{ once: true }的简写,但它不区分新旧值真假;而whenever的{ once: true }语义是“条件(真值)满足一次即停”,二者解决的问题不同。另外,若你需要“等到值满足某个条件再继续后续流程”的 Promise 化写法,应选用until(同为 Watch 分类,见 until.md),它与whenever形成“命令式回调 / 异步等待”的互补。
六、使用要点小结
whenever适用于“标志位 / 就绪状态 / 按键状态”这类布尔语义的响应式值,把“监听 + 判真 + 执行”压缩为一行;- 回调签名与
watch完全一致(value, oldValue, onInvalidate),且value被Truthy<T>收窄,触发点即为真值; - 选项直接透传
WatchOptions(flush、immediate、deep等),并额外提供once控制一次性触发; - 它依赖“false → true 跃迁”:注册时源值已为真则不触发;若下游流程可能让标志位残留为
true,务必像 auth.ts 那样在回调内消费(复位)标志位,并用 auth.test.ts 这类用例固化“可重复触发”的行为; - airi 的 AI 开发规范 SKILL.md 将其标记为
AUTO级函数,在 Vue 3 项目中遇到“条件为真时做事”的诉求,应优先想到whenever而非手写watch。
【免费下载链接】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-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考