命令模式

将请求封装成对象,实现撤销、重做和命令队列

问题

什么是命令模式?如何在前端中应用命令模式实现撤销/重做功能?

解答

命令模式将”请求”封装成对象,就像江湖中的通缉令——发布者不需要知道谁来执行,执行者也不需要知道谁发布的,通缉令本身就包含了所有必要信息。

基本结构

// 命令接口
class Command {
  execute() {}
  undo() {}
}

// 具体命令:加法
class AddCommand extends Command {
  constructor(receiver, value) {
    super()
    this.receiver = receiver
    this.value = value
  }

  execute() {
    this.receiver.add(this.value)
  }

  undo() {
    this.receiver.subtract(this.value)
  }
}

// 接收者:计算器
class Calculator {
  constructor() {
    this.result = 0
  }

  add(value) {
    this.result += value
    console.log(`结果: ${this.result}`)
  }

  subtract(value) {
    this.result -= value
    console.log(`结果: ${this.result}`)
  }
}

// 调用者:支持撤销/重做
class CommandManager {
  constructor() {
    this.history = []    // 已执行的命令
    this.undoStack = []  // 已撤销的命令
  }

  execute(command) {
    command.execute()
    this.history.push(command)
    this.undoStack = []  // 执行新命令后清空重做栈
  }

  undo() {
    const command = this.history.pop()
    if (command) {
      command.undo()
      this.undoStack.push(command)
    }
  }

  redo() {
    const command = this.undoStack.pop()
    if (command) {
      command.execute()
      this.history.push(command)
    }
  }
}

使用示例

const calculator = new Calculator()
const manager = new CommandManager()

// 执行命令
manager.execute(new AddCommand(calculator, 10))  // 结果: 10
manager.execute(new AddCommand(calculator, 20))  // 结果: 30

// 撤销
manager.undo()  // 结果: 10
manager.undo()  // 结果: 0

// 重做
manager.redo()  // 结果: 10

实际应用:文本编辑器

// 文本编辑器接收者
class TextEditor {
  constructor() {
    this.content = ''
  }

  insert(text, position) {
    this.content = 
      this.content.slice(0, position) + 
      text + 
      this.content.slice(position)
  }

  delete(position, length) {
    this.content = 
      this.content.slice(0, position) + 
      this.content.slice(position + length)
  }

  getContent() {
    return this.content
  }
}

// 插入命令
class InsertCommand extends Command {
  constructor(editor, text, position) {
    super()
    this.editor = editor
    this.text = text
    this.position = position
  }

  execute() {
    this.editor.insert(this.text, this.position)
  }

  undo() {
    this.editor.delete(this.position, this.text.length)
  }
}

// 删除命令
class DeleteCommand extends Command {
  constructor(editor, position, length) {
    super()
    this.editor = editor
    this.position = position
    this.length = length
    this.deletedText = ''  // 保存被删除的文本用于撤销
  }

  execute() {
    // 保存将被删除的文本
    this.deletedText = this.editor.content.slice(
      this.position, 
      this.position + this.length
    )
    this.editor.delete(this.position, this.length)
  }

  undo() {
    this.editor.insert(this.deletedText, this.position)
  }
}

// 使用
const editor = new TextEditor()
const cmdManager = new CommandManager()

cmdManager.execute(new InsertCommand(editor, 'Hello', 0))
console.log(editor.getContent())  // "Hello"

cmdManager.execute(new InsertCommand(editor, ' World', 5))
console.log(editor.getContent())  // "Hello World"

cmdManager.undo()
console.log(editor.getContent())  // "Hello"

cmdManager.redo()
console.log(editor.getContent())  // "Hello World"

宏命令:批量执行

// 宏命令:组合多个命令
class MacroCommand extends Command {
  constructor() {
    super()
    this.commands = []
  }

  add(command) {
    this.commands.push(command)
  }

  execute() {
    this.commands.forEach(cmd => cmd.execute())
  }

  undo() {
    // 逆序撤销
    this.commands.slice().reverse().forEach(cmd => cmd.undo())
  }
}

// 使用宏命令
const macro = new MacroCommand()
macro.add(new AddCommand(calculator, 5))
macro.add(new AddCommand(calculator, 10))
macro.add(new AddCommand(calculator, 15))

manager.execute(macro)  // 一次执行三个命令
manager.undo()          // 一次撤销三个命令

关键点

  • 解耦:发送者和接收者互不依赖,通过命令对象通信
  • 可撤销:命令对象保存执行状态,实现 undo/redo
  • 可组合:宏命令可以将多个命令组合成一个
  • 可记录:命令队列可用于日志、事务、延迟执行
  • 前端场景:富文本编辑器、画板工具、表单操作、游戏回放