Pinia 可通过插件机制实现轻量级撤销重做功能。核心是仅对显式定义的业务状态子对象做快照,用栈管理历史,action 前调用 track() 记录,再通过 Object.assign 安全还原,支持快捷键与 UI 控制。

Pinia 本身不内置撤销重做(Undo/Redo),但凭借其响应式设计、可扩展的 store 结构和插件机制,可以轻量、清晰地实现「时间旅行」式状态回溯。核心思路是:记录每次关键状态变更前的快照,配合栈结构管理历史,再通过替换当前 state 实现回退与前进。
一、设计可快照化的 Store 结构
不是所有状态都需要被撤销——只对用户显式操作影响的核心业务状态做快照。建议将「可撤销状态」集中在一个对象中,便于统一序列化与比对:
- 避免直接对整个 $state 做深拷贝(性能差、易漏嵌套响应式);
- 用 toRaw() + JSON.parse(JSON.stringify()) 或 structuredClone()(现代浏览器)获取纯净快照;
- 将待快照字段显式定义为一个子对象,例如:state: { ui: { ... }, data: { list: [], filter: '' } } → 只对 data 做快照。
二、用插件封装撤销重做逻辑
推荐以 Pinia 插件形式注入,避免每个 store 重复逻辑。插件在 store 初始化时添加 undoStack、redoStack 和操作方法:
-
track():在关键 action 执行前调用(如
addItem()、updateItem()),保存当前 data 快照到 undoStack,并清空 redoStack; - undo():弹出 undoStack 顶部快照,赋值给目标 state 字段,并将原 state 推入 redoStack;
- redo():弹出 redoStack 顶部快照,赋值并推回 undoStack;
- 注意:赋值必须用 Object.assign(store.data, snapshot) 或解构更新,避免破坏响应式连接。
三、在 Action 中精准触发快照
撤销粒度由你控制。不要在 every mutation 都 track,而是在语义明确的用户动作结尾调用:
- ✅ 正确:
addTodo() { this.track(); this.todos.push(newTodo); } - ✅ 正确:
batchUpdate(items) { this.track(); this.items = items.map(transform); } - ❌ 避免:
setLoading(loading) { this.track(); this.loading = loading; }(UI 状态通常不需撤销)
四、支持快捷键与 UI 控制(Vue 组件中)
在组件 setup 中使用:
- 通过 store.canUndo / store.canRedo 控制按钮禁用状态;
- 监听键盘事件:
onKeyStroke('z', () => store.undo(), { prevent: true, eventName: 'keydown', target: window })(配合@vueuse/core); - 显示操作简史(如“撤销添加第5项”)?可在 track() 时传入描述字符串,存入栈中与快照配对。
不复杂但容易忽略:快照时机必须在状态变更之前,否则 undo 会回到“刚改完”的状态,失去意义。只要结构清晰、粒度合理、快照及时,Pinia 的撤销重做就能稳定支撑表单编辑、画布操作、配置编排等真实场景。

















