Vue3 可通过快照+双栈+响应式同步实现轻量可靠的撤销重做功能;核心是在操作结束时用 structuredClone(toRaw(state)) 保存不可变快照,用 ref 管理 undoStack/redoStack,替换 state 触发视图更新。

Vue3 的响应式数据本身不自带撤销重做(Undo/Redo)能力,但借助其响应式系统 + 有限状态快照 + 栈结构,可以轻量、可靠地实现该功能,无需引入大型状态管理库。
核心思路:快照 + 栈 + 响应式同步
撤销重做本质是「在历史状态间跳转」。关键不在监听所有变化,而在于:在用户认为“一次操作”结束时,保存当前响应式状态的**不可变快照**;用两个栈分别管理已执行操作(undoStack)和已撤销操作(redoStack);跳转时用快照**替换整个响应式对象**,触发 Vue 自动更新视图。
注意:不能直接 push 响应式对象(会污染快照),必须深克隆或结构化提取(推荐 toRaw + JSON.parse(JSON.stringify()) 或 structuredClone(现代浏览器))。
基础实现:用 ref + shallowRef 管理栈
避免对快照做响应式处理(否则体积大、性能差),用 shallowRef 存储普通 JS 对象快照,用 ref 管理栈数组:
递归分析 Vue 项目组件依赖,从入口文件生成组件层级图,支持 Vue 2/3,输出组件名、文件路径和属性。适用于分析组件结构、排查依赖或了解项目架构。
立即学习“前端免费学习笔记(深入)”;
- 定义 undoStack 和 redoStack:`const undoStack = ref([]); const redoStack = ref([]);`
- 保存快照:`undoStack.value.push(structuredClone(toRaw(state)));`(state 是你的 reactive 对象)
- 执行撤销:`if (undoStack.value.length > 0) { redoStack.value.push(structuredClone(toRaw(state))); state = reactive(undoStack.value.pop()); }`
- 执行重做:`if (redoStack.value.length > 0) { undoStack.value.push(structuredClone(toRaw(state))); state = reactive(redoStack.value.pop()); }`
封装成可复用的 Composable
把逻辑抽成 useUndo Hook,支持任意 reactive 对象:
- 接收初始 state 和可选最大历史步数(如 50 步),自动裁剪过长栈
- 暴露
save(手动存档)、undo、redo、canUndo、canRedo等方法/状态 - 内部用 watch 监听关键操作完成(例如表单提交、按钮点击后调用 save),而非监听每个字段变化——更可控、更符合用户预期
注意事项与优化点
实际使用中需留意几个细节:
- 避免高频自动保存:不要在 input@input 中每输一个字就 save,应在 blur、Enter 或显式「确认」操作后触发
- 处理嵌套响应式对象:确保 toRaw 后再克隆,否则仍含 proxy,影响快照纯净性
- 大型对象考虑增量快照:若 state 极大(如富文本编辑器全量内容),可只记录 diff(需额外实现),但多数场景全量快照更简单可靠
- 配合 UI:禁用按钮根据 canUndo/canRedo 动态切换,视觉反馈更友好
不复杂但容易忽略。用好 toRaw + structuredClone + shallowRef,几行代码就能让 Vue3 表单、配置面板、简易画布等场景拥有专业级撤销体验。

















