

最近在做一个在线PPT编辑器,其中状态管理用到了Zustand,撤销重做功能用的是zundo,编辑器类型的项目一般都会有历史记录功能,但是记录用户的操作有哪些方案呐?
主流技术方案对比
目前,业界主要有三种实现思路:命令模式 、状态快照(备忘录模式)和差异-补丁(Diff-Patch) 。
方案一:命令模式 (Command Pattern)
这是实现撤销/重做功能最经典、最正统的设计模式。
-
核心思想:
将用户的每一个"操作"封装成一个包含execute(执行)和undo(撤销)方法的命令对象。
-
数据结构:
使用两个栈:undoStack(撤销栈)和redoStack(重做栈)。
-
工作流程:
- 执行操作: 用户执行一个新操作(如"添加矩形")。
- 创建一个
AddRectangleCommand对象。 - 调用
command.execute()来执行该操作(更新画布状态)。 - 将该
command压入undoStack。 - 撤销 (Undo):
- 从
undoStack弹出一个command。 - 调用
command.undo()。 - 将该
command压入redoStack。 - 重做 (Redo):
- 从
redoStack弹出一个command。 - 调用
command.execute()。 - 将该
command压入undoStack。
-
优点:
- 内存占用低: 只存储"操作"本身,而不是完整的画布状态。
- 逻辑清晰:
undo和execute逻辑高度内聚,易于理解和测试。 - 高性能:
undo/redo操作通常很快,因为它们只执行逆向/正向操作。
-
缺点:
- 实现复杂度高: 必须为每一个 可撤销的操作(移动、缩放、变色、删除、组合...)编写一个具体的
Command类及其undo逻辑。 undo的实现难度:undo操作(如"撤销删除")可能需要存储被删除对象的状态,这会增加命令对象的复杂度。
- 实现复杂度高: 必须为每一个 可撤销的操作(移动、缩放、变色、删除、组合...)编写一个具体的
方案二:状态快照 (State Snapshots / Memento Pattern)
这是一种实现上更简单粗暴,但在特定场景下非常有效的模式。
-
核心思想:
在每次"有效操作"结束时,将整个画布的完整状态(通常是JSON数据)序列化并存储起来。
-
数据结构:
一个数组(historyStack)和一个指针(currentIndex)。
-
工作流程:
- 执行操作: 用户完成操作(如拖拽结束)。
- 获取当前画布的完整状态
newState。 - 将
newState添加到historyStack中currentIndex的位置。 currentIndex加一。- 撤销 (Undo):
currentIndex减一。- 从
historyStack[currentIndex]获取previousState。 - 用
previousState完全覆盖当前画布状态并重新渲染。 - 重做 (Redo):
currentIndex加一。- 从
historyStack[currentIndex]获取nextState。 - 用
nextState覆盖当前状态并重新渲染。
-
优点:
- 实现简单: 核心逻辑与业务操作解耦。历史记录系统不需要"理解"什么是"移动",什么是"变色",它只负责保存和恢复状态。
- 绝对可靠: 只要状态的序列化和反序列化是正确的,
undo/redo就绝对不会出错。
-
缺点:
- 内存占用极大: 如果画布状态有10MB,100步历史就是1GB内存。这在Web端几乎是不可接受的。
- 性能瓶颈: 序列化/反序列化/深拷贝大型状态对象(Deep Clone)可能非常耗时,导致UI卡顿。
方案三:增量差异 (Diff-Patch)
这是快照模式的进一步演进,也是目前在React等状态驱动框架中非常流行的一种方案。
-
核心思想:
结合了命令模式(只存变化)和快照模式(不关心操作逻辑)的优点。它不存储完整的状态,也不存储操作命令,而是存储两个状态之间的差异(Diff/Patch)。
-
数据结构:
undoStack(存储逆向Patch)和redoStack(存储正向Patch)。
-
工作流程:
- 执行操作:
- (操作前)记录当前状态
State A。 - (操作后)生成新状态
State B。 - 计算差异:
Patch (A -> B)(正向补丁)和Inverse Patch (B -> A)(逆向补丁)。 - 将
Inverse Patch压入undoStack。 - 将
Patch压入redoStack(或在undo时再计算)。 - 撤销 (Undo):
- 从
undoStack弹出Inverse Patch。 - 将该补丁Apply 到当前状态,使其回退到
State A。 - 重做 (Redo):
- 从
redoStack弹出Patch。 - 将该补丁应用到当前状态,使其前进到
State B。
-
优点:
- 内存与性能均衡: 内存占用远小于全量快照(只存Diff),实现复杂度远低于命令模式(自动生成Diff和Patch)
-
缺点:
- Diff/Patch的开销: 如果一次操作改变了状态树的很多部分,计算Diff和生成Patch本身也可能有性能开销(但通常快于深拷贝)。
- 依赖库: 通常需要依赖一个健壮的Diff/Patch库
方案对比总结
| 特性 | 命令模式 (Command Pattern) | 状态快照 (State Snapshot) | 增量差异 (Diff-Patch) |
|---|---|---|---|
| 核心 | 存储"操作" | 存储"完整状态" | 存储"状态差异" |
| 内存占用 | 极低 | 极高 | 较低 |
| 性能开销 | undo/redo极快 |
存取时开销大 (深拷贝/序列化) | 存取时有Diff/Patch计算开销 |
| 实现复杂度 | 极高 (需实现所有undo逻辑) |
极低 | 中等 (需依赖Diff库) |
| 适用场景 | 性能和内存要求苛刻的复杂应用 | 状态简单的小型应用 | 现代前端框架 (React/Vue) ,状态驱动型应用 |