Airi 项目中的 VueUse useTimeout 实战指南:响应式延时、可控定时与源码级原理
【免费下载链接】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
useTimeout 是 VueUse 中位于 Animation(动画)分类下的响应式组合式函数,它把"延时后置为true"这个需求抽象成一个可响应、可控制、可复用的定时器。本文以 .agents/skills/vueuse-functions/references/useTimeout.md 为核心骨架,结合 Airi 仓库中stage-tamagotchi桌面端与stage-ui的实际调用代码,讲解 useTimeout 的完整 API、与useTimeoutFn的差异,以及它在真实项目中的落地姿势。
读完本文,你将掌握:如何用 useTimeout 写出"延时后自动变为 true 的响应式状态",如何通过controls暴露start / stop / isPending手动控制定时器,如何传入响应式时长与完成回调,以及底层Stoppable接口如何保证组件卸载时自动清理定时器、避免内存泄漏。
一、useTimeout 是什么:把 setTimeout 变成响应式状态
VueUse 中的useTimeout是一个"响应式布尔值",它在经过指定时间后从false变为true。它与原生setTimeout的核心区别在于:
- 原生
setTimeout是一次性、命令式的回调调用,状态变化无法被模板或watch直接感知; useTimeout返回的是ComputedRef<boolean>,可以像任何 Vue 响应式状态一样用于模板渲染、watch、computed派生,甚至参与动画过渡。
在 SKILL.md 的函数清单中,useTimeout 被归入 Animation 分类,描述为 "Reactive value that becomestrueafter a given time",其调用规则(Invocation)为AUTO,即:只要适用就应优先使用 VueUse 组合式函数而非手写自定义代码,以提升可读性、可维护性与性能。
最简用法
import { useTimeout } from '@vueuse/core' const ready = useTimeout(1000)1 秒之后,ready.value变为true。默认情况下定时器立即启动(immediate: true),所以你甚至不需要手动调用任何函数,只需在模板或逻辑里读取ready:
<script setup lang="ts"> import { useTimeout } from '@vueuse/core' const ready = useTimeout(1000) </script> <template> <p>{{ ready ? '已就绪' : '等待中…' }}</p> </template>二、开启 controls:start / stop / isPending 手动控制
很多场景下我们需要的不是一个"静默的倒计时",而是一个可暂停、可重启的定时器。传入{ controls: true }后,useTimeout 会额外返回start、stop、isPending三个控制项:
import { useTimeout } from '@vueuse/core' const { ready, start, stop, isPending } = useTimeout(1000, { controls: true }) // 检查定时器是否仍在计时 console.log(isPending.value) // true // 停止定时器(ready 将停留在 false) stop() // 重新开始/重启定时器 start()注意区分:
| 返回值 | 类型 | 含义 |
|---|---|---|
ready | ComputedRef<boolean> | 到点后变为true的响应式状态 |
start | () => void | 启动或重新启动定时器(非响应式版本,见下节) |
stop | () => void | 停止定时器 |
isPending | ComputedRef<boolean> | 定时器是否仍在计时中 |
这组返回值的类型签名对应Stoppable接口——VueUse 中凡是实现Stoppable的组合式函数,都承诺在组件卸载时自动调用stop()完成清理,这正是用它替换手写setTimeout不会产生内存泄漏的底层保证。
三、Options 参数一览
useTimeout的第二个参数是UseTimeoutOptions,它继承自UseTimeoutFnOptions并追加了两个字段:
| Option | Type | Default | Description |
|---|---|---|---|
controls | boolean | false | 是否暴露start、stop、isPending控制项 |
immediate | boolean | true | 是否立即启动定时器 |
callback | () => void | — | 定时器完成时触发的回调 |
三个选项的相互作用
controls只影响返回值形状:为false时返回单个ComputedRef<boolean>;为true时返回{ ready, start, stop, isPending }的组合对象(下文的类型声明会给出精确签名)。immediate: false配合controls: true是"手动门闩"模式:定时器不会自动启动,只有显式调用start()才开始计时。这在"用户触发后才开始等待"的交互中非常有用。callback与ready并不冲突:到点时两者都会触发/置位,你可以用ready驱动 UI,用callback执行副作用(比如发出埋点、切换路由、释放资源)。
四、Callback:到点后的副作用钩子
import { useTimeout } from '@vueuse/core' useTimeout(1000, { callback: () => { console.log('Timeout completed!') }, })callback的类型是() => void,也就是UseTimeoutOptions中callback?: Fn所指的通用函数类型。它是轻量的"完成通知"钩子,适合在倒计时结束瞬间执行一段与 UI 状态解耦的逻辑。
五、响应式时长:interval 可以是 ref 或 getter
useTimeout 的时长参数类型是MaybeRefOrGetter<number>,这意味着你不仅可以传字面量,还可以传ref或 getter 函数,让时长本身具备响应性:
import { useTimeout } from '@vueuse/core' const duration = ref(1000) const ready = useTimeout(duration) // 修改时长(在启用 controls 时,只影响未来启动的定时器) duration.value = 2000注意原文档明确提示的行为细节:修改时长只影响"未来的"定时器。如果定时器已经在计时中,中途修改duration不会打断当前这一次计时;如果你需要"以新时长重新开始",正确做法是更新 ref 后显式调用start()重启。这也解释了为什么"响应式时长"通常与controls: true搭配使用才最有价值——单纯的ready布尔值拿到的是旧时长下的一次性结果。
六、Type Declarations 精读:重载签名与兼容别名
原文档给出了完整类型声明,这里逐段拆解其含义:
export interface UseTimeoutOptions< Controls extends boolean, > extends UseTimeoutFnOptions { /** * Expose more controls * * @default false */ controls?: Controls /** * Callback on timeout */ callback?: Fn }UseTimeoutOptions通过泛型Controls把controls的取值直接映射到返回类型上,从而实现"传了controls: true就自动得到完整控制对象"的编译期类型推断。
export type UseTimeoutReturn = | ComputedRef<boolean> | ({ readonly ready: ComputedRef<boolean> } & Stoppable)UseTimeoutReturn是联合类型:要么是纯ComputedRef<boolean>,要么是"ready+Stoppable"的组合。Stoppable正是提供start/stop/isPending的来源。
/** * @deprecated use UseTimeoutReturn instead */ export type UseTimoutReturn = UseTimeoutReturn注意一个历史遗留:UseTimoutReturn(少写了一个字母o)是官方标记@deprecated的旧别名,类型上与UseTimeoutReturn完全等价。如果你是 VueUse 早期版本的迁移用户,看到这个别名不必困惑,新代码应统一使用UseTimeoutReturn。
两个重载的函数签名:
export declare function useTimeout( interval?: MaybeRefOrGetter<number>, options?: UseTimeoutOptions<false>, ): ComputedRef<boolean> export declare function useTimeout( interval: MaybeRefOrGetter<number>, options: UseTimeoutOptions<true>, ): { ready: ComputedRef<boolean> } & Stoppable从重载可以看出:第一个签名中interval是可选的(省略时默认0,立即置true);第二个签名要求interval必填且controls: true。这一设计让"无参场景"(默认返回一个会立刻/按时变为 true 的 ref)和"可控场景"(需要 interval)在类型上被严格区分。
七、源码级原理:useTimeout 与 useTimeoutFn 的分工
useTimeout 本质上是useTimeoutFn(useTimeoutFn.md)之上的一个状态化封装。看useTimeoutFn的声明就能理解两者关系:
export declare function useTimeoutFn<CallbackFn extends AnyFn>( cb: CallbackFn, interval: MaybeRefOrGetter<number>, options?: UseTimeoutFnOptions, ): UseTimeoutFnReturn<CallbackFn>其中UseTimeoutFnOptions提供:
| Option | Type | Default | Description |
|---|---|---|---|
immediate | boolean | true | 是否立即启动定时器 |
immediateCallback | boolean | false | 调用start()时是否立即执行回调 |
对比两条使用路径:
useTimeoutFn(cb, interval):核心职责是"到点执行回调",返回isPending/start/stop,不维护布尔状态;useTimeout(interval, { callback }):在内部用useTimeoutFn把"到点"翻译成ready = true的响应式状态,callback则是透传给你的副作用。
因此,如果你的目标是"等待一段时间后执行一段代码",useTimeoutFn更直接;如果目标是"让某个布尔状态在延时后翻转并参与渲染/watch",useTimeout是更贴合语义的选择。本文聚焦的 useTimeout 属于前者之上的"状态化"抽象。
Airi 中的真实对照:controls 模式的完整范本
Airi 的桌面端舞台(stage-tamagotchi)在控制岛(Controls Island)角落搬迁动画中,用三个useTimeoutFn(同样基于Stoppable控制模型)串联了"离开旧角落 → 准备入场 → 抵达新角落"的阶段机:
- controls-island-root.vue
const { start: finishArrival, stop: stopArrival } = useTimeoutFn(() => { motionPhase.value = 'idle' relocationTarget.value = undefined }, placementArrivalDurationMs, { immediate: false }) const { start: startArrival, stop: stopEnterPreparation } = useTimeoutFn(() => { motionPhase.value = 'arriving' finishArrival() }, placementEnterPreparationMs, { immediate: false }) const { start: finishLeave, stop: stopLeave } = useTimeoutFn(() => { if (!relocationTarget.value) { motionPhase.value = 'idle' return } dock.value = relocationTarget.value motionPhase.value = 'entering' startArrival() }, placementLeaveDurationMs, { immediate: false })这段代码把immediate: false、start()、stop()用到了极致:三个定时器都不自动启动,由relocate()在用户拖动窗口触发搬迁时先stopRelocation()取消所有未完成任务,再按阶段逐个start()接力。这正是文档中"immediate: false配合controls实现手动门闩"的真实写照。
Airi 中的响应式时长范本:OIDC Token 定时刷新
stage-ui 的认证 Store 用useTimeoutFn实现了 OIDC access token 的"寿命 80% 时刻"刷新调度,完美示范了"响应式 ref 作为时长参数"的用法:
const refreshDelayMs = ref(0) const { start: startRefreshTimer, stop: stopRefreshTimer } = useTimeoutFn( () => { void useAuthStore().refreshTokenNow() }, refreshDelayMs, { immediate: false }, ) function scheduleTokenRefresh(expiresInSeconds: number): void { stopRefreshTimer() // Guard against missing/invalid lifetimes(例如 token 响应缺少 expires_in)。 // useTimeoutFn 在 NaN/<=0 延时下会立即触发并造成刷新死循环——这里直接跳过调度。 if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) return // 在生命周期 80% 的时间点刷新 refreshDelayMs.value = expiresInSeconds * 0.8 * 1000 startRefreshTimer() }三个要点值得对照前文吸收:
immediate: false+start():定时器只在scheduleTokenRefresh里手动启动,避免 Store 初始化时误触发刷新;- 响应式时长:
refreshDelayMs是ref(0),每次刷新成功后按新的expires_in更新再start(),同一套代码复用于不同寿命的 token; - 边缘守卫:注释里明确提到 "useTimeoutFn with NaN/<=0 delay would fire immediately",即非有限/非正时长会立刻触发回调——这与前文"interval 省略时默认 0"的类型语义呼应,提醒开发者对动态时长必须做有效性校验。
这个案例也直接印证了 SKILL.md 的核心主张:优先用 VueUse 组合式函数而非手写setTimeout——useTimeoutFn在组件/Store 销毁时自动清理定时器,代码里不需要再手动clearTimeout。
八、与相邻定时类函数的选型对比
在 VueUse 中与 useTimeout 相邻的定时/动画函数还有useInterval、useIntervalFn、useRafFn,它们同属 Animation 分类(见 SKILL.md 中 Animation 一节):
| 函数 | 语义 | 典型场景 |
|---|---|---|
useTimeout | 延时后布尔值翻转为true | 一次性等待、就绪门闩、延时展示 |
useTimeoutFn | 延时后执行回调(Stoppable) | 一次性副作用、节流式调度 |
useInterval | 每隔 N 毫秒递增的响应式计数器 | 倒计时、轮询进度展示 |
useIntervalFn | setInterval的可控包装 | 周期性任务 |
useRafFn | 每帧调用一次回调 | 动画循环、渲染节流 |
Airi 中也能找到后者在用的实例:例如 use-chat-history-top-fade.ts 用useRafFn把"滚动到底部后淡出顶部历史"的更新推迟到下一帧,避免布局抖动;use-virtualizer-scroll.ts 也用useRafFn做滚动渲染节流。选择依据很简单:一次性延时看 useTimeout/useTimeoutFn,周期性任务看 useInterval/useIntervalFn,逐帧动画看 useRafFn。
九、实战要点与陷阱清单
综合原文档、类型声明与仓库证据,这里给出 useTimeout 的实战 checklist:
- 默认自动启动:
immediate默认为true,只想要"到点翻 true"就一行搞定;需要手动触发则必须controls: true+immediate: false。 - controls 三件套:
start()可重启计时,stop()立即中止,isPending可读是否在计时;它们来自Stoppable接口,组件卸载时自动清理。 - 响应式时长只影响未来定时器:中途改 interval 不会打断当前计时,需要新时长生效就改 ref 后重新
start()。 - 动态时长要防非法值:参考 auth.ts 的做法,对来自外部(token 响应、接口数据)的时长先做
Number.isFinite与> 0校验,避免 NaN/负值导致定时器立即触发。 - callback 与 ready 分工:UI 用
ready驱动,副作用放callback,保持状态与行为的解耦。 - 与 useTimeoutFn 的选择:需要的是"状态"选 useTimeout,需要的是"到点执行函数"选 useTimeoutFn;两者共享
Stoppable控制模型,可互相替代实现。 - 旧别名注意:
UseTimoutReturn已废弃,新代码一律写UseTimeoutReturn。
十、延伸阅读
- 函数索引与调用规则:.agents/skills/vueuse-functions/SKILL.md
- 本文主文档(含完整类型声明):useTimeout.md
- 底层实现对照:useTimeoutFn.md
- 仓库实战:控制岛动画阶段机 controls-island-root.vue、OIDC 定时刷新 auth.ts
【免费下载链接】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),仅供参考