await 本身不能直接构建撤销重做状态机,它仅暂停异步执行;真正实现依赖命令对象(含 execute/undo/redo)、双栈管理(historystack/redostack)及快照机制,await 仅用于命令方法内部等待异步任务。

await 本身不能直接构建撤销重做状态机,它只是暂停异步函数执行、等待 Promise 解析,并不记录操作历史或管理状态快照。所谓“完美复刻同步命令模式的撤销重做”,关键不在 await,而在于你如何用 await 配合 命令对象、状态快照和栈结构来组织逻辑。
命令对象必须封装可逆操作
每个异步操作(如保存文件、更新数据库、切换 UI 状态)需包装成一个实现 execute()、undo()、redo() 的命令类。await 用于在 execute/undo/redo 内部等待异步任务完成,但命令本身要持有必要上下文(如前序数据、ID、参数),确保能准确还原。
- 避免在命令中直接读取外部可变状态(如全局变量),改用构造时捕获快照
- undo 和 redo 方法也应返回 Promise,以便用 await 统一等待(例如:await cmd.undo())
- 命令实例化时机很重要——应在用户触发动作时立即创建并入栈,而非等到 await 执行完才生成
撤销重做栈需独立于执行流管理
用两个栈(historyStack 和 redoStack)维护操作序列。每次成功执行命令后,将其 push 到 historyStack;undo 时 pop 并调用 undo(),再 push 到 redoStack;redo 则反向操作。await 出现在调用命令方法时,而非栈操作本身。
- 栈操作(push/pop)是同步的,保证顺序确定性;await 只作用于命令内部的异步副作用
- 执行 undo/redo 前检查栈空状态,避免越界;执行后触发 UI 更新或事件通知
- 支持批量命令组合(CompositeCommand),其 undo/redo 递归调用子命令,每个子调用仍可用 await
控制流挂起要落在命令边界,而非随意 await
不要在业务函数里到处写 await 试图“模拟同步”;而是让顶层控制器按序调用命令的 execute(),并在每步后 await —— 这样既保持线性可读性,又自然形成操作断点,便于插入撤销点。
- 例如:
await saveCmd.execute(); await publishCmd.execute(); await notifyCmd.execute(); - 若某步失败,可根据已执行命令数决定回滚范围(部分撤销),而非简单 throw 中断整个流程
- 不建议用 try/catch 包裹单个 await 表达式来“控制撤销”,而应在命令 execute() 内部处理错误,并让控制器决定是否继续或触发补偿逻辑
状态快照不是必须,但有时比命令更轻量
对纯数据操作(如表单编辑、画布变换),可不用命令对象,改用深拷贝当前状态存入 historyStack。await 用于异步获取快照(如 serialize() 返回 Promise),但快照本身是同步捕获的值。
- 快照方式适合高频、低复杂度操作(如输入框内容变更),避免命令类膨胀
- 注意性能:深拷贝大对象开销高,可考虑结构共享(immer)或增量 diff 存储
- 混合使用更灵活:核心业务走命令,UI 辅助状态走快照,统一由同一栈管理











