1. 模块摘要 (Executive Summary) Undo/Redo 机制是画布应用中实现操作撤销和重做的核心功能模块。它基于命令模式(Command Pattern)实现来,管理操作历史、执行撤销/重做操作和防止操作冲突,通过维护撤销栈和重做栈来管理用户的操作历史。
项目结构树 :
1 2 3 4 5 6 src/ ├── lib/ │ ├── UndoRedoManager.ts │ └── UpdateElementCommand.ts └── stores/ └── canvasStore.ts
Command Pattern:设计模式,用于封装操作命令
Zustand:状态管理库,作为命令操作的目标
TypeScript:提供类型安全和代码可维护性
2. Props 和相关类型定义 2.1 UndoRedoManager 核心方法 撤销重做管理器提供了一系列核心方法用于管理操作命令。
方法名
参数
返回值
描述
executeCommand
command: Command
void
执行并记录命令
undo
无
void
执行撤销操作
redo
无
void
执行重做操作
lock
无
void
锁定管理器,防止记录新命令
unlock
无
void
解锁管理器
isLocked
无
boolean
检查管理器是否被锁定
canUndo
无
boolean
检查是否可以撤销
canRedo
无
boolean
检查是否可以重做
2.2 核心类型定义 Command 接口 : 定义了命令对象必须实现的方法。
1 2 3 4 5 export interface Command { execute (): void ; undo (): void ; redo (): void ; }
UpdateOperation 接口 : 定义了元素更新操作的数据结构。
1 2 3 4 5 interface UpdateOperation { id : string ; initialAttrs : Partial <CanvasElement >; finalAttrs : Partial <CanvasElement >; }
3. 核心状态管理 (State Architecture)
⚠️ 为防止在执行撤销/重做操作时记录新的命令,系统实现了锁定机制。在执行命令时会先锁定管理器,执行完成后再解锁,确保操作的原子性。
3.1 内部状态 (Local State) UndoRedoManager 维护以下内部状态用于管理操作历史:
状态名
类型
描述
undoStack
Command[]
撤销命令栈,存储可以撤销的命令
redoStack
Command[]
重做命令栈,存储可以重做的命令
locked
boolean
锁定状态,防止在执行命令时记录新命令
3.2 外部状态 (Global/Server State) Undo/Redo 机制通过 Zustand 状态管理库操作外部状态:
状态名
类型
描述
elements
Record
所有画布元素数据,命令操作的目标
3.3 状态同步机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 graph TD A[用户操作] --> B{StageManager} B --> C[创建命令对象] C --> D[UndoRedoManager.executeCommand] D --> E{管理器锁定?} E -->|是| F[忽略命令] E -->|否| G[执行命令] G --> H[命令入撤销栈] H --> I[清空重做栈] I --> J[Zustand 状态更新] subgraph 撤销操作 K[UndoRedoManager.undo] K --> L{撤销栈空?} L -->|是| M[无法撤销] L -->|否| N[弹出命令] N --> O[执行命令.undo] O --> P[命令入重做栈] P --> Q[Zustand 状态更新] end subgraph 重做操作 R[UndoRedoManager.redo] R --> S{重做栈空?} S -->|是| T[无法重做] S -->|否| U[弹出命令] U --> V[执行命令.redo] V --> W[命令入撤销栈] W --> X[Zustand 状态更新] end style A fill:#e1f5fe style J fill:#e8f5e8 style Q fill:#e8f5e8 style X fill:#e8f5e8 style F fill:#ffebee
4. 命令管理机制 Undo/Redo 机制采用命令模式(Command Pattern)来管理操作命令,通过定义统一的接口和不同的实现类来处理各种操作。
4.1 命令类型 系统中主要有两种命令类型:
快照命令(SnapshotCommand) :用于记录整个画布状态的变化,通常用于添加元素、删除元素等较大范围的操作,保存完整的状态快照
更新元素命令(UpdateElementCommand) :用于记录特定元素的属性变化,主要用于拖拽移动和调整大小操作,只保存相关元素的特定属性变化
4.2 命令接口定义 所有命令都实现统一的 Command 接口:
1 2 3 4 5 export interface Command { execute (): void ; undo (): void ; redo (): void ; }
4.3 快照命令(SnapshotCommand) 快照命令用于记录整个画布状态的变化,适用于影响范围较大的操作。
核心实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export class SnapshotCommand implements Command { private prevState : any ; private nextState : any ; private commandId : number ; private type : string ; constructor (prevState : any , nextState : any , type : any ) { this .prevState = structuredClone (prevState); this .nextState = structuredClone (nextState); this .type = type ; this .commandId = Date .now () % 1000000 ; } execute (): void { } undo (): void { useStore.setState (this .prevState ); } redo (): void { useStore.setState (this .nextState ); } }
4.4 更新元素命令(UpdateElementCommand) 更新元素命令用于记录特定元素的属性变化,适用于影响范围较小的精细操作。
核心实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 interface UpdateOperation { id : string ; initialAttrs : Partial <CanvasElement >; finalAttrs : Partial <CanvasElement >; }export class UpdateElementCommand implements Command { private commandId : string ; constructor ( private operations : UpdateOperation [], private operationType : string = "更新元素" ) { this .commandId = `UpdateElementCommand-${Math .random() .toString(36 ) .slice(2 , 11 )} ` ; } execute (): void { const updates : Record <string , Partial <CanvasElement >> = {}; this .operations .forEach ((op ) => { updates[op.id ] = op.finalAttrs ; }); useStore.setState ((state ) => { const newElements = { ...state.elements }; Object .entries (updates).forEach (([id, attrs] ) => { if (newElements[id]) newElements[id] = { ...newElements[id], ...attrs }; }); return { elements : newElements }; }); } undo (): void { const updates : Record <string , Partial <CanvasElement >> = {}; this .operations .forEach ((op ) => { updates[op.id ] = op.initialAttrs ; }); useStore.setState ((state ) => { const newElements = { ...state.elements }; Object .entries (updates).forEach (([id, attrs] ) => { if (newElements[id]) newElements[id] = { ...newElements[id], ...attrs }; }); return { elements : newElements }; }); } redo (): void { const updates : Record <string , Partial <CanvasElement >> = {}; this .operations .forEach ((op ) => { updates[op.id ] = op.finalAttrs ; }); useStore.setState ((state ) => { const newElements = { ...state.elements }; Object .entries (updates).forEach (([id, attrs] ) => { if (newElements[id]) newElements[id] = { ...newElements[id], ...attrs }; }); return { elements : newElements }; }); } }
4.5 命令生命周期 命令的生命周期包括创建、执行、撤销和重做四个阶段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 graph TD A[命令创建] --> B[命令执行] B --> C{用户操作} C -->|撤销| D[执行undo方法] C -->|重做| E[执行redo方法] D --> F[命令状态切换] E --> F F --> G[状态更新完成] style A fill:#e1f5fe style B fill:#f3e5f5 style D fill:#fff3e0 style E fill:#fff3e0 style G fill:#e8f5e8
5. 命令栈管理机制 撤销/重做机制使用两个栈来管理命令历史:
撤销栈(Undo Stack) :
存储用户可以撤销的操作命令,栈顶是最近执行的命令,执行新命令时,命令被推入此栈,执行撤销操作时,命令从此栈弹出并推入重做栈
重做栈(Redo Stack) :
存储用户可以重做的操作命令,在执行撤销操作时,被撤销的命令被推入此栈,执行重做操作时,命令从此栈弹出并推入撤销栈,执行新命令时,此栈被清空
5.1 命令栈操作流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 graph TD A[执行新命令] --> B[命令入撤销栈] B --> C[清空重做栈] D[执行撤销] --> E{撤销栈空?} E -->|是| F[无操作] E -->|否| G[弹出命令] G --> H[执行命令.undo] H --> I[命令入重做栈] J[执行重做] --> K{重做栈空?} K -->|是| L[无操作] K -->|否| M[弹出命令] M --> N[执行命令.redo] N --> O[命令入撤销栈] style A fill:#e1f5fe style B fill:#f3e5f5 style C fill:#f3e5f5 style D fill:#e1f5fe style H fill:#fff3e0 style I fill:#fff3e0 style J fill:#e1f5fe style N fill:#e8f5e8 style O fill:#e8f5e8
5.2 不同类型的命令 撤销栈中并不全是快照命令。系统中至少有两种不同类型的命令:
快照命令(SnapshotCommand) :
用于记录整个画布状态的变化
通常用于添加元素、删除元素等较大范围的操作
保存完整的状态快照
更新元素命令(UpdateElementCommand) :
用于记录特定元素的属性变化
主要用于拖拽移动和调整大小操作
只保存相关元素的特定属性变化
5.3 操作序列和撤销栈状态变化示例 初始状态 撤销栈:空 重做栈:空
1. 创建元素 A 当创建元素 A 时,系统会生成一个快照命令,记录整个画布状态的变化。 撤销栈:[SnapshotCommand_A] (大小: 1) 重做栈:空
2. 移动 A 到一个位置 当移动元素 A 时,系统会生成一个更新元素命令(UpdateElementCommand),只记录 A 元素位置的变化。 撤销栈:[SnapshotCommand_A, UpdateElementCommand_MoveA] (大小: 2) 重做栈:空
3. 创建元素 B 当创建元素 B 时,系统会生成另一个快照命令,记录添加 B 元素后的状态。 撤销栈:[SnapshotCommand_A, UpdateElementCommand_MoveA, SnapshotCommand_B] (大小: 3) 重做栈:空
4. 缩放 B 到一个位置 当缩放元素 B 时,系统会生成一个更新元素命令,记录 B 元素尺寸和位置的变化。 撤销栈:[SnapshotCommand_A, UpdateElementCommand_MoveA, SnapshotCommand_B, UpdateElementCommand_ResizeB] (大小: 4) 重做栈:空
5. 移动 B 到一个位置 当再次移动元素 B 时,系统会生成另一个更新元素命令,记录 B 元素位置的新变化。 撤销栈:[SnapshotCommand_A, UpdateElementCommand_MoveA, SnapshotCommand_B, UpdateElementCommand_ResizeB, UpdateElementCommand_MoveB] (大小: 5) 重做栈:空
5.4 执行撤销操作时的状态变化 第一次撤销(移动 B 操作)
从撤销栈弹出最后一个命令:UpdateElementCommand_MoveB
执行该命令的 undo()方法,将 B 元素恢复到缩放后的位置
将该命令推入重做栈
撤销栈:[SnapshotCommand_A, UpdateElementCommand_MoveA, SnapshotCommand_B, UpdateElementCommand_ResizeB] (大小: 4) 重做栈:[UpdateElementCommand_MoveB] (大小: 1)
第二次撤销(缩放 B 操作)
从撤销栈弹出最后一个命令:UpdateElementCommand_ResizeB
执行该命令的 undo()方法,将 B 元素恢复到刚创建时的尺寸和位置
将该命令推入重做栈
撤销栈:[SnapshotCommand_A, UpdateElementCommand_MoveA, SnapshotCommand_B] (大小: 3) 重做栈:[UpdateElementCommand_MoveB, UpdateElementCommand_ResizeB] (大小: 2)
第三次撤销(创建 B 操作)
从撤销栈弹出最后一个命令:SnapshotCommand_B
执行该命令的 undo()方法,将整个画布状态恢复到创建 B 之前的状态(即只包含 A 元素的状态)
将该命令推入重做栈
撤销栈:[SnapshotCommand_A, UpdateElementCommand_MoveA] (大小: 2) 重做栈:[UpdateElementCommand_MoveB, UpdateElementCommand_ResizeB, SnapshotCommand_B] (大小: 3)
第四次撤销(移动 A 操作)
从撤销栈弹出最后一个命令:UpdateElementCommand_MoveA
执行该命令的 undo()方法,将 A 元素恢复到初始位置
将该命令推入重做栈
撤销栈:[SnapshotCommand_A] (大小: 1) 重做栈:[UpdateElementCommand_MoveB, UpdateElementCommand_ResizeB, SnapshotCommand_B, UpdateElementCommand_MoveA] (大小: 4)
第五次撤销(创建 A 操作)
从撤销栈弹出最后一个命令:SnapshotCommand_A
执行该命令的 undo()方法,将整个画布状态恢复到初始状态(空画布)
将该命令推入重做栈
撤销栈:空 重做栈:[UpdateElementCommand_MoveB, UpdateElementCommand_ResizeB, SnapshotCommand_B, UpdateElementCommand_MoveA, SnapshotCommand_A] (大小: 5)
6. 逻辑流程 (Logic Flow) 6.1 交互时序图 (Mermaid) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 sequenceDiagram participant U as 用户 participant SM as StageManager participant URM as UndoRedoManager participant C as Command participant ZS as Zustand Store U->>SM: 执行操作(如拖拽元素) SM->>SM: 记录操作初始状态 SM->>ZS: 更新元素状态 SM->>C: 创建 UpdateElementCommand SM->>URM: executeCommand(command) URM->>C: command.execute() C->>URM: 命令入撤销栈 URM->>URM: 清空重做栈 U->>URM: 执行撤销 (Ctrl+Z) URM->>URM: 锁定管理器 URM->>C: command.undo() C->>ZS: 恢复初始状态 C->>URM: 命令入重做栈 URM->>URM: 解锁管理器 U->>URM: 执行重做 (Ctrl+Y) URM->>URM: 锁定管理器 URM->>C: command.redo() C->>ZS: 恢复最终状态 C->>URM: 命令入撤销栈 URM->>URM: 解锁管理器
6.2 核心函数解析 executeCommand 函数 :当用户完成一个操作(如创建、更新、删除元素)时触发,执行命令并将命令添加到撤销栈,同时清空重做栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 executeCommand (command : Command ) { if (this .locked ) { return } command.execute () this .undoStack .push (command) this .redoStack = [] }
undo 函数 :当用户执行撤销操作(如按 Ctrl+Z)时触发,从撤销栈弹出命令,执行命令的 undo 方法,并将命令放入重做栈
1 2 3 4 5 6 7 8 9 10 11 12 undo ( ) { if (this .undoStack .length === 0 ) { return } this .lock () const command = this .undoStack .pop ()! command.undo () this .redoStack .push (command) this .unlock () }
redo 函数 :当用户执行重做操作(如按 Ctrl+Y)时触发,从重做栈弹出命令,执行命令的 redo 方法,并将命令放入撤销栈
1 2 3 4 5 6 7 8 9 10 11 12 redo ( ) { if (this .redoStack .length === 0 ) { return } this .lock () const command = this .redoStack .pop ()! command.redo () this .undoStack .push (command) this .unlock () }
7. UI 与样式实现 (UI Implementation) Undo/Redo 机制通过快捷键和控制台界面与用户交互:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 graph TD A[用户交互] --> B{交互方式} B --> C[键盘快捷键] B --> D[控制台界面] C --> E[Ctrl+Z 撤销] C --> F[Ctrl+Y 重做] D --> G[命令栈控制台] G --> H[撤销按钮] G --> I[重做按钮] G --> J[清空按钮] style A fill:#e1f5fe style C fill:#f3e5f5 style D fill:#f3e5f5 style E fill:#e8f5e8 style F fill:#e8f5e8 style G fill:#fff3e0