撤销与重做是编辑器最基础也是最重要的功能之一。本文详细介绍命令模式实现、操作逆反、选择性撤销等高级特性。
命令模式基础
使用命令模式将每个编辑操作封装为可撤销的命令对象:
/* 命令接口 */
interface Command {
execute(): void; // 执行命令
undo(): void; // 撤销命令
redo(): void; // 重做命令
getDescription(): string;
}
/* 插入文本命令 */
class InsertCommand implements Command {
constructor(document, position, text) {
this.document = document;
this.position = position;
this.text = text;
}
execute() {
this.document.insert(this.position, this.text);
}
undo() {
this.document.delete(this.position, this.position + this.text.length);
}
redo() {
this.execute();
}
getDescription() {
return `插入 "${this.text}"`;
}
}
历史栈管理
使用两个栈分别管理撤销和重做历史:
/* 撤销/重做管理器 */
class UndoRedoManager {
constructor(maxHistory = 1000) {
this.undoStack: Command[] = [];
this.redoStack: Command[] = [];
this.maxHistory = maxHistory;
}
/* 执行新命令并清除重做栈 */
execute(command: Command) {
command.execute();
this.undoStack.push(command);
this.redoStack = []; // 新操作清除重做历史
// 限制历史长度
if (this.undoStack.length > this.maxHistory) {
this.undoStack.shift();
}
}
undo() {
if (this.canUndo()) {
const command = this.undoStack.pop();
command.undo();
this.redoStack.push(command);
}
}
redo() {
if (this.canRedo()) {
const command = this.redoStack.pop();
command.redo();
this.undoStack.push(command);
}
}
canUndo(): boolean {
return this.undoStack.length > 0;
}
canRedo(): boolean {
return this.redoStack.length > 0;
}
}
复合命令
将多个操作合并为一个原子命令:
/* 复合命令 - 将多个命令合并执行 */
class CompositeCommand implements Command {
constructor(commands: Command[]) {
this.commands = commands;
}
execute() {
this.commands.forEach(cmd => cmd.execute());
}
undo() {
// 逆序撤销
for (let i = this.commands.length - 1; i >= 0; i--) {
this.commands[i].undo();
}
}
redo() {
this.commands.forEach(cmd => cmd.redo());
}
getDescription() {
const descriptions = this.commands.map(c => c.getDescription());
return `复合操作: ${descriptions.join(', ')}`;
}
}
/* 查找并替换示例 */
function createReplaceCommand(doc, find, replace) {
const positions = doc.findAll(find);
const commands = positions.map(pos =>
new ReplaceCommand(doc, pos, find.length, replace)
);
return new CompositeCommand(commands);
}
选择性撤销
支持撤销到特定的编辑点:
/* 选择性撤销 - 撤销到指定历史位置 */
class SelectiveUndoManager extends UndoRedoManager {
/* 标记保存点 */
createCheckpoint(name: string) {
this.undoStack.push({
isCheckpoint: true,
name: name,
undo: () => {},
redo: () => {},
getDescription: () => `检查点: ${name}`
} as any);
}
/* 撤销到上一个检查点 */
undoToCheckpoint() {
while (this.canUndo()) {
const cmd = this.undoStack[this.undoStack.length - 1];
if (cmd['isCheckpoint']) {
this.undoStack.pop();
break;
}
this.undo();
}
}
/* 获取历史摘要 */
getHistorySummary() {
return this.undoStack.map((cmd, i) => ({
index: this.undoStack.length - i,
description: cmd.getDescription(),
isCheckpoint: cmd['isCheckpoint'] || false
}));
}
}
性能优化
- 限制历史栈最大长度,防止内存溢出
- 批量操作使用复合命令减少历史条目
- 长时间操作使用检查点标记
鼠标拖拽撤销
支持通过鼠标操作历史记录:
/* 时间线组件 - 可视化历史 */
class HistoryTimeline {
constructor(undoManager) {
this.manager = undoManager;
this.currentIndex = -1;
}
/* 渲染时间线 UI */
render() {
const history = this.manager.getHistorySummary();
const container = createDiv();
history.forEach((item, i) => {
const node = createHistoryNode(item);
if (i <= this.currentIndex) {
node.classList.add('applied');
}
node.onClick(() => this.jumpTo(i));
container.appendChild(node);
});
return container;
}
/* 跳转到指定历史位置 */
jumpTo(targetIndex: number) {
while (this.currentIndex > targetIndex) {
this.manager.undo();
this.currentIndex--;
}
while (this.currentIndex < targetIndex) {
this.manager.redo();
this.currentIndex++;
}
}
}
总结
撤销重做系统设计要点:
- 使用命令模式封装操作为可逆对象
- 使用双栈结构管理撤销和重做历史
- 复合命令实现原子性批量操作
- 检查点机制支持选择性撤销
- 合理限制历史长度防止内存问题