单纯靠 watch 无法实现撤销重做,它仅负责捕获变更时机,真正依赖快照管理、双栈结构和受控赋值逻辑;需精准监听、防抖、静默屏蔽及深拷贝存档。

单纯靠 watch 无法实现撤销重做,它只是“发现变化”的耳朵,不是“记住历史”的大脑。真正起作用的是快照管理、栈结构和受控更新逻辑,watch 只负责在合适时机触发存档动作。
watch 的定位:捕获时机,不主导逻辑
watch 在 Vue 中是响应式系统的监听工具,适合在数据变更后执行副作用——比如保存一份当前状态快照。但它不参与决策“该不该记”“记哪一部分”“怎么还原”,这些必须由你显式设计。
- 监听目标要精准:通常监听整个可编辑对象(如
form或canvasData),开启deep: true,但需配合防抖防止拖拽/输入过程频繁触发 - 避免无限循环:撤销或重做操作本身会修改被监听的数据,必须用静默标志(如
muteNextWatcher = ref(false))临时屏蔽记录 - 不直接在
watch回调里改状态:否则可能引发递归调用或视图异常
核心三要素:快照 + 双栈 + 受控赋值
撤销重做的本质是状态回溯,不是逆向执行操作。因此关键不在“怎么变回去”,而在“原来是什么样子”。
-
快照生成:使用
structuredClone(toRaw(state))或JSON.parse(JSON.stringify())深拷贝关键字段(不必全量应用状态),推入undoStack -
双栈管理:一个
undoStack存历史快照,一个redoStack存被撤掉的状态;每次新操作前清空redoStack -
还原方式:撤销即从
undoStack弹出一项 → 赋值给响应式变量 → 将原值压入redoStack;重做则反向操作
防坑与优化实践
真实项目中容易忽略边界情况和性能损耗,这几个点直接影响体验稳定性:
-
限制栈长度:设最大步数(如 50 步),超出时自动
shift()老快照,避免内存持续增长 -
区分操作粒度:一次用户行为(如点击“添加组件”)可能触发多次 state 更新,纯
watch会记多条。建议结合手动saveSnapshot()调用更可控 -
跳过无效变更:例如表单初始加载、API 同步更新等非用户主动操作,可通过标记位(如
isSnapshotEnabled)控制是否记录 -
封装为 Composition API:把栈管理、静默开关、防抖逻辑抽成
useUndoable(state),多个组件复用,避免重复代码
什么时候该换方案?
当遇到以下情况时,watch-驱动的快照模式就力不从心了:
- 状态嵌套极深、快照体积大,克隆和存储开销明显
- 需要合并连续操作(如连输 10 个字符只算一步)
- 涉及异步流程(如保存后服务端返回再更新 UI),watch 无法区分来源
此时更适合命令模式(Command Pattern):只记录“做了什么”(如 { type: 'INSERT_TEXT', pos: 5, text: 'abc' }),体积小、可压缩、易序列化,也便于协同编辑场景扩展。

















