响应式数据撤销重做需通过不可变快照与双栈实现:操作完成后用 toraw + structuredclone 深拷贝存入 undostack,跳转时全量替换 reactive 状态;快照须用 shallowref 包裹防响应式污染,操作边界应设在 blur/enter/mouseup 等明确事件点。

响应式数据本身不带“撤回”能力,但可以像 Excel 那样,在用户完成一次有意义的操作后,保存一个干净、不可变的状态快照,并用两个栈(undoStack / redoStack)管理跳转。关键不是监听每个字段变化,而是定义好“一次操作”的边界,再配合深克隆和响应式替换。
快照必须是不可变的,且剥离响应式代理
Vue 的 reactive 对象内部是 Proxy,直接 push 到栈里会导致后续修改污染历史状态。正确做法是:
- 先用
toRaw(state)获取原始 JS 对象 - 再用
structuredClone()深拷贝(现代浏览器支持,Safari 16.4+ 起可用) - 避免用
JSON.parse(JSON.stringify()),它会丢掉 Date、Map、Set、函数等类型 - 快照存进栈时,用
shallowRef包裹,防止 Vue 对快照做响应式处理,节省内存和性能
双栈结构与状态替换逻辑
撤销重做本质是状态跳转,不是差量更新。每次跳转都应完整替换整个响应式对象,让 Vue 自动触发视图更新:
本文档主要介绍如何通过python对office excel进行读写操作,使用了xlrd、xlwt和xlutils模块。另外还演示了如何通过Tcl tcom包对excel操作。感兴趣的朋友可以过来看看
-
保存快照:操作完成后调用
save()→undoStack.value.push(structuredClone(toRaw(state))) - 执行撤销:弹出 undoStack 最后一项 → 克隆当前 state 推入 redoStack → 用弹出项重建 reactive 状态
- 执行重做:弹出 redoStack 最后一项 → 克隆当前 state 推入 undoStack → 用弹出项重建 reactive 状态
- 栈长度超限时(如最多 50 步),自动
shift()踢掉最早一条,避免内存膨胀
操作边界要明确,避免高频误存
不能在输入框 @input 里每打一个字就 save,那样栈会爆炸,且不符合用户对“一次操作”的直觉:
- 表单场景:在
blur、Enter或「提交」「确认」按钮点击后触发save() - 富文本编辑器:监听自定义的
change事件(如 Tiptap 的transaction提交后) - 画布类应用:在鼠标抬起(
mouseup)、拖拽结束或指令执行完毕后存档 - 若连续操作间隔
封装成可复用的组合式函数
把逻辑收进 useUndo,接收初始 state 和最大步数,暴露清晰 API:
-
save():手动存档(推荐) -
undo()/redo():执行跳转 -
canUndo/canRedo:计算属性,用于禁用按钮 - 内部用
watch监听关键事件完成,而非监听 state 变化本身——更可控、语义更准










