纯函数+闭包是构建轻量可预测撤销重做引擎的黄金组合:纯函数确保状态变更确定性,闭包捕获操作上下文快照,双栈管理命令生命周期,并通过裁剪与合并优化内存。

纯函数 + 闭包是构建轻量、可预测、无副作用撤销重做引擎的黄金组合。关键不在于保存大量状态,而是在每次操作发生时,用闭包捕获“当时的关键上下文”,再配合纯函数确保每次 undo/redo 的结果完全可复现。
纯函数保障状态变更的确定性
撤销重做系统最怕“执行两次 undo 结果不同”。纯函数天然杜绝这一问题:给定相同输入(当前状态 + 命令),必返回相同新状态,且不修改原状态。这意味着:
- reducer 或状态更新函数必须是纯的:只依赖参数,不读写外部变量或全局状态
- 所有命令(如 InsertText、DeleteNode)的 execute 和 undo 方法,都应接收 state 并返回 new state,而非直接 mutate
- 例如:✅ 正确:const newState = insertText(state, { pos: 5, text: "hello" });
❌ 错误:state.content.splice(5, 0, "hello"); return state;
闭包封装“可逆操作”的上下文快照
纯函数解决“怎么变”,闭包解决“变什么、从哪来”。每次用户操作(如删除一个节点)发生时,不是只记下动作类型,而是立即用闭包把“那一刻的必要数据”封存起来:
- 删除节点时,闭包记住:被删节点的完整数据、它在列表中的索引、父容器ID
- 移动节点时,闭包记住:原位置、目标位置、节点ID
- 这些值在创建命令对象时被捕获,后续调用 undo 就能精准还原,不受后续状态污染
双栈结构管理命令生命周期
用两个数组栈(undoStack、redoStack)线性组织命令对象,每个对象都是一个闭包封装的纯函数对:
- 命令结构示例:
{ execute: () => newState, undo: () => prevState },两者都基于闭包内捕获的数据 - 执行操作后,将该命令 push 到 undoStack,同时清空 redoStack(新操作打断重做链)
- 触发 undo:pop undoStack 顶部命令 → 执行其 undo() → 将该命令 push 到 redoStack
- 触发 redo:pop redoStack 顶部命令 → 执行其 execute() → 将该命令 push 到 undoStack
自动裁剪与内存友好设计
闭包虽方便,但长期持有引用可能造成内存泄漏。需主动控制历史深度:
- 设定最大步数(如 50),每次 push 前检查长度,超限时 shift() 舍弃最早命令
- 对连续同类操作(如快速连输 5 个字符)做合并:闭包内聚合文本和位置,生成单个 InsertRange 命令,而非 5 个独立 InsertChar
- 避免在闭包中捕获整个响应式对象或大型 DOM 引用;只取必要字段(id、value、index 等原始值)

















