您现在的位置是:网站首页 > 命令模式(Command)的封装与撤销操作文章详情
命令模式(Command)的封装与撤销操作
陈川
【
JavaScript
】
13633人已围观
9727字
命令模式的基本概念
命令模式是一种行为型设计模式,它将请求封装成对象,从而允许用户使用不同的请求、队列或日志请求来参数化其他对象。这种模式的核心思想是将"做什么"和"谁来做"解耦,使得请求的发送者和接收者之间不直接交互。
在JavaScript中,命令模式通常由一个命令对象实现,该对象包含一个执行方法(execute)和一个撤销方法(undo)。命令对象知道接收者(执行实际操作的对象)和调用接收者的方法。
class Command {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
throw new Error('必须实现execute方法');
}
undo() {
throw new Error('必须实现undo方法');
}
}
class LightOnCommand extends Command {
constructor(light) {
super(light);
this.light = light;
}
execute() {
this.light.on();
}
undo() {
this.light.off();
}
}
class Light {
on() {
console.log('灯亮了');
}
off() {
console.log('灯灭了');
}
}
// 使用示例
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
lightOnCommand.execute(); // 输出: 灯亮了
lightOnCommand.undo(); // 输出: 灯灭了
命令模式的封装机制
命令模式通过将操作封装为对象,实现了操作的参数化和延迟执行。这种封装带来了几个显著优势:
- 解耦调用者和接收者:调用者不需要知道接收者的具体实现细节,只需要知道如何发送命令。
- 支持命令队列:可以很容易地将命令放入队列中,实现批处理或延迟执行。
- 支持日志和事务:可以记录命令的执行历史,便于实现撤销/重做功能。
一个更复杂的例子展示了如何封装多个操作:
class MacroCommand {
constructor() {
this.commands = [];
}
add(command) {
this.commands.push(command);
}
execute() {
this.commands.forEach(command => command.execute());
}
undo() {
// 反向执行撤销操作
[...this.commands].reverse().forEach(command => command.undo());
}
}
// 使用示例
const light = new Light();
const tv = {
on() { console.log('电视打开了'); },
off() { console.log('电视关闭了'); }
};
const lightOn = new LightOnCommand(light);
const tvOn = {
execute() { tv.on(); },
undo() { tv.off(); }
};
const macro = new MacroCommand();
macro.add(lightOn);
macro.add(tvOn);
macro.execute(); // 灯亮了 电视打开了
macro.undo(); // 电视关闭了 灯灭了
撤销操作的实现原理
命令模式实现撤销操作的核心在于:每个命令对象不仅知道如何执行操作,还知道如何撤销这个操作。通常有两种实现方式:
- 反向操作法:执行与原始操作相反的操作。例如,如果原始操作是"增加",则撤销操作就是"减少"。
- 状态恢复法:在执行操作前保存对象状态,撤销时恢复这个状态。
下面是一个使用状态恢复法实现撤销的编辑器示例:
class Editor {
constructor() {
this.content = '';
this.clipboard = '';
}
write(text) {
this.content += text;
}
copy() {
this.clipboard = this.content;
}
paste() {
this.content += this.clipboard;
}
getContent() {
return this.content;
}
}
class CopyCommand {
constructor(editor) {
this.editor = editor;
this.previousClipboard = '';
}
execute() {
this.previousClipboard = this.editor.clipboard;
this.editor.copy();
}
undo() {
this.editor.clipboard = this.previousClipboard;
}
}
class PasteCommand {
constructor(editor) {
this.editor = editor;
this.previousContent = '';
}
execute() {
this.previousContent = this.editor.getContent();
this.editor.paste();
}
undo() {
this.editor.content = this.previousContent;
}
}
// 使用示例
const editor = new Editor();
editor.write('Hello ');
const copyCmd = new CopyCommand(editor);
copyCmd.execute();
const pasteCmd = new PasteCommand(editor);
pasteCmd.execute(); // 内容变为 "Hello Hello "
pasteCmd.undo(); // 恢复为 "Hello "
复杂撤销栈的实现
在实际应用中,通常需要维护一个撤销栈来支持多级撤销。下面是一个完整的撤销/重做管理器的实现:
class CommandManager {
constructor() {
this.undoStack = [];
this.redoStack = [];
this.maxStackSize = 100; // 防止内存泄漏
}
execute(command) {
command.execute();
this.undoStack.push(command);
this.redoStack = []; // 执行新命令时清空重做栈
// 限制栈大小
if (this.undoStack.length > this.maxStackSize) {
this.undoStack.shift();
}
}
undo() {
if (this.undoStack.length === 0) return;
const command = this.undoStack.pop();
command.undo();
this.redoStack.push(command);
}
redo() {
if (this.redoStack.length === 0) return;
const command = this.redoStack.pop();
command.execute();
this.undoStack.push(command);
}
clear() {
this.undoStack = [];
this.redoStack = [];
}
}
// 使用示例
const editor = new Editor();
const cmdManager = new CommandManager();
editor.write('初始内容');
console.log(editor.getContent()); // "初始内容"
const writeCmd = {
execute: () => editor.write('添加的文字'),
undo: () => { editor.content = editor.content.slice(0, -'添加的文字'.length); }
};
cmdManager.execute(writeCmd);
console.log(editor.getContent()); // "初始内容添加的文字"
cmdManager.undo();
console.log(editor.getContent()); // "初始内容"
cmdManager.redo();
console.log(editor.getContent()); // "初始内容添加的文字"
命令模式在前端框架中的应用
命令模式在前端框架中有广泛应用,特别是在状态管理和用户交互处理方面。以下是几个典型应用场景:
- Redux中的action:Redux的action本质上是命令模式的实现,每个action描述了一个状态变更请求。
- Vuex的mutation:Vuex中的mutation也是命令模式的应用,它封装了状态变更操作。
- UI组件操作:按钮点击、菜单选择等用户交互通常可以抽象为命令。
下面是一个模拟Redux中使用命令模式的例子:
// 模拟Redux store
class Store {
constructor(reducer, initialState) {
this.state = initialState;
this.reducer = reducer;
this.history = [];
this.historyIndex = -1;
}
dispatch(action) {
// 保存当前状态用于撤销
if (this.historyIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.historyIndex + 1);
}
this.history.push(this.state);
this.historyIndex++;
this.state = this.reducer(this.state, action);
}
undo() {
if (this.historyIndex <= 0) return;
this.historyIndex--;
this.state = this.history[this.historyIndex];
}
redo() {
if (this.historyIndex >= this.history.length - 1) return;
this.historyIndex++;
this.state = this.history[this.historyIndex];
}
getState() {
return this.state;
}
}
// reducer函数
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
// 使用示例
const store = new Store(counterReducer, { count: 0 });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // { count: 2 }
store.undo();
console.log(store.getState()); // { count: 1 }
store.redo();
console.log(store.getState()); // { count: 2 }
命令模式与异步操作
命令模式也可以很好地处理异步操作,只需在命令对象中实现异步的execute和undo方法。下面是一个处理AJAX请求的命令示例:
class ApiCommand {
constructor(apiClient, endpoint, data) {
this.apiClient = apiClient;
this.endpoint = endpoint;
this.data = data;
this.previousData = null;
}
async execute() {
try {
// 保存当前状态以便撤销
const response = await this.apiClient.get(this.endpoint);
this.previousData = response.data;
// 执行更新操作
await this.apiClient.post(this.endpoint, this.data);
return true;
} catch (error) {
console.error('执行失败:', error);
return false;
}
}
async undo() {
if (!this.previousData) {
console.error('无法撤销: 没有之前的记录');
return false;
}
try {
await this.apiClient.post(this.endpoint, this.previousData);
return true;
} catch (error) {
console.error('撤销失败:', error);
return false;
}
}
}
// 模拟API客户端
const mockApiClient = {
dataStore: {
'/user': { name: 'John', age: 30 }
},
get(endpoint) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ data: { ...this.dataStore[endpoint] } });
}, 100);
});
},
post(endpoint, data) {
return new Promise(resolve => {
setTimeout(() => {
this.dataStore[endpoint] = data;
resolve();
}, 100);
});
}
};
// 使用示例
(async () => {
const updateUserCmd = new ApiCommand(
mockApiClient,
'/user',
{ name: 'Mike', age: 25 }
);
await updateUserCmd.execute();
console.log(mockApiClient.dataStore['/user']); // { name: 'Mike', age: 25 }
await updateUserCmd.undo();
console.log(mockApiClient.dataStore['/user']); // { name: 'John', age: 30 }
})();
命令模式的性能考量
虽然命令模式提供了很大的灵活性,但在实现时需要考虑一些性能问题:
- 内存使用:每个命令对象都会占用内存,特别是在需要支持多级撤销时,保存的状态可能会占用大量内存。
- 执行效率:某些命令可能需要复杂的撤销逻辑,这会影响性能。
- 序列化开销:如果需要将命令序列化(例如用于持久化或网络传输),可能会引入额外的开销。
下面是一个考虑性能优化的命令实现示例,使用差异记录而不是完整状态保存:
class EfficientTextEditor {
constructor() {
this.content = '';
this.version = 0;
this.changeLog = [];
}
insert(text, position) {
// 记录变更差异而不是完整内容
this.changeLog.push({
type: 'insert',
text,
position,
version: ++this.version
});
this.content = this.content.slice(0, position) + text + this.content.slice(position);
}
delete(position, length) {
const deletedText = this.content.slice(position, position + length);
this.changeLog.push({
type: 'delete',
text: deletedText,
position,
version: ++this.version
});
this.content = this.content.slice(0, position) + this.content.slice(position + length);
}
undo() {
if (this.changeLog.length === 0) return;
const lastChange = this.changeLog.pop();
this.version--;
if (lastChange.type === 'insert') {
// 撤销插入操作就是删除插入的文本
this.content = this.content.slice(0, lastChange.position) +
this.content.slice(lastChange.position + lastChange.text.length);
} else if (lastChange.type === 'delete') {
// 撤销删除操作就是重新插入被删除的文本
this.content = this.content.slice(0, lastChange.position) +
lastChange.text +
this.content.slice(lastChange.position);
}
}
getContent() {
return this.content;
}
}
// 使用示例
const editor = new EfficientTextEditor();
editor.insert('Hello', 0);
editor.insert(' World', 5);
console.log(editor.getContent()); // "Hello World"
editor.delete(5, 6);
console.log(editor.getContent()); // "Hello"
editor.undo();
console.log(editor.getContent()); // "Hello World"