☰
VueUse watchOnce 详解:只触发一次的单次监听器,从用法到源码实现
2026/10/6 2:07:26 网站建设 项目流程
  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

VueUse 的watchOnce是 Vue 3watch的一个简洁封装(shorthand),等价于开启{ once: true }选项的监听器:当回调触发一次之后,监听器会被自动停止。本文将以 VueUse 仓库中的官方参考文档与真实源码、测试为依据,完整讲解watchOnce的用法、类型签名、底层实现原理,并给出可复用的实战场景,帮助你在"只需响应一次变化"的场景中写出更干净的代码。

为什么需要 watchOnce

在 Vue 3 开发中,watch是最常用的响应式能力之一。但在不少业务场景里,我们并不希望监听器"持续响应":

  • 等待某个异步状态第一次变为就绪后,只做一次初始化;
  • 等待某个全局状态(如登录状态、用户信息)第一次到位后执行一次性逻辑;
  • 等待某个元素尺寸、路由参数首次变化后触发一次副作用。

如果每次都手动声明{ once: true }并处理停止逻辑,代码会变得啰嗦。Vue 3 的watch原生支持once选项(在回调触发一次后自动停止监听),watchOnce正是对这一能力的极简封装——正如官方参考文档 skills/vueuse-functions/references/watchOnce.md 所描述:"Shorthand for watching value with{ once: true }. Once the callback fires once, the watcher will be stopped."(即:监听值并开启once的简写;回调触发一次后监听器即停止。)

基本用法

watchOnce的 API 形态与 Vue 的watch几乎完全一致,只是无需再手动传入once选项。参考 packages/shared/watchOnce/index.md 中的官方示例:

import { watchOnce } from '@vueuse/core' watchOnce(source, () => { // 只会触发一次 console.log('source changed!') })

当source变化导致回调执行一次之后,监听器随即被停止,后续source的再次变化不会再触发回调。

它同样支持传入一个 getter 函数、ref/shallowRef、reactive对象,以及多个数据源组成的数组作为第一个参数,用法与watch完全对齐。

与原生 watch 的关系

从语义上讲,watchOnce(source, cb, options)完全等价于:

import { watch } from 'vue' watch(source, cb, { ...options, once: true, })

区别仅在于:

  • watchOnce把once: true作为固定语义内建,调用方不能再传once选项去覆盖它;
  • 其余一切watch能力(flush、deep、immediate、onTrack、onTrigger等选项)均可通过第三个参数透传。

因此,watchOnce并不是一个独立的新机制,而是基于 Vue 3 原生oncewatcher 的类型化简写。官方文档提到可参考 Vue 官方关于 once watchers 的说明,对应到本仓库内,也可对照同一系列的watchImmediate({ immediate: true }的封装,见 packages/shared/watchImmediate/index.md)来理解这类"单选项封装"的家族式设计。

完整类型签名

参考文档 skills/vueuse-functions/references/watchOnce.md 中给出了完整的 TypeScript 声明,共包含三个重载,覆盖了watch支持的三种数据源形态:

export declare function watchOnce<T>( source: WatchSource<T>, cb: WatchCallback<T, T | undefined>, options?: Omit<WatchOptions<true>, "once">, ): WatchHandle export declare function watchOnce<T extends Readonly<MultiWatchSources>>( source: [...T], cb: WatchCallback<MapSources<T>, MapOldSources<T, true>>, options?: Omit<WatchOptions<true>, "once">, ): WatchHandle export declare function watchOnce<T extends object>( source: T, cb: WatchCallback<T, T | undefined>, options?: Omit<WatchOptions<true>, "once">, ): WatchHandle

逐条解读这三个重载:

  1. 单个数据源:source: WatchSource<T>,覆盖ref、getter 函数等单一监听源,回调收到新值T与旧值T | undefined(首次触发时旧值为undefined)。
  2. 多数据源数组:source: [...T],其中T extends Readonly<MultiWatchSources>,回调参数由MapSources<T>与MapOldSources<T, true>推导,即"多个源的新值数组"与"多个源的旧值数组",与原生watch([a, b], ...)的类型行为一致。
  3. 响应式对象:source: T extends object,直接监听整个响应式对象,回调收到新对象与旧对象。

options的类型为Omit<WatchOptions<true>, "once">——这是关键设计:类型层面就移除了once字段,杜绝调用方传入与内建语义冲突的配置。返回值WatchHandle与原生watch相同,可通过调用返回的 stop 函数手动提前停止监听。

关于MapSources/MapOldSources这两个辅助类型,其定义位于 packages/shared/utils/types.ts:

export type MapSources<T> = { [K in keyof T]: T[K] extends WatchSource<infer V> ? V : never; } export type MapOldSources<T, Immediate> = { [K in keyof T]: T[K] extends WatchSource<infer V> ? Immediate extends true ? V | undefined : V : never; }

可以看到,MapSources<T>将多数据源数组映射为各自值的元组类型;MapOldSources<T, Immediate>则根据是否 immediate 决定旧值是否包含undefined联合。watchOnce的回调旧值类型走的是Immediate extends true分支(T | undefined),因为oncewatcher 的首次触发同样没有旧值。这些类型与 Vue 3 原生watch的重载保持一致,保证了watchOnce在类型层面与watch无缝兼容。

源码级实现剖析

watchOnce的真实实现非常精简,完整源码位于 packages/shared/watchOnce/index.ts:

import type { MultiWatchSources, WatchCallback, WatchHandle, WatchOptions, WatchSource } from 'vue' import type { MapOldSources, MapSources } from '../utils' import { watch } from 'vue' // overloads export function watchOnce<T>( source: WatchSource<T>, cb: WatchCallback<T, T | undefined>, options?: Omit<WatchOptions<true>, 'once'>, ): WatchHandle export function watchOnce<T extends Readonly<MultiWatchSources>>( source: [...T], cb: WatchCallback<MapSources<T>, MapOldSources<T, true>>, options?: Omit<WatchOptions<true>, 'once'>, ): WatchHandle export function watchOnce<T extends object>( source: T, cb: WatchCallback<T, T | undefined>, options?: Omit<WatchOptions<true>, 'once'>, ): WatchHandle /** * Shorthand for watching value with { once: true } * * @see https://vueuse.org/watchOnce */ export function watchOnce<T = any>(source: T, cb: any, options?: Omit<WatchOptions, 'once'>) { return watch( source as WatchSource<T>, cb, { ...options, once: true, }, ) }

实现要点:

  • 透传并固定once:函数体只做一件事——把调用方传入的options展开,再强制并入once: true后转发给 Vue 的watch。这意味着「只触发一次并自动停止」的语义由 Vue 3 运行时保证,VueUse 本身不额外维护停止逻辑。
  • 重载与实现分离:三个export function声明只提供类型层面的重载签名,最后一个宽松实现签名(source: T, cb: any)负责实际运行,内部通过source as WatchSource<T>做类型断言,这是 VueUse 中一类简写 API 的常见写法。
  • 导出入口:watchOnce由 packages/shared/index.ts 统一导出,用户可通过import { watchOnce } from '@vueuse/core'直接引入。

值得强调的是,once: true的行为是 Vue 3 原生特性:在回调触发一次后,watcher 的 effect 会自动停止(stop),此后不再响应任何数据变化,也不需要手动调用返回的 stop 函数。

测试验证:回调确实只触发一次

仓库为watchOnce提供了对应的单元测试 packages/shared/watchOnce/index.test.ts:

import { describe, expect, it, vi } from 'vitest' import { nextTick, shallowRef } from 'vue' import { watchOnce } from './index' describe('watchOnce', () => { it('should work', async () => { const num = shallowRef(0) const spy = vi.fn() watchOnce(num, spy) num.value = 1 await nextTick() num.value = 2 await nextTick() expect(spy).toBeCalledTimes(1) }) })

该测试精确刻画了watchOnce的契约:

  • 先用shallowRef(0)创建监听源,并用vi.fn()作为回调 spy;
  • 第一次修改num.value = 1并等待一个 tick,回调应触发;
  • 第二次修改num.value = 2并等待一个 tick,回调不应再次触发;
  • 最终断言spy恰好被调用 1 次(toBeCalledTimes(1))。

这从测试角度实证了「回调触发一次后 watcher 即被停止」的核心行为。如果你在项目中遇到watchOnce回调被多次触发的疑问,也可以参照此测试结构在本地用vitest复现验证(仓库使用 Vitest,配置见 vitest.config.ts)。

实战场景示例

1. 等待异步状态首次就绪

import { watchOnce } from '@vueuse/core' import { ref } from 'vue' const isReady = ref(false) watchOnce(isReady, (val) => { if (val) { // 状态首次变为就绪,仅初始化一次 initOnce() } })

注意:watchOnce默认非 immediate,isReady在监听建立时若已为true,回调不会立即触发,只会在其之后发生变化时触发一次。若希望"当前值已满足即立即触发且只触发一次",可结合immediate: true使用。

2. 首次变化即停止的副作用

import { watchOnce } from '@vueuse/core' import { useRoute } from 'vue-router' const route = useRoute() watchOnce( () => route.params.id, (id) => { console.log('首次拿到路由参数:', id) }, )

当路由参数第一次变化时执行一次记录,之后监听自动失效,避免后续多次导航反复触发。

3. 组合 immediate 实现"一次性初始化"

import { watchOnce } from '@vueuse/core' import { ref } from 'vue' const user = ref<User | null>(null) // 用户信息一旦就位(或已就位时立即触发),只执行一次初始化 watchOnce(user, (val) => { if (val) initDashboard(val) }, { immediate: true })

由于options会原样透传给 Vue 的watch,你可以自由组合immediate、deep、flush等选项,让"一次性监听"适配更多初始化场景。

注意事项与最佳实践

  • once选项被类型层面禁用:options类型为Omit<WatchOptions<true>, "once">,不要(也无法在 TS 中)传入once来试图覆盖。
  • 回调可收到undefined旧值:首次触发时旧值为undefined,与原生watch首次回调行为一致,解构旧值前需留意。
  • 配合flush控制触发时机:默认flush: 'pre'(组件更新前触发),若需同步或后置触发,可传入flush: 'sync'/'post',与 Vue 原生watch完全一致。
  • 同类简写家族:VueUse 还提供watchImmediate、watchDeep、watchDebounced、watchThrottled等 watch 系简写(均可从@vueuse/core导入),它们遵循同样的"透传 + 内建单选项"设计哲学,掌握watchOnce的实现后即可举一反三。

总结

watchOnce是 VueUse 中"小而美"的代表:它以不到 10 行的实现,将 Vue 3 的oncewatcher 封装成一个语义清晰、类型完备的 API,让"只响应一次变化"的诉求从"记得传选项 + 手动管理停止"简化为一次函数调用。无论是等待异步状态、首次初始化,还是任何只需要单次响应的场景,watchOnce都是比手写watch更可读、更不易出错的选择。

  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

相关推荐

上一篇:如何从Google AI Python SDK迁移到新版本:完整迁移指南与最佳实践
下一篇:终极指南:如何突破微信单设备限制?WeChatPad实现多端登录新方法

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询