多级撤销必须用双栈而非单列表或指针下标,因为双栈(past/future)天然规避越界、快照爆炸和日志混淆:past存可撤操作,future存可重做操作,所有操作均为push/pop,无索引计算,避免静默崩溃。

多级撤销不是简单地“把上一步存起来”,而是要让每次操作可逆、分支可裁剪、状态不污染。核心在于分离「当前数据」和「操作日志」,用两个独立栈管理历史流向,避免指针越界、快照爆炸或日志混淆。
为什么必须用双栈,而不是单列表或指针下标
单列表只存状态快照,内存随操作线性增长;指针方案依赖下标运算,在用户撤销后又执行新操作时极易越界或漏清未来历史。双栈(past / future)天然规避这些问题:past 存已执行且可撤的操作,future 存被撤掉但尚可重做的操作。所有动作都是 push/pop,无索引计算,不会出现 Cannot read property 'undoFn' of undefined 这类静默崩溃。
past 和 future 栈里该存什么
不存完整状态快照,而存「可逆操作对」:每个元素是 { doFn: () => void, undoFn: () => void }。比如文本插入操作,doFn 是 text += input,undoFn 是 text = text.slice(0, -input.length)。pop 操作记录被弹出的值,push 操作记录入栈值——这样 undo 才能真正还原,而不是盲目删最后一个元素。
- 新操作发生时:执行
doFn,压入past,并清空future - 执行 undo:从
past弹出,运行其undoFn,再压入future开头 - 执行 redo:从
future头部取出,运行doFn,再压回past
如何安全处理边界与异常路径
空栈 pop、重复 undo、未来栈为空时调用 redo —— 这些都不是报错场景,而是应静默忽略。关键检查点只有三个:
-
past.length > 0才允许 undo -
future.length > 0才允许 redo - 每次新操作前,强制执行
future = [],截断所有已失效的重做路径
不要在 undoFn 内部再次调用系统级 push/pop 方法,否则会二次写日志,导致循环嵌套。
变量状态链式回溯的关键细节
所谓“链式”,不是靠引用串联,而是靠操作顺序保证因果。例如连续三次修改一个对象属性:a.x=1 → a.y=2 → a.x=3,如果第二步被撤销,第三步不能保留——因为它的前提(a.y 已设)已不存在。因此,撤销必须是原子操作单元,每个单元封装自身依赖。推荐将粒度控制在“用户一次交互”级别(如一次输入、一次拖拽),而非单个属性赋值。

















