命令模式(Command)的封装与撤销操作
命令模式的基本概念
命令模式是一种行为型设计模式,它将请求封装成对象,从而允许用户使用不同的请求、队列或日志来参数化其他对象。这种模式的核心思想是将"做什么"(具体操作)与"谁来做"(调用者)解耦。在JavaScript中,命令模式通常表现为一个包含执行方法的对象。
class Command {
execute() {
throw new Error('必须实现execute方法');
}
}
class ConcreteCommand extends Command {
constructor(receiver) {
super();
this.receiver = receiver;
}
execute() {
this.receiver.action();
}
}
class Receiver {
action() {
console.log('执行具体操作');
}
}
const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
command.execute(); // 输出: 执行具体操作
命令模式的封装特性
命令模式通过将操作封装为对象,实现了操作的参数化和延迟执行。这种封装带来了几个显著优势:
- 请求的封装:将请求的细节隐藏在命令对象内部
- 调用者与接收者解耦:调用者不需要知道接收者的具体实现
- 支持复合命令:可以将多个命令组合成一个复合命令
// 复合命令示例
class MacroCommand {
constructor() {
this.commands = [];
}
add(command) {
this.commands.push(command);
}
execute() {
this.commands.forEach(command => command.execute());
}
}
const macro = new MacroCommand();
macro.add(new ConcreteCommand(receiver));
macro.add(new ConcreteCommand(receiver));
macro.execute(); // 会执行两次receiver.action()
实现撤销操作
命令模式天然支持撤销(undo)操作,这是它的一个重要应用场景。要实现撤销功能,命令对象需要存储足够的状态信息以便回滚操作。
class UndoableCommand extends Command {
constructor(receiver, value) {
super();
this.receiver = receiver;
this.value = value;
this.previousValue = null;
}
execute() {
this.previousValue = this.receiver.value;
this.receiver.value = this.value;
console.log(`设置值为: ${this.value}`);
}
undo() {
this.receiver.value = this.previousValue;
console.log(`撤销到: ${this.previousValue}`);
}
}
class ReceiverWithState {
constructor() {
this.value = 0;
}
}
const receiverWithState = new ReceiverWithState();
const undoableCommand = new UndoableCommand(receiverWithState, 10);
undoableCommand.execute(); // 设置值为: 10
console.log(receiverWithState.value); // 10
undoableCommand.undo(); // 撤销到: 0
console.log(receiverWithState.value); // 0
更复杂的撤销栈实现
在实际应用中,我们通常需要维护一个完整的撤销栈,支持多步撤销和重做:
class CommandManager {
constructor() {
this.undoStack = [];
this.redoStack = [];
}
execute(command) {
command.execute();
this.undoStack.push(command);
this.redoStack = []; // 执行新命令时清空重做栈
}
undo() {
if (this.undoStack.length > 0) {
const command = this.undoStack.pop();
command.undo();
this.redoStack.push(command);
}
}
redo() {
if (this.redoStack.length > 0) {
const command = this.redoStack.pop();
command.execute();
this.undoStack.push(command);
}
}
}
// 使用示例
const manager = new CommandManager();
const receiver = new ReceiverWithState();
manager.execute(new UndoableCommand(receiver, 10));
manager.execute(new UndoableCommand(receiver, 20));
console.log(receiver.value); // 20
manager.undo();
console.log(receiver.value); // 10
manager.undo();
console.log(receiver.value); // 0
manager.redo();
console.log(receiver.value); // 10
命令模式在UI交互中的应用
命令模式在前端开发中特别适合处理用户交互,比如按钮点击、菜单选择等操作。下面是一个实际的DOM操作示例:
// DOM操作命令
class DOMCommand {
constructor(element, property, newValue) {
this.element = element;
this.property = property;
this.newValue = newValue;
this.oldValue = null;
}
execute() {
this.oldValue = this.element.style[this.property];
this.element.style[this.property] = this.newValue;
}
undo() {
this.element.style[this.property] = this.oldValue;
}
}
// 使用示例
const button = document.createElement('button');
button.textContent = '点击我';
document.body.appendChild(button);
const commandManager = new CommandManager();
const changeColorCmd = new DOMCommand(button, 'backgroundColor', 'red');
const changeTextCmd = new DOMCommand(button, 'color', 'white');
// 执行命令
commandManager.execute(changeColorCmd);
commandManager.execute(changeTextCmd);
// 添加撤销按钮
const undoBtn = document.createElement('button');
undoBtn.textContent = '撤销';
undoBtn.addEventListener('click', () => commandManager.undo());
document.body.appendChild(undoBtn);
// 添加重做按钮
const redoBtn = document.createElement('button');
redoBtn.textContent = '重做';
redoBtn.addEventListener('click', () => commandManager.redo());
document.body.appendChild(redoBtn);
命令队列与延迟执行
命令模式还支持将命令放入队列中,按需执行或延迟执行,这在动画、批量操作等场景中非常有用:
class CommandQueue {
constructor() {
this.queue = [];
this.isExecuting = false;
}
add(command) {
this.queue.push(command);
if (!this.isExecuting) {
this.executeNext();
}
}
executeNext() {
if (this.queue.length > 0) {
this.isExecuting = true;
const command = this.queue.shift();
command.execute();
// 模拟异步操作完成
setTimeout(() => {
this.isExecuting = false;
this.executeNext();
}, 1000);
}
}
}
// 使用示例
const queue = new CommandQueue();
const receiver = {
action: (msg) => console.log(`执行: ${msg}`)
};
class LogCommand {
constructor(receiver, message) {
this.receiver = receiver;
this.message = message;
}
execute() {
this.receiver.action(this.message);
}
}
queue.add(new LogCommand(receiver, '第一个命令'));
queue.add(new LogCommand(receiver, '第二个命令'));
queue.add(new LogCommand(receiver, '第三个命令'));
// 输出:
// 执行: 第一个命令
// (1秒后)执行: 第二个命令
// (再过1秒)执行: 第三个命令
命令模式的变体与应用场景
命令模式有多种变体,适用于不同场景:
- 简单命令:只包含执行方法的基本命令
- 可撤销命令:包含undo方法的命令
- 事务性命令:要么全部执行成功,要么全部回滚
- 宏命令:组合多个命令作为一个命令执行
典型应用场景包括:
- GUI按钮和菜单项
- 事务处理系统
- 进度条操作
- 多级撤销/重做功能
- 日志记录系统
- 任务调度系统
// 事务性命令示例
class Transaction {
constructor() {
this.commands = [];
this.executed = false;
}
add(command) {
if (this.executed) {
throw new Error('事务已执行,不能添加新命令');
}
this.commands.push(command);
}
execute() {
if (this.executed) return;
try {
this.commands.forEach(cmd => cmd.execute());
this.executed = true;
} catch (error) {
// 执行失败则回滚已执行的命令
for (let i = this.commands.length - 1; i >= 0; i--) {
if (this.commands[i].undo) {
this.commands[i].undo();
}
}
throw error;
}
}
}
// 使用示例
const transaction = new Transaction();
transaction.add(new UndoableCommand(receiverWithState, 10));
transaction.add(new UndoableCommand(receiverWithState, 20));
try {
transaction.execute();
console.log(receiverWithState.value); // 20
} catch (error) {
console.error('事务执行失败:', error);
}
命令模式与内存管理
在使用命令模式实现撤销/重做功能时,需要注意内存管理问题。长时间运行的应用程序可能会积累大量命令对象,导致内存占用过高。解决方案包括:
- 限制历史记录长度:只保留最近的N条命令
- 快照模式:定期保存完整状态而不是记录每个命令
- 命令压缩:将多个连续命令合并为一个
// 限制历史记录长度的命令管理器
class LimitedCommandManager extends CommandManager {
constructor(limit = 50) {
super();
this.limit = limit;
}
execute(command) {
super.execute(command);
if (this.undoStack.length > this.limit) {
this.undoStack.shift(); // 移除最旧的命令
}
}
}
// 快照命令示例
class SnapshotCommand {
constructor(receiver) {
this.receiver = receiver;
this.snapshot = null;
this.newState = null;
}
execute(newState) {
this.snapshot = JSON.stringify(this.receiver.state);
this.receiver.state = newState;
this.newState = JSON.stringify(newState);
}
undo() {
if (this.snapshot) {
this.receiver.state = JSON.parse(this.snapshot);
}
}
redo() {
if (this.newState) {
this.receiver.state = JSON.parse(this.newState);
}
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn