airi 项目实战:VueUse whenever —— 只在“变真时”执行的 watch 简写
2026/9/10 1:23:45 网站建设 项目流程

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实际上只在源值从假值(false0''nullundefined)变为真值时执行回调。值在真值之间互相变化(例如12)不会触发。
  • 源值初始即为真时不触发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))

isReadyfalse变为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 完全一致

第三个参数透传给watchWatchOptions,例如 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

三个值得注意的点:

  1. Truthy<T>收窄了回调参数的值类型。例如源是Ref<boolean>,普通watch的回调拿到value: boolean;而whenever的回调拿到的是Truthy<T>——在触发点保证为真值类型(false | null | undefined被排除为never),省去回调内的二次判空。
  2. Once选项WheneverOptions.once(默认false)表示条件满足、回调触发一次后自动停止监听,语义上覆盖了WatchOptions中的同名once选项。
  3. 两个重载区分 oldValue 是否可能为undefinedWatchCallback<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 = falseawait 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形成“命令式回调 / 异步等待”的互补。

六、使用要点小结

  1. whenever适用于“标志位 / 就绪状态 / 按键状态”这类布尔语义的响应式值,把“监听 + 判真 + 执行”压缩为一行;
  2. 回调签名与watch完全一致(value, oldValue, onInvalidate),且valueTruthy<T>收窄,触发点即为真值;
  3. 选项直接透传WatchOptionsflushimmediatedeep等),并额外提供once控制一次性触发;
  4. 它依赖“false → true 跃迁”:注册时源值已为真则不触发;若下游流程可能让标志位残留为true,务必像 auth.ts 那样在回调内消费(复位)标志位,并用 auth.test.ts 这类用例固化“可重复触发”的行为;
  5. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询