一、核心实现原理
命令模式 + 双栈机制
- 操作记录栈(undoStack):存储所有已执行的操作
- 撤销恢复栈(redoStack):存储被撤销的操作
- 每个操作需要记录反向操作(用于撤销时的逆向执行)
二、基础实现(伪代码)
class UndoRedoManager {
constructor() {
this.undoStack = []; // 操作记录栈
this.redoStack = []; // 撤销恢复栈
this.currentState = null; // 当前状态
}
// 执行新操作
execute(command) {
command.execute();
this.undoStack.push(command);
this.redoStack = []; // 清空重做栈
}
// 撤销
undo() {
if (this.undoStack.length === 0) return;
const lastCommand = this.undoStack.pop();
lastCommand.undo();
this.redoStack.push(lastCommand);
}
// 重做
redo() {
if (this.redoStack.length === 0) return;
const nextCommand = this.redoStack.pop();
nextCommand.execute();
this.undoStack.push(nextCommand);
}
}
// 命令基类
class Command {
constructor(target) {
this.target = target;
this.prevState = null;
}
execute() {
this.prevState = this.target.getState(); // 保存旧状态
}
undo() {
this.target.setState(this.prevState); // 恢复旧状态
}
}
三、完整实现示例(文本编辑器案例)
// 文本编辑器状态管理
class TextEditor {
constructor() {
this.content = "";
this.history = new UndoRedoManager();
}
// 添加文本
addText(text) {
const command = new AddTextCommand(this, text);
this.history.execute(command);
}
// 删除文本
deleteText(position, length) {
const command = new DeleteTextCommand(this, position, length);
this.history.execute(command);
}
getState() {
return this.content;
}
setState(state) {
this.content = state;
}
}
// 具体命令类:添加文本
class AddTextCommand extends Command {
constructor(editor, text) {
super(editor);
this.text = text;
}
execute() {
super.execute();
this.target.content += this.text;
}
undo() {
this.target.content = this.target.content.slice(0, -this.text.length);
}
}
// 具体命令类:删除文本
class DeleteTextCommand extends Command {
constructor(editor, position, length) {
super(editor);
this.position = position;
this.length = length;
this.deletedText = "";
}
execute() {
super.execute();
this.deletedText = this.target.content.substr(this.position, this.length);
this.target.content =
this.target.content.slice(0, this.position) +
this.target.content.slice(this.position + this.length);
}
undo() {
this.target.content =
this.target.content.slice(0, this.position) +
this.deletedText +
this.target.content.slice(this.position);
}
}
// 使用示例
const editor = new TextEditor();
editor.addText("Hello");
editor.addText(" World!");
console.log(editor.content); // "Hello World!"
editor.history.undo();
console.log(editor.content); // "Hello"
editor.history.redo();
console.log(editor.content); // "Hello World!"
editor.deleteText(5, 6);
console.log(editor.content); // "Hello!"
editor.history.undo();
console.log(editor.content); // "Hello World!"
四、优化策略
-
状态快照优化:
// 使用差异存储代替全量存储 class OptimizedCommand extends Command { constructor(target) { super(target); this.snapshot = null; } execute() { this.snapshot = this.target.getStateDiff(); // 获取差异数据 } undo() { this.target.applyDiff(this.snapshot); } }
-
操作合并:
// 合并连续输入操作 class CompositeCommand { constructor() { this.commands = []; } add(command) { if (command instanceof TextInputCommand) { this.commands.push(command); } } execute() { this.commands.forEach(cmd => cmd.execute()); } undo() { this.commands.reverse().forEach(cmd => cmd.undo()); } }
-
限制历史记录长度:
class LimitedHistoryManager extends UndoRedoManager { constructor(maxSteps = 100) { super(); this.maxSteps = maxSteps; } execute(command) { if (this.undoStack.length >= this.maxSteps) { this.undoStack.shift(); // 移除最旧记录 } super.execute(command); } }
五、实现流程图
通过这种模式可以实现:
- 时间复杂度:O(1) 的撤销/重做操作
- 空间复杂度:O(n)(取决于历史记录保留数量)
- 支持无限级撤销/重做(受内存限制)
- 可扩展性:轻松添加新操作类型
你提出的使用数组结构配合指针的方案确实是非常经典的实现方式,尤其适合状态快照型的撤销重做场景。这种方案相比命令模式有以下特点:
六、基础数据结构
6.1、实现原理图解
操作历史: [S0]->[S1]->[S2]->[S3]->[S4] (当前指针位置)
↑ ↑
undo redo
6.2 基础数据结构
class HistoryManager {
constructor(initialState, maxSteps = 100) {
this.history = [initialState]; // 初始状态
this.currentIndex = 0; // 当前指针位置
this.maxSteps = maxSteps; // 最大历史记录数
}
// 当前状态获取
get currentState() {
return this.history[this.currentIndex];
}
// 添加新状态(核心方法)
push(newState) {
// 截断旧的重做记录
this.history = this.history.slice(0, this.currentIndex + 1);
// 添加新记录
this.history.push(newState);
this.currentIndex++;
// 清理过期记录
if (this.history.length > this.maxSteps) {
this.history.shift();
this.currentIndex--;
}
}
// 撤销
undo() {
this.currentIndex = Math.max(0, this.currentIndex - 1);
return this.currentState;
}
// 重做
redo() {
this.currentIndex = Math.min(this.history.length - 1, this.currentIndex + 1);
return this.currentState;
}
// 是否可撤销
canUndo() {
return this.currentIndex > 0;
}
// 是否可重做
canRedo() {
return this.currentIndex < this.history.length - 1;
}
}
6.3 优化点解析
优化方向 | 实现策略 | 优势说明 |
---|---|---|
内存控制 | maxSteps 限制最大步数 | 防止内存无限增长 |
性能优化 | 截断旧的重做记录(slice ) | 避免无效数据占用内存 |
状态安全 | 返回当前状态的深拷贝 | 防止外部修改污染历史记录 |
边界检查 | canUndo /canRedo 方法 | 避免指针越界 |
七、方案对比分析
对比维度 | 数组指针方案 | 命令模式方案 |
---|---|---|
实现复杂度 | ⭐️⭐️(简单) | ⭐️⭐️⭐️(较复杂) |
内存占用 | ⭐️(存储完整状态) | ⭐️⭐️⭐️(存储差异) |
适用场景 | 状态结构简单、变化量小的场景 | 复杂操作、需要精细控制的场景 |
扩展性 | 修改状态结构需调整整个方案 | 新增命令类即可扩展新操作 |
操作粒度 | 只能做到状态级回滚 | 支持原子操作级别的撤销/重做 |
八、如何选择方案?
-
选择数组指针方案当:
- 状态结构简单(JSON序列化体积小)
- 不需要精细的操作记录(如只需要整体状态回滚)
- 开发周期紧张需要快速实现
-
选择命令模式方案当:
- 需要记录每个独立操作(如文本编辑的逐个字符输入)
- 状态结构复杂(如DOM树操作)
- 需要支持操作合并等高级功能
两种方案各有优劣,数组指针非常适合状态快照类的应用场景。在实际项目中,可以根据具体需求混合使用两种方案,例如用数组指针管理宏观状态,用命令模式处理微观操作。