深度解析 VueUse watchImmediate:immediate 触发语义、类型重载与 airi 项目中的实战范式
【免费下载链接】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
在 airi(自托管的 AI 陪伴体/虚拟形象项目)中,Web、Electron 桌面端(stage-web、stage-tamagotchi、stage-pocket)均为 Vue 3 应用,大量依赖 VueUse 组合式函数处理响应式监听逻辑。本篇围绕 airi 仓库内置的 VueUse 技能参考文档 watchImmediate.md 展开,系统讲解watchImmediate的"注册即触发"语义、三组 TypeScript 重载签名、返回句柄的用法,并结合 airi 源码中大量等价的watch(..., { immediate: true })范式,说明这类"立即执行"监听在资源加载状态、音频输入等场景中的落地方式。
watchImmediate 是什么:{ immediate: true }的语法糖
参考文档给出的核心定义只有一句话:
Shorthand for watching value with
{immediate: true}
即watchImmediate是 Vue 内置watch的简写形式,等价于始终传入{ immediate: true }选项。两者的差异在于触发时机:
- 普通
watch:只在被监听源发生变化之后才回调; watchImmediate(即immediate: true):在监听注册时立即同步执行一次回调,之后行为与普通watch一致。
文档中的官方用法示例完整如下:
import { watchImmediate } from '@vueuse/core' const obj = ref('vue-use') // changing the value from some external store/composables obj.value = 'VueUse' watchImmediate(obj, (updated) => { console.log(updated) // Console.log will be logged twice })示例的注释说明日志会打印两次,对应两种触发:
- immediate 触发——
watchImmediate注册瞬间,回调以当前值'VueUse'同步执行一次(此时"旧值"参数为undefined); - 变更触发——之后源值再次变化时,回调携带新旧值再次执行。
这正是immediate: true存在的价值:许多业务需要"拿到初始值就做一件事"(如初始化 UI、拉取首屏数据、把外部 store 的现成状态水合进本地),普通watch只能额外写一段重复的初始化代码,而immediate模式让"首次执行"和"变更响应"复用同一段逻辑,避免两套几乎相同的路径。
类型声明解读:三组重载与选项裁剪
参考文档完整给出了watchImmediate的类型声明(对应 VueUse@vueuse/core,airi 仓库通过 pnpm catalog 锁定@vueuse/core版本为^14.4.0,见 pnpm-workspace.yaml):
export declare function watchImmediate<T>( source: WatchSource<T>, cb: WatchCallback<T, T | undefined>, options?: Omit<WatchOptions<true>, "immediate">, ): WatchHandle export declare function watchImmediate<T extends Readonly<MultiWatchSources>>( source: [...T], cb: WatchCallback<MapSources<T>, MapOldSources<T, true>>, options?: Omit<WatchOptions<true>, "immediate">, ): WatchHandle export declare function watchImmediate<T extends object>( source: T, cb: WatchCallback<T, T | undefined>, options?: Omit<WatchOptions<true>, "immediate">, ): WatchHandle三个重载分别覆盖三类监听源,且都返回WatchHandle(一个停止监听的函数,可交给onScopeDispose或手动调用以解绑):
| 重载 | 监听源 | 回调新旧值类型 | 语义 |
|---|---|---|---|
| 第一个 | WatchSource<T>:单个 ref、getter 或任意值 | WatchCallback<T, T \| undefined> | 单源监听。注意旧值类型为T \| undefined——这正是immediate触发的类型学体现:首次同步调用时并没有"旧值",只能为undefined |
| 第二个 | [...T]元组形式的多源数组 | WatchCallback<MapSources<T>, MapOldSources<T, true>> | 多源监听,回调收到映射后的源值元组与旧值元组;MapOldSources<T, true>中的true表示每个旧值都可能为undefined |
| 第三个 | T extends object(reactive 对象) | WatchCallback<T, T \| undefined> | 监听整个响应式对象,VueUse 内部会加上deep: true的等效行为 |
另一个值得注意的细节是第三个参数:options?: Omit<WatchOptions<true>, "immediate">。Omit<..., "immediate">在类型层面禁用了immediate选项——因为该函数已经隐含immediate: true,再传immediate: false没有意义,编译器会直接报错,防止误用。而WatchOptions<true>仍允许传入deep、once、flush等其余选项。
在 airi 中的对应范式:watch+{ immediate: true }
需要说明的是,airi 业务代码中没有直接导入watchImmediate,而是以等价的watch(source, cb, { immediate: true })形式广泛使用——全仓库有数十处,例如 audio-input.ts、App.vue、theme-color.ts 等。两者运行时行为完全一致,watchImmediate只是更简洁的书写形式。
一个有代表性的实现位于桌面端资源 Store resources.ts:
const atLeastOneLoadingDelay5s = refDelayed(atLeastOneLoading, 5000, { immediate: true }) const atLeastOneLoadingDelay10s = refDelayed(atLeastOneLoading, 10000, { immediate: true })这里的atLeastOneLoading是一个computed(聚合所有资源模块的加载状态),refDelayed则基于watch+setTimeout实现"延迟 N 毫秒才反映变化"的防抖式派生值。阅读 resources.ts 中refDelayed的实现,可以看到immediate语义对初始化路径的实际影响:
const delayedRef = ref<T>(outRef.value) let isFirstRun = true watch(outRef, (newVal) => { if (isFirstRun && options?.immediate) { delayedRef.value = newVal isFirstRun = false return } setTimeout(() => { delayedRef.value = newVal }, delay) })首次(immediate)执行时不做延迟、直接同步赋值,只有后续变更才走setTimeout。这类"加载超过 5 秒/10 秒才提示"的 UI 状态派生,就是immediate: true语义的典型受益场景:监听注册时必须立即拿到当前值完成初始同步,否则派生 ref 的初始状态会与真实状态脱节。
在 VueUse Watch 分类中的定位
SKILL.md 将watchImmediate归入Watch分类,描述为 "Shorthand for watching value with{immediate: true}",并标注调用规则为AUTO(满足场景时可直接选用,无需用户显式要求)。同分类下还有一组围绕watch的衍生函数,各自解决不同问题,可与watchImmediate互补使用:
watchDeep:{ deep: true }的简写,只解决"深度监听",不解决"立即触发";若两者都需要,可叠加deep: true选项传给watchImmediate;whenever:监听值变为 truthy 的简写,语义偏向条件触发;watchOnce、watchDebounced、watchThrottled等:一次性、防抖、节流等触发次数与频率控制。
这些参考卡片来自上游 VueUse 技能库的同步产物(见 SYNC.md),其类型声明与@vueuse/core的发行 API 保持一致。
实践要点小结
- 何时选
watchImmediate:回调逻辑在"初始值"上同样必须执行一次(初始化同步、首帧派生、外部 store 水合),用它替代"手动初始化 + watch"的双路代码; - 首次调用旧值为
undefined:类型签名T | undefined已明确提示,回调内对 oldValue 的解引用需要判空; - 返回
WatchHandle:在组件 setup 外或非作用域环境中使用时,应显式持有并在合适时机调用停止函数,避免监听泄漏; - 与其他选项组合:
deep、once、flush等仍可正常传入(immediate被类型裁剪),例如"立即执行且深度监听对象"是常见组合; - 等价写法可互换:在 airi 现有代码库中,
watch(src, cb, { immediate: true })与watchImmediate(src, cb)行为等价,阅读源码时可将二者视为同一模式。
【免费下载链接】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),仅供参考