Skip to content
返回设计中心

撤销/重做与命令模式

通过命令栈可逆编辑 — 撤销弹出并反转,重做再应用。

UX 模式高级

为什么

无撤销的破坏性动作(删除、移动)迫使确认对话框,用户反射性关闭。动作后撤销更快(无对话框)且更安全(可恢复)— Gmail 模式。

Nielsen Norman Group 研究显示用户在离开前平均停留 10-15 秒。清晰的视觉层次和渐进式披露将任务完成率提高 40-60%。

何时使用

用于文档编辑器(文本、画布、表单构建器)、列表重排、批量操作、及任何确认对话框会拖慢流的破坏性动作。跳过一次性不可逆操作(支付、发邮件)。

如何做

1. 命令对象 — 每动作 { do(): void, undo(): void };do 应用、undo 反转 2. 栈 — do 后 push 命令;undo() 弹出并调 command.undo();redo() 再 push 3. 合并 — 合并连续相同命令(5 字符按键 → 1 '输入 "hello"' 命令) 4. 内存上限 — 留末 50 命令;旧编辑不可恢复(避免无界内存) 5. 快捷键 — Cmd/Ctrl+Z 撤销、Cmd+Shift+Z 重做;全局绑定非每输入 6. 快照替代 — 复杂状态存不可变快照而非 do/undo(更简、更耗内存) 7. 标签 — toast 或菜单项显将撤销何("撤销删除 3 项")

示例

interface Command { do(): void; undo(): void; } class History { private stack: Command[] = []; private redoStack: Command[] = []; push(cmd: Command) { this.stack.push(cmd); this.redoStack = []; } // 新动作清重做 undo() { const c = this.stack.pop(); if (c) { c.undo(); this.redoStack.push(c); } } redo() { const c = this.redoStack.pop(); if (c) { c.do(); this.stack.push(c); } } } // 删除命令 — do 删、undo 插回 function deleteCmd(list: T[], idx: number): Command { const removed = list[idx]; return { do: () => { list.splice(idx, 1); }, undo: () => { list.splice(idx, 0, removed); }, }; } // 用法 const history = new History(); const cmd = deleteCmd(items, 2); cmd.do(); history.push(cmd); // 删项 2 history.undo(); // 回来了 history.redo(); // 又删了 // 键盘 useHotkey("mod+z", () => history.undo()); useHotkey("mod+shift+z", () => history.redo());

常见问题

  • ⚠新动作不清重做栈 — 新编辑后重做应用陈旧命令
  • ⚠无界栈 — 画布应用 1000 编辑,内存涨至崩
  • ⚠命令直接变共享状态 — 中间有其他编辑时撤销反转错状态

最佳实践

  • ✓每新动作清重做栈 — 重做仅在撤销后立即有意义
  • ✓栈上限(50–100 命令)— 旧编辑不可恢复,内存有界
  • ✓显将撤销何 — toast/菜单 '撤销删除 3 项',用户更信

常见问题解答