Model历史记录:X6撤销/重做功能原理
【免费下载链接】X6 一个使用SVG和HTML进行渲染的JavaScript绘图库。 项目地址: https://gitcode.com/GitHub_Trending/x6/X6
引言:绘图操作的时光机需求
在可视化建模场景中,用户经常需要在复杂图形操作中进行反复调整。想象一下在绘制流程图时误删关键节点、调整布局后发现不如之前版本,或是在ER图设计中错误连接实体关系——这些操作若无法挽回,将严重影响工作效率。X6作为专业的JavaScript绘图库,其撤销/重做(Undo/Redo)功能如同时光机,通过精准记录Model层状态变化,实现操作历史的回溯与重现。本文将深入剖析X6历史记录系统的实现原理,包括命令设计、状态管理、批处理机制等核心技术细节。
核心架构:命令模式的精妙应用
X6的撤销/重做系统基于命令模式(Command Pattern) 设计,将所有可撤销操作封装为命令对象,通过栈结构管理执行历史。其核心架构包含三大组件:
命令结构设计
每个命令对象(HistoryCommand)包含操作元数据:
event: 触发命令的事件类型(如cell:added、cell:change:position)data: 操作数据快照,分为创建型(CreationData)和变更型(ChangingData)batch: 是否为批处理命令options: 操作时的附加参数
// 节点创建命令示例
{
event: "cell:added",
batch: false,
data: {
id: "node1",
node: true,
props: { x: 100, y: 200, width: 80, height: 40 }
},
options: { ui: true }
}
// 属性变更命令示例
{
event: "cell:change:attrs",
batch: true,
data: {
id: "edge1",
key: "attrs",
prev: { line: { stroke: "#000" } },
next: { line: { stroke: "#f00" } }
},
options: { propertyPath: ["line", "stroke"] }
}
实现原理:状态捕获与时光回溯
1. 事件驱动的命令捕获
History插件通过监听Model层事件实现命令自动捕获:
关键事件监听逻辑位于startListening方法:
// src/plugin/history/index.ts
protected startListening() {
this.model.on('batch:start', this.initBatchCommand, this)
this.model.on('batch:stop', this.storeBatchCommand, this)
this.options.eventNames.forEach((name, index) => {
this.handlers[index] = this.addCommand.bind(this, name)
this.model.on(name, this.handlers[index])
})
}
默认监听的核心事件包括:
cell:added: 节点/边创建cell:removed: 节点/边删除cell:change:*: 属性变更(位置、样式、连接关系等)
2. 双栈存储模型
采用两个栈结构实现历史记录管理:
- 撤销栈(undoStack):存储已执行的命令序列
- 重做栈(redoStack):存储被撤销的命令序列
栈操作核心代码:
// 撤销操作
protected undo(options: KeyValue = {}) {
if (!this.disabled) {
const cmd = this.undoStack.pop()
if (cmd) {
this.revertCommand(cmd, options)
this.redoStack.push(cmd)
this.notify('undo', cmd, options)
}
}
}
// 重做操作
protected redo(options: KeyValue = {}) {
if (!this.disabled) {
const cmd = this.redoStack.pop()
if (cmd) {
this.applyCommand(cmd, options)
this.undoStackPush(cmd)
this.notify('redo', cmd, options)
}
}
}
3. 命令执行引擎
executeCommand方法是命令回放的核心,根据命令类型和revert标志执行相应操作:
protected executeCommand(cmd: HistoryCommand, revert: boolean, options: KeyValue) {
const cell = this.model.getCell(cmd.data.id!)
// 添加/删除操作的逆向执行
if ((isAddEvent(event) && revert) || (isRemoveEvent(event) && !revert)) {
cell && cell.remove(options)
}
// 属性变更操作的逆向执行
else if (isChangeEvent(event)) {
const data = cmd.data as HistoryChangingData
const value = revert ? data.prev[key] : data.next[key]
cell.prop(key, value, options)
}
}
高级特性:批处理与命令合并
批处理机制
复杂操作(如拖拽节点)会触发多个连续事件(如多次cell:change:position),X6通过批处理机制将其合并为单个命令:
合并逻辑位于storeBatchCommand方法,通过filterBatchCommand过滤无效命令:
protected filterBatchCommand(batchCommands: HistoryCommand[]) {
// 移除添加后立即删除的节点命令
// 合并重复的属性变更命令
// 过滤无实际变更的命令
}
智能命令合并
X6会自动识别需要合并的关联命令,例如将"移动节点+嵌入节点"的连续操作合并为一个撤销单元:
// 位置变更命令 + 父子关系变更命令 → 合并为一个复合命令
protected consolidateCommands() {
const lastGroup = this.undoStack[this.undoStack.length - 1]
const prevGroup = this.undoStack[this.undoStack.length - 2]
// 判断是否为位置变更后跟随父子关系变更
if (isPositionChange(prevGroup) && isParentChange(lastGroup)) {
prevGroup.push(...lastGroup)
this.undoStack.pop()
}
}
验证系统:确保历史状态一致性
Validator组件通过回调函数验证命令有效性,防止无效操作进入历史栈:
// 注册验证规则示例
graph.getPlugin('history').validate('cell:added', (err, cmd, next) => {
if (cmd.data.props.type === 'forbidden') {
next(new Error('禁止添加此类型节点'))
} else {
next(null)
}
})
验证流程:
- 命令添加时触发验证
- 按事件类型执行注册的验证回调
- 验证失败时可自动撤销操作(
cancelInvalid: true)
API与使用示例
基础API
X6为Graph实例扩展了完整的历史记录控制接口:
// 启用历史记录
graph.enableHistory()
// 执行操作
const node = graph.addNode({ id: 'node1', x: 100, y: 200 })
node.prop('attrs', { label: { text: '节点' } })
// 撤销
graph.undo() // 撤销属性变更
graph.undo() // 撤销节点添加
// 重做
graph.redo()
// 状态查询
graph.canUndo() // true
graph.getUndoStackSize() // 1
高级配置
初始化历史记录插件时可自定义行为:
new Graph({
plugins: [
{
name: 'history',
args: {
stackSize: 50, // 限制历史记录深度
ignoreAdd: false, // 是否忽略添加操作
beforeAddCommand: (event, args) => {
// 自定义命令过滤逻辑
return args.cell.isNode() // 仅记录节点操作
}
}
}
]
})
性能优化策略
栈大小限制
通过stackSize选项限制历史记录数量,防止内存溢出:
protected undoStackPush(cmd: HistoryCommands) {
if (this.stackSize > 0 && this.undoStack.length >= this.stackSize) {
this.undoStack.shift() // 移除最旧的命令
}
this.undoStack.push(cmd)
}
按需快照
对大型数据(如复杂节点属性)采用按需快照策略,仅记录变更部分而非完整对象:
// 仅记录变更的属性键值对
data.prev[key] = ObjectExt.cloneDeep(cell.previous(key))
data.next[key] = ObjectExt.cloneDeep(cell.prop(key))
延迟执行
通过队列机制延迟处理非关键命令,避免频繁操作导致的性能问题:
// renderer/scheduler.ts
queueJob(() => {
// 延迟执行命令存储,合并短时间内的重复操作
})
总结与实践建议
X6的撤销/重做系统通过命令模式+双栈结构+事件驱动的设计,实现了高效可靠的历史记录管理。核心优势包括:
- 细粒度控制:支持单个属性变更的精确撤销
- 智能合并:关联操作自动合并,提升用户体验
- 灵活扩展:通过验证器和自定义命令执行器扩展功能
- 性能优化:批处理和栈限制有效控制内存占用
实践建议:
- 对复杂操作使用
graph.startBatch()和graph.stopBatch()手动创建批处理 - 通过
beforeAddCommand过滤不需要记录的内部操作 - 使用验证器防止非法状态进入历史记录
- 对于超大图,建议将
stackSize限制在20-30条以优化内存
X6的历史记录系统不仅满足了基础的撤销/重做需求,更通过智能合并和验证机制为复杂可视化编辑场景提供了可靠支持。在实际项目中,合理配置历史记录参数并结合业务场景定制验证规则,能显著提升用户操作体验。
附录:核心API速查表
| 方法 | 描述 | 参数 |
|---|---|---|
graph.undo() | 执行撤销操作 | options: 附加参数 |
graph.redo() | 执行重做操作 | options: 附加参数 |
graph.canUndo() | 检查是否可撤销 | - |
graph.canRedo() | 检查是否可重做 | - |
graph.cleanHistory() | 清空历史记录 | options: 附加参数 |
history.validate() | 注册验证规则 | event: 事件名, callbacks: 验证函数 |
history.getUndoSize() | 获取撤销栈大小 | - |
history.getRedoSize() | 获取重做栈大小 | - |
通过这些API,开发者可以全面控制X6的历史记录功能,为用户提供流畅的操作体验。
【免费下载链接】X6 一个使用SVG和HTML进行渲染的JavaScript绘图库。 项目地址: https://gitcode.com/GitHub_Trending/x6/X6
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



