airi 项目中的 VueUse useTimeoutPoll:实现"任务完成后"再轮询的定时拉取方案
【免费下载链接】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
导读
useTimeoutPoll是 VueUse 提供的一个轮询工具组合式函数(composable),其核心语义是"上一次任务执行完毕之后,才重新计时并触发下一次轮询",从而天然避免异步任务积压与回调重入问题。在 airi 这类需要实时刷新聊天、认证令牌、模型供应商状态的多端应用(Web / Capacitor / Electron,见 apps 目录)中,它是构建可靠轮询逻辑的高频选择。读完本文,你将掌握useTimeoutPoll的完整 API、选项语义、与useIntervalFn/useTimeoutFn的取舍,以及它在仓库中的实际应用模式。
一、函数定位:一个"等做完再等下轮"的轮询器
useTimeoutPoll归类于 VueUse 的 Utilities 类别,官方对其行为的定义只有一句话:Use timeout to poll something. It will trigger callback after last task is done(用 timeout 去轮询某件事,并且只在最后一次任务结束后才触发下一次回调)。
这句话点出了它与传统setInterval轮询的本质区别:
setInterval按固定周期触发回调,不关心上一次回调是否执行完毕,异步任务一旦超过周期就会产生"回调堆叠"(callback stacking / reentrancy),导致并发请求风暴或状态错乱;useTimeoutPoll在内部把轮询实现为"执行任务 → 等待任务完成 → 再起一个 timeout → 再次执行任务"的链条,每次只存在一个在途任务,天然满足"串行轮询"的需求。
从类型声明可以看出,它的fn签名是() => Awaitable<void>,即允许传入async函数;框架会await该函数返回的 Promise,确认完成后才安排下一次调度。这正是它能"等到上次任务结束"的机制前提。
二、API 签名与选项详解
根据仓库内 useTimeoutPoll.md 中记录的 Type Declarations,完整签名如下:
export interface UseTimeoutPollOptions { /** * Start the timer immediately * * @default true */ immediate?: boolean /** * Execute the callback immediately after calling `resume` * * @default false */ immediateCallback?: boolean } export declare function useTimeoutPoll( fn: () => Awaitable<void>, interval: MaybeRefOrGetter<number>, options?: UseTimeoutFnOptions, ): Pausable参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
fn | () => Awaitable<void> | 每次轮询要执行的异步回调;可返回 Promise,框架会等待其 resolve 后再安排下一轮 |
interval | MaybeRefOrGetter<number> | 轮询间隔(毫秒)。既可以是普通数字,也可以是ref/reactive值或 getter 函数,意味着间隔可以被动态响应式地修改 |
options | UseTimeoutFnOptions | 选项对象,见下表 |
选项语义
| 选项 | 类型 | 默认值 | 行为 |
|---|---|---|---|
immediate | boolean | true | 是否立即启动计时器。设为false时不会自动开始轮询,需要手动调用resume() |
immediateCallback | boolean | false | 调用resume()时是否立即执行一次回调,而不是等待一个完整间隔。适合"恢复轮询时马上刷新一次数据"的场景 |
注意:
options类型标注为UseTimeoutFnOptions——这与同仓库 useTimeoutFn.md 中定义的选项接口一致。事实上useTimeoutPoll正是基于useTimeoutFn实现的:每次任务结束后调用内部start()重新开启一个 timeout,形成"串行轮询"的循环。
返回值:Pausable
useTimeoutPoll返回的是Pausable接口,包含三个成员:
isActive:当前是否处于轮询激活状态(只读响应式);pause():暂停轮询。暂停后不会立即中断在途任务,而是不再安排下一轮;resume():恢复轮询。若设置了immediateCallback: true,恢复时会立刻执行一次回调。
三、基础用法:串行拉取数据
官方 Usage 示例展示了一个最典型的场景——串行轮询一个异步数据源:
import { useTimeoutPoll } from '@vueuse/core' const count = ref(0) async function fetchData() { await new Promise(resolve => setTimeout(resolve, 1000)) count.value++ } // Only trigger after last fetch is done const { isActive, pause, resume } = useTimeoutPoll(fetchData, 1000)执行时序拆解:
- 组件挂载后,
immediate默认为true,立即执行第一次fetchData(); fetchData需要约 1 秒完成,期间不会有第二个回调触发;- 任务 resolve 后,框架再等待
1000ms间隔,然后执行第二轮; - 如此往复,直到
pause()被调用或组件卸载时自动清理。
如果改用setInterval(fetchData, 1000),当fetchData耗时超过 1 秒时,新的调用会在旧的还没结束时再次发起,造成并发执行与响应乱序。useTimeoutPoll通过"任务完成再计时"彻底规避了这个问题。
四、进阶用法:响应式间隔与恢复即刷新
4.1 响应式轮询间隔
interval参数支持MaybeRefOrGetter<number>,因此可以随业务状态动态调整轮询频率,而无需重建整个轮询器:
import { useTimeoutPoll } from '@vueuse/core' const pollingMs = ref(3000) const { resume } = useTimeoutPoll(async () => { await refreshStatus() }, pollingMs) // 某些场景下需要更频繁刷新时,直接修改 ref 即可 pollingMs.value = 5004.2 延迟启动 + 恢复时立即刷新
如果希望轮询在初始化时不立刻执行(例如等待某个前置条件就绪),可以关闭immediate;而在条件满足后resume()时立刻抓一次最新数据,则配合immediateCallback:
const { resume, pause, isActive } = useTimeoutPoll(fetchData, 5000, { immediate: false, // 先不自动开始 immediateCallback: true, // resume() 时立即执行一次 }) // 用户登录成功后开始轮询,且马上刷一次 watch(isLoggedIn, (v) => { if (v) resume() else pause() })这种"条件驱动 + 恢复即刷新"的组合,是管理后台仪表盘、在线状态同步等场景的常用写法。
五、与相关工具函数的对比与取舍
airi 仓库多个包都依赖@vueuse/core(见 packages/stage-ui/package.json 中的@vueuse/core、@vueuse/shared等 catalog 依赖),实际代码中围绕"定时/轮询"存在一个工具族,理解它们的差异有助于选型:
| 函数 | 底层机制 | 是否等待任务完成 | 典型用途 |
|---|---|---|---|
useTimeoutPoll | 任务完成后重启 timeout | 是 | 串行轮询,避免异步积压 |
useIntervalFn | setInterval | 否(周期固定触发) | 心跳、固定频率轻量刷新 |
useTimeoutFn | 单次setTimeout封装 | 是(执行一次) | 一次性延迟任务,如防抖式刷新 |
useRafFn | requestAnimationFrame | 否 | 逐帧动画、布局测量 |
useInterval | setInterval | 否 | 响应式计数器 |
一个直观的仓库实例是 packages/stage-layouts/src/composables/theme-color.ts:它使用useIntervalFn(() => {...}, 250, { immediate: false })以 250ms 的固定周期刷新theme-colormeta 标签,并在页面不可见(useDocumentVisibility检测到非 visible)时直接return跳过。这里任务极轻(读一次 computed style 并写一个属性),不存在异步堆积风险,因此固定间隔的useIntervalFn比串行的useTimeoutPoll更合适——这也印证了选型原则:任务可能耗时或相互依赖时选useTimeoutPoll,任务轻量且允许固定节奏时选useIntervalFn。
六、仓库实战:轮询在 airi 中的落地模式
虽然仓库中未直接出现useTimeoutPoll调用,但其"串行轮询/受控轮询"的设计思想在 airi 的 Vue 层被useIntervalFn、useTimeoutFn、useRafFn广泛实践,可为你应用useTimeoutPoll提供直接参照。
6.1 供应商周期性校验(useIntervalFn + 手动 resume)
在 packages/stage-ui/src/stores/providers/provider.ts 中,startPeriodicRuntimeValidation为每个启用的模型供应商按各自的intervalMs建立校验循环:
const loop = useIntervalFn(() => { void useProviderStore().validateProvider(providerId, { force: true }) }, intervalMs, { immediate: false, immediateCallback: false }) loop.resume() providerRevalidationLoops.set(providerId, loop)这里有一个细节:选项同时关闭了immediate与immediateCallback,随后手动调用loop.resume()启动,并通过providerRevalidationLoopsMap 保存句柄以便按需pause()。若把validateProvider换成可能耗时更长的异步任务,且担心校验请求互相重叠,把这里的useIntervalFn换成useTimeoutPoll即可获得"校验完成才发起下一轮"的串行语义——这正是useTimeoutPoll的典型替换场景。
6.2 认证令牌刷新(useTimeoutFn 一次性延迟)
packages/stage-ui/src/stores/auth.ts 从@vueuse/core引入useTimeoutFn,用于在令牌即将过期时安排一次性的延迟刷新动作。useTimeoutFn是useTimeoutPoll的"单轮版本":useTimeoutPoll内部就是"每次任务完成后再次调用useTimeoutFn的 start",理解useTimeoutFn的immediate/immediateCallback语义(同样默认true/false,见 useTimeoutFn.md)即可完全掌握useTimeoutPoll的调度细节。
6.3 按需轮询与"任务完成"思维(useRafFn 示例)
- packages/stage-ui/src/components/scenarios/chat/composables/use-virtualizer-scroll.ts 使用
useRafFn(..., { immediate: false })并返回{ pause, resume }:只有当存在待处理的滚动请求时才resume(),任务完成后立即pause()。这与useTimeoutPoll的"有需求才运转、无需求即停"理念一致; - packages/stage-ui/src/components/scenarios/chat/composables/use-chat-history-top-fade.ts 用
useRafFn把滚动、resize、虚拟列表变更合并到同一动画帧再读取布局,避免每帧重复测量。
这些模式共同说明:airi 的定时逻辑普遍遵循"最小化在途工作 + 显式暂停/恢复"的纪律,useTimeoutPoll正是该纪律在"需要周期性拉取外部状态"时的标准答案。
七、使用注意事项
- 组件卸载自动清理:
useTimeoutPoll基于 Vue 的effectScope/onScopeDispose机制,在组件或 effect scope 销毁时自动清除内部 timer,无需手动pause(); pause()不中断在途任务:正在执行的异步fn会跑完,只是不再安排下一轮;如需"取消当前任务",应在fn内部配合 AbortSignal 等机制自行处理;- 避免在
fn内抛出未捕获异常导致轮询中断:异常会使当前 Promise 提前 reject,轮询链随之停止,建议在fn内 try/catch 并做兜底; - 间隔过短且任务耗时长时:
useTimeoutPoll的实际周期 ≈ 任务耗时 +interval,如果对刷新频率有硬性上限要求,需结合业务侧的超时控制; - 响应式间隔变化:
interval为ref时修改会即时生效于后续轮询,但不会重置当前在途的 timeout,避免产生边界竞态。
八、小结
useTimeoutPoll以极小的 API 面(一个函数 + 两个选项 + 一个Pausable返回值)解决了轮询中最棘手的异步任务重入问题,是"串行轮询"的标准实现。在本仓库语境下,可将其视为useIntervalFn(固定节奏)与useTimeoutFn(单次延迟)之间的"任务完成后继续"折中方案;airi 中供应商周期性校验(provider.ts)、主题色采样(theme-color.ts)等场景的既有轮询模式,均可无缝迁移到useTimeoutPoll以获得更强的串行保证。
【免费下载链接】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),仅供参考