使用 JavaScript 实现撤销和重做功能的实现思路与伪代码


一、核心实现原理

命令模式 + 双栈机制

  1. 操作记录栈(undoStack):存储所有已执行的操作
  2. 撤销恢复栈(redoStack):存储被撤销的操作
  3. 每个操作需要记录反向操作(用于撤销时的逆向执行)

二、基础实现(伪代码)

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!"

四、优化策略

  1. 状态快照优化

    // 使用差异存储代替全量存储
    class OptimizedCommand extends Command {
      constructor(target) {
        super(target);
        this.snapshot = null;
      }
    
      execute() {
        this.snapshot = this.target.getStateDiff(); // 获取差异数据
      }
    
      undo() {
        this.target.applyDiff(this.snapshot);
      }
    }
    
  2. 操作合并

    // 合并连续输入操作
    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());
      }
    }
    
  3. 限制历史记录长度

    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);
      }
    }
    

五、实现流程图

User Application UndoStack RedoStack Command 执行操作 压入新命令 清空栈 点击撤销 弹出最后命令 压入命令 执行undo() 点击重做 弹出最后命令 压入命令 执行execute() User Application UndoStack RedoStack Command

通过这种模式可以实现:

  1. 时间复杂度:O(1) 的撤销/重做操作
  2. 空间复杂度:O(n)(取决于历史记录保留数量)
  3. 支持无限级撤销/重做(受内存限制)
  4. 可扩展性:轻松添加新操作类型
    你提出的使用数组结构配合指针的方案确实是非常经典的实现方式,尤其适合状态快照型的撤销重做场景。这种方案相比命令模式有以下特点:

六、基础数据结构

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方法避免指针越界

七、方案对比分析

对比维度数组指针方案命令模式方案
实现复杂度⭐️⭐️(简单)⭐️⭐️⭐️(较复杂)
内存占用⭐️(存储完整状态)⭐️⭐️⭐️(存储差异)
适用场景状态结构简单、变化量小的场景复杂操作、需要精细控制的场景
扩展性修改状态结构需调整整个方案新增命令类即可扩展新操作
操作粒度只能做到状态级回滚支持原子操作级别的撤销/重做

八、如何选择方案?

  1. 选择数组指针方案当

    • 状态结构简单(JSON序列化体积小)
    • 不需要精细的操作记录(如只需要整体状态回滚)
    • 开发周期紧张需要快速实现
  2. 选择命令模式方案当

    • 需要记录每个独立操作(如文本编辑的逐个字符输入)
    • 状态结构复杂(如DOM树操作)
    • 需要支持操作合并等高级功能

两种方案各有优劣,数组指针非常适合状态快照类的应用场景。在实际项目中,可以根据具体需求混合使用两种方案,例如用数组指针管理宏观状态,用命令模式处理微观操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值