news 2026/10/16 7:03:54

airi 项目中的 VueUse useThrottledRefHistory:为 ref 状态接入节流式撤销/重做历史

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
airi 项目中的 VueUse useThrottledRefHistory:为 ref 状态接入节流式撤销/重做历史
  • 人工智能
  • AI Agent
  • MCP 服务
  • 工具调用
  • GUI 自动化
  • 语音
  • 数字人
  • 交互助手

【免费下载链接】airi

💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。

项目地址:https://gitcode.com/moeru-ai/airi
点击查看免费下载

本篇技术指南以 airi 仓库中vueuse-functions技能包 的useThrottledRefHistory参考文档 为骨架,讲解 VueUse 中这个「带节流过滤的useRefHistory简写」组合式函数:它如何在一个 ref 的值高频变化时,按节流节奏沉淀历史快照,并顺带提供undo/redo能力。读完本文,你将掌握它的参数语义、与useDebouncedRefHistory/useManualRefHistory的取舍,以及如何在类似 airi 这类 Vue 3 应用中为编辑器、设置表单等场景落地可撤销的本地状态。

函数定位:State 分类下的节流版 ref 历史记录

useThrottledRefHistory属于 VueUse 的 State 分类。在 airi 仓库的 SKILL.md 函数索引表 中,它的定位是:

Shorthand foruseRefHistorywith throttled filter(带节流过滤器的useRefHistory简写),调用规则为AUTO(在适用场景下自动使用)。

这意味着它是 VueUse 三大「ref 历史记录」函数之一,三者共享同一套返回结构与大部分选项:

函数过滤方式快照时机
useRefHistory无过滤,基于watch触发每个 tick 内异步批量提交
useThrottledRefHistorythrottleFilter(节流)节流窗口内最多按前/后沿取样
useDebouncedRefHistorydebounceFilter(防抖)值停止变化指定时间后才取样

三者的类型定义都基于useRefHistory:UseThrottledRefHistoryReturn直接等价于UseRefHistoryReturn<Raw, Serialized>,区别只在于把原本的eventFilter选项替换成了throttle与trailing两个专有选项。这也是它名字中 "Shorthand" 的含义——内部本质上就是把useRefHistory挂上一个节流事件过滤器。

快速上手:为计数器接入节流历史

文档给出的核心用法如下,注意这里显式传入deep: true与throttle: 1000:

import { useThrottledRefHistory } from '@vueuse/core' import { shallowRef } from 'vue' const counter = shallowRef(0) const { history, undo, redo } = useThrottledRefHistory(counter, { deep: true, throttle: 1000 })

行为上,这个函数在计数器值被修改后的瞬间立刻采集第一份快照,之后在 1000ms 的节流窗口内不再重复记录,直到窗口结束后(若期间仍有变更)再采集第二份快照。也就是说,它同时具备「前沿立即生效 + 后沿兜底捕获」的节流语义,避免高频写入把 history 撑爆,同时保证最终状态不会丢失。

history是一个响应式数组,最新快照在数组头部(history[0]为当前值,越靠后越旧);调用undo()会把 ref 回退到上一份快照,redo()则反向重做。默认情况下历史容量不设上限,可用clear()显式清空。

参数详解:UseThrottledRefHistoryOptions全解

文档给出的类型声明中,选项类型是UseRefHistoryOptions去掉eventFilter后,再并入节流专属参数:

export type UseThrottledRefHistoryOptions<Raw, Serialized = Raw> = Omit< UseRefHistoryOptions<Raw, Serialized>, "eventFilter" > & { throttle?: MaybeRef<number> trailing?: boolean } export type UseThrottledRefHistoryReturn< Raw, Serialized = Raw, > = UseRefHistoryReturn<Raw, Serialized>

节流专属参数

参数类型默认值说明
throttleMaybeRef<number>—节流间隔(毫秒)。MaybeRef意味着可以传一个 ref,间隔本身也能是响应式的;传 0 时退化为每次变更都记录。
trailingbooleantrue是否在节流窗口结束时捕获尾随快照。关闭后只保留节流窗口开头的快照,末尾的最终状态不会被单独记录。

从useRefHistory继承的通用选项

throttle/trailing之外,其余选项与 useRefHistory 完整参考 完全一致,同样适用于本函数:

  • deep?: boolean(默认false):对象/数组场景下,直接修改其属性不会改变引用,不会触发提交;传入true才会深度监听并为每份快照创建克隆。文档示例中deep: true正是为计数器这种基本类型之外的场景准备的保险写法。
  • capacity?: number:最多保留的历史条数,默认无限;超出后旧记录被丢弃。
  • clone?: boolean | CloneFn<Raw>:快照时克隆值。默认仅内嵌JSON.parse(JSON.stringify(x))这种最简克隆;可换成structuredClone或cloneDeep等完整克隆函数。deep: true时相当于自动启用克隆。
  • dump?: (v: Raw) => Serialized/parse?: (v: Serialized) => Raw:自定义序列化与反序列化。例如dump: JSON.stringify, parse: JSON.parse,可以让快照直接以字符串形态存入localStorage,并在撤销时省去一次克隆。
  • shouldCommit?: (oldValue, newValue) => boolean:每次变更提交前的裁决函数,返回false则跳过本次记录,可用于按值变化量过滤无意义的历史点。

关于flush

useRefHistory系列默认沿用 Vue watcher 的'pre'flush 时序,同一 tick 内的多次修改会被合并成一条历史记录;若需要在同一 tick 内产生多条记录,可改用commit()手动提交。节流版本同样继承这一行为,意味着节流与异步批处理是叠加生效的——既要等待 Vue 的批量刷新,又要受节流窗口约束。

trailing的行为语义与底层原理

trailing之所以单独出现在选项里,是因为节流过滤器(throttle)天然包含「前缘(leading)/ 后缘(trailing)」两个触发边。可以参考同技能包中 watchThrottled 的选项说明 来理解:

组合效果
leading: true, trailing: false只在节流周期开始触发一次,高频期间的中间值全部丢弃
leading: false, trailing: true只在节流周期结束时记录最终值
leading: true, trailing: true(VueUse 默认)周期开始立即记录,结束后再补一条最终值

在useThrottledRefHistory中,trailing是唯一暴露出来的边控制项,前缘默认开启(即「值一变就立刻记录第一份快照」)。这与仓库文档描述的「takes the first snapshot right after the counter's value was changed and the second with a delay of 1000ms」完全吻合:第一条来自前缘、第二条来自后缘。

从原理上推断(参见同包 refThrottled 参考 与watchThrottled的 "How It Works" 一节),节流函数本质上等价于:

watchWithFilter(source, commit, { eventFilter: throttleFilter(1000), })

即通过eventFilter管线把 Vuewatch的每次触发交给throttleFilter裁决,从而在底层复用同一套过滤器基础设施。

节流 vs 防抖 vs 手动提交:怎么选

airi 技能包把三个「历史记录」函数放在同一分类下(State 分类索引),实际选型建议:

  • 连续高频、但需要保留过程中间态(如拖拽滑块、画布移动、实时参数调节):用useThrottledRefHistory,节流确保历史点密度可控,前缘快照保证「拖到哪都能回退到最近的稳定点」。
  • 只关心「停下来之后」的结果(如输入框停止输入后再记录):用useDebouncedRefHistory(参考文档),它在值开始变化 1000ms 后才取样,中间态一律不记录,历史更干净。
  • 完全掌控提交时机:用useManualRefHistory,仅在业务逻辑认为「这一步是完整操作」时调用commit(),例如一次批量修改只产生一条历史点。
  • 无节流、全量记录:直接用useRefHistory。

airi 仓库中的实际应用与源码证据

airi 是一个大量使用@vueuse/core的 Vue 3 全栈 monorepo。在根目录 pnpm-workspace.yaml 中,catalog 统一声明了'@vueuse/core': ^14.4.0(以及@vueuse/shared、@vueuse/motion),各子应用与包(如 apps/stage-tamagotchi/package.json、apps/stage-web/package.json、packages/electron-vueuse/package.json)均通过catalog:引用,说明 VueUse 生态是该项目响应式基础设施的一部分。

仓库中与本主题最直接相关的实战案例是 Live2D 关键帧编辑器keyframe-editor.vue:

// keyframe-editor.vue (L65-L68) const { canRedo, canUndo, clear, commit, redo, undo } = useManualRefHistory(project, { clone: structuredClone, capacity: 100, })

这里用structuredClone对 Live2D 运动工程对象做完整克隆、用capacity: 100限制历史深度,并通过updateProject/replaceProject在每次增删关键帧、移动时间点后显式commit()——这正是上一节「手动提交」选型的现实写照。它验证了useRefHistory系列返回对象在真实编辑器中的完整用法(canUndo/canRedo驱动按钮禁用态,clear用于清空时间线)。如果你需要的是「拖拽关键帧期间自动按节流记录」,把这里的useManualRefHistory换成useThrottledRefHistory(project, { clone: structuredClone, throttle: 200 })即可得到同样的撤销/重做界面,但省去手动commit()调用。

另一个证据位于语音提供方设置页openai-compatible-audio-speech.vue(第 67 行),源码中留有注释:

// TODO: use `useRefHistory` for this

它正在用一段手写的watch+ 手工同步逻辑实现「provider 配置被外部重置时回写本地 ref」的复位功能。这恰恰说明:这类「记录变更前状态、支持回退/重置」的需求,正是useRefHistory/useThrottledRefHistory家族的典型用武之地——当高频设置项需要可撤销历史或重置快照时,一行组合式函数即可替代手写 watcher。

完整类型声明

最后给出文档收录的完整函数签名(去掉对外部文档站的跳转,本文以仓库内参考文档为准):

export type UseThrottledRefHistoryOptions<Raw, Serialized = Raw> = Omit< UseRefHistoryOptions<Raw, Serialized>, "eventFilter" > & { throttle?: MaybeRef<number> trailing?: boolean } export type UseThrottledRefHistoryReturn< Raw, Serialized = Raw, > = UseRefHistoryReturn<Raw, Serialized> export declare function useThrottledRefHistory<Raw, Serialized = Raw>( source: Ref<Raw>, options?: UseThrottledRefHistoryOptions<Raw, Serialized>, ): UseThrottledRefHistoryReturn<Raw, Serialized>

其中返回对象UseRefHistoryReturn除history、undo、redo外,还包含canUndo、canRedo、clear()、commit()、batch(fn)、pause()、resume(commit?)、isTracking与dispose(),完整语义见 useRefHistory 参考文档。综上,useThrottledRefHistory的核心价值在于:以最小的心智成本,为高频变化的响应式状态提供「有节流、可撤销、可重做」的历史能力——在 airi 这类需要实时调节(Live2D 动作、语音参数、画布元素)的 Vue 3 应用中,它比手写 watcher + 快照数组的方案更简洁、更不易出错。

  • 人工智能
  • AI Agent
  • MCP 服务
  • 工具调用
  • GUI 自动化
  • 语音
  • 数字人
  • 交互助手

【免费下载链接】airi

💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。

项目地址:https://gitcode.com/moeru-ai/airi
点击查看免费下载

相关推荐

上一篇:Visual Studio Code 的 Azure 扩展全景:用 Azure Tools 扩展包从开发到部署一站式搞定云服务
下一篇:Marshal实战案例:构建一个完整的JSON解析流程

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 7:02:59

MATLAB 64位调用REFPROP物性计算:开箱即用配置与避坑指南

简介&#xff1a;这份资源面向需要在MATLAB环境中调用REFPROP进行流体热力学与传输性质计算的工程师、科研人员及高年级学生&#xff0c;尤其适合64位Windows平台下从事制冷、热交换器设计或化工流程模拟的用户。压缩包共117个文件&#xff0c;约1.64MB&#xff0c;以105个fld流…

作者头像 李华
网站建设 2026/10/16 7:02:59

SAP销售订单计划行确认数量批量更新:V_V2与MASS实操指南

去年同期&#xff0c;我被一票计划订单搞得焦头烂额&#xff1a;三百多张销售订单&#xff0c;每张都带两三条计划行&#xff0c;确认数量和实际交付计划对不上。挨个用VA02打开改&#xff0c;眼睛先报废&#xff0c;效率也完全没法看。一开始我想用标准事务码MASS直接批量改&a…

作者头像 李华
网站建设 2026/10/16 7:02:58

插件投毒漏洞复盘与供应链安全扫描实践

3月13日一早&#xff0c;应急群里的“高危”标红就把我拉回了屏幕前。开源自动化框架“龙虾”&#xff08;业内习惯叫龙虾&#xff09;被曝出高危漏洞&#xff0c;攻击者可以在插件安装过程中悄然植入恶意代码——俗称插件投毒。当天下午&#xff0c;某安全团队上线了“硬壳扫描…

作者头像 李华
网站建设 2026/10/16 7:02:52

12亿联合领投背后:智能电动车产业链深度绑定意味着什么

12个亿人民币&#xff0c;三家联合领投。这条融资消息放到智能电动车圈子里&#xff0c;第一反应不是“终于有钱了”&#xff0c;而是“这车厂要开始交卷了”。资金到位只是一个信号&#xff0c;真正值得琢磨的&#xff0c;是站在钱后面的那三股力量分别代表着什么&#xff1a;…

作者头像 李华
网站建设 2026/10/16 7:01:39

AI编码助手如何让MVP验证周期缩短55%?从工作流重构到效率倍增

1. 为什么 MVP 验证总是慢半拍&#xff1a;先搞清楚卡点在哪先说结论&#xff1a;MVP 验证周期长短&#xff0c;根本不取决于你写代码的速度&#xff0c;而取决于你能不能快速把“想法”变成一个可以被真实用户触碰、操作、反馈的东西。我见过太多团队把时间耗在写不必要的代码…

作者头像 李华