状态撤销重做需用两个ref管理undo/redo栈,存入toRaw后structuredClone的不可变快照,避免响应式污染;关键在操作完成时保存,而非暂存上一次状态。

Ref 状态缓存实现撤销重做,核心不是“暂存上一次操作状态”本身,而是**在操作完成时,把当前整个响应式状态的不可变快照保存下来,并用两个 ref 分别管理历史栈和重做栈**。直接把 reactive 对象 push 进 ref 数组会污染快照,必须剥离响应式再深克隆。
为什么不能直接用 ref 存 reactive 对象?
Vue 的 reactive 是 Proxy 包裹的对象,直接存进栈里,后续修改 state 会导致历史快照也被改——因为它们共享同一份响应式代理后的引用。所以必须先用 toRaw() 提取原始 JS 数据,再用 structuredClone() 深拷贝,生成真正独立、不可变的快照。
用 ref 管理双栈结构
两个 ref 分别承载 undo 和 redo 历史记录,值为普通数组,里面存的是纯 JS 对象(非响应式):
- undoStack = ref([]):保存用户执行过的每一步操作前的状态快照(从旧到新排列)
- redoStack = ref([]):保存被撤销的操作对应的状态快照,供重做使用
- 快照本身建议用 shallowRef 包裹(可选),避免 Vue 尝试对快照做响应式处理,节省性能
关键操作时机与 ref 更新逻辑
撤销重做的可靠性取决于“什么时候存”,而不是“存了多少次”。ref 的值更新要发生在明确的操作边界之后:
- 表单编辑:监听 blur、Enter 键或「确认」按钮点击后调用
saveSnapshot() - 画布/编辑器:在 mouseup、dragend 或 action.commit() 后触发保存
- 每次 save 时:
undoStack.value.push(structuredClone(toRaw(state))),同时清空redoStack.value = [] - undo 时:弹出
undoStack.value.pop(),把当前 state 克隆后推入 redoStack,再用弹出项重建 reactive 状态
封装成可复用的 ref 工具函数
可以把栈逻辑和状态替换封装成一个组合式函数,返回带控制能力的 ref:
- 输入:初始 reactive 状态 + 最大步数限制(如 50)
- 内部用 ref 管理两个栈,暴露
save()、undo()、redo()、canUndo(computed)等 - 所有状态跳转都通过
state = reactive(poppedSnapshot)完成,确保视图自动响应


















