Model历史记录:X6撤销/重做功能原理

Model历史记录:X6撤销/重做功能原理

【免费下载链接】X6 一个使用SVG和HTML进行渲染的JavaScript绘图库。 【免费下载链接】X6 项目地址: https://gitcode.com/GitHub_Trending/x6/X6

引言:绘图操作的时光机需求

在可视化建模场景中,用户经常需要在复杂图形操作中进行反复调整。想象一下在绘制流程图时误删关键节点、调整布局后发现不如之前版本,或是在ER图设计中错误连接实体关系——这些操作若无法挽回,将严重影响工作效率。X6作为专业的JavaScript绘图库,其撤销/重做(Undo/Redo)功能如同时光机,通过精准记录Model层状态变化,实现操作历史的回溯与重现。本文将深入剖析X6历史记录系统的实现原理,包括命令设计、状态管理、批处理机制等核心技术细节。

核心架构:命令模式的精妙应用

X6的撤销/重做系统基于命令模式(Command Pattern) 设计,将所有可撤销操作封装为命令对象,通过栈结构管理执行历史。其核心架构包含三大组件:

mermaid

命令结构设计

每个命令对象(HistoryCommand)包含操作元数据:

  • event: 触发命令的事件类型(如cell:addedcell: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层事件实现命令自动捕获:

mermaid

关键事件监听逻辑位于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):存储被撤销的命令序列

mermaid

栈操作核心代码:

// 撤销操作
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通过批处理机制将其合并为单个命令:

mermaid

合并逻辑位于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)
  }
})

验证流程:

  1. 命令添加时触发验证
  2. 按事件类型执行注册的验证回调
  3. 验证失败时可自动撤销操作(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的撤销/重做系统通过命令模式+双栈结构+事件驱动的设计,实现了高效可靠的历史记录管理。核心优势包括:

  1. 细粒度控制:支持单个属性变更的精确撤销
  2. 智能合并:关联操作自动合并,提升用户体验
  3. 灵活扩展:通过验证器和自定义命令执行器扩展功能
  4. 性能优化:批处理和栈限制有效控制内存占用

实践建议:

  • 对复杂操作使用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绘图库。 【免费下载链接】X6 项目地址: https://gitcode.com/GitHub_Trending/x6/X6

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值