mxgraph 系列【4】:事务管理

3,464 阅读7分钟

事务 是 mxGraph 内部实现的一套更新控制方法,在一次事务过程会持续收集所有变更请求,直到事务结束后一次性推送到渲染器 mxCellRenderer 执行图形UI更新,或者在出现异常时允执行回滚。技术上来说, 事务 并不是必要的,我们可以直接使用图形更新接口,结果会被即时渲染到图形上,但是这种操作违背了 mxGraph 的设计理念,可能出现性能问题,而且更新过程出现异常时无法撤回已经执行的变更。

mxGraph  事务 接口其实非常简单,但理解接口背后的实现将有助于编写出性能更佳,更健壮的代码;也有助于理解调用更新接口后, mxGraph 内部如何处理变更并最终渲染到页面。本文主要内容包括:

  1. 事务的作用与实现
  2. 异常回滚

1. 事务管理


简单复习下事务的概念,事务 本质上是借助某种技术手段,将多个原子操作绑定成单个复合操作,事务过程中如果所有原子操作都成功执行,则该事务被视为成功,若其中任何一个出错,则视情况可选择回滚整个事务操作,以保证操作前后系统状态的一致性。 mxGraph 的事务功能与此类似,当我们需要一次执行多个图形更新操作时,可将代码放入一个事务上下文中执行。

mxGraph 启动事务功能非常简单,例如:

const graph = new mxGraph(document.getElementById('root'));
const parent = graph.getDefaultParent();

// 启动一次更新事务
const model = graph.getModel();
model.beginUpdate();
try {
  // 插入一个矩形
  const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
  // 插入第二个矩形
  const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
  // 连接两个矩形
  graph.insertEdge(parent, null, '', v1, v2);
} finally {
  // 结束更新会话
	model.endUpdate();
}


示例第6行调用 mxGraphModel.prototype.beginUpdate启动了一个更新事务,后续第9-13行连续插入三个图形,之后在第16行调用 mxGraphModel.prototype.endUpdate 结束该次事务。这段代码参考自官方文档 《Core mxGraph Architecture》 一节,是 mxGraph 比较推荐的写法,特点是将更新操作放入 try-catch 块中;将 beginUpdate 调用放在 try-catch 前;将 endUpdate 放在 finally ,这样即使更新过程触发异常,也能正常渲染异常代码行之前的更新操作,具体介绍请看下一节。

2. 异常回滚


我们先来看看,事务过程如果发生异常, mxGraph 会如何处理。修改上例代码,主动抛出异常:

const graph = new mxGraph(document.getElementById('root'));
const parent = graph.getDefaultParent();

// 启动一次更新会话
const model = graph.getModel();
model.beginUpdate();
try {
  // 插入一个矩形
  const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
  throw new Error('try throw exception');
  const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
  // 连接两个矩形
  graph.insertEdge(parent, null, '', v1, v2);
} finally {
  // 结束更新会话
	model.endUpdate();
}

修改前后效果对比:

image.png

可以看出,默认情况下在 try-catch 中出现异常中断时,异常语句之前的更新代码会被正常执行,而之后的代码则没有应用到图形上。此时,视场景需求可选择使用 mxGraphModel.prototype.currentEdit.undo 接口回滚所有操作,更新后的代码:

const graph = new mxGraph(document.getElementById('root'));
const parent = graph.getDefaultParent();

// 启动一次更新会话
const model = graph.getModel();
model.beginUpdate();
try {
  // 插入一个矩形
  const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
  throw new Error('try throw exception');
  const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
  // 连接两个矩形
  graph.insertEdge(parent, null, '', v1, v2);
} catch(e){
  // 在异常块调用undo函数,回滚更新操作。
  model.currentEdit.undo();
} finally {
  // 结束更新会话
	model.endUpdate();
}

这样就可以达到要么全部成功,要么全部回滚的效果,保持业务状态与图形效果的一致性。

提示:

上例 try-catch 模板代码同样适用于基于 promise 的异步操作,不过不能用 then 回调风格,而需要改用 async-await 风格。关于 async-await 的更多介绍,可参考我的另一篇文章 《async/await 应用指南》。

3. 源码分析

事务功能由 mxGraphModel 类实现,每个 mxGraph 实例对应一个 mxGraphModel 对象,用于管理文档状态 mxCell 属性,可通过 getModel 方法获得。三个类的关系如下图:

> 提示:

mxCell 是mxGraph 内部状态管理的基础单元,在《底层数据结构 mxCell》一节中有更详细介绍。

**
事务 的实现代码其实特别简单,主要完成:

  1. 执行 beginUpdate 时, this.updateLevel++
  2. endUpdate 则 this.updateLevel--
  3. endUpdate 函数内部,判断若 this.udpateLevel === 0 则:
    1. 触发 endUpdate 事件
    2. 调用 this.currentEdit.notify() 通知 mxGraph 执行图形更新流程
    3. 重置 this.currentEdit 对象

提示:

主要代码分布在 mxGraphModel.js#L1946 、 mxGraphModel.js#L1921 、mxGraphModel.js#L1871 、mxUndoableEdit.js#L131 。

3.1 嵌套会话

事务接口支持嵌套调用,例如:

const graph = new mxGraph(document.getElementById('root'));
const parent = graph.getDefaultParent();

const insertVertex = (...arg) => {
	model.beginUpdate();
  try {
    return graph.insertVertex(parent, null, ...arg);
  } finally {
		model.endUpdate();
  }
};

const model = graph.getModel();
model.beginUpdate();
try {
  const v1 = insertVertex('Hello,', 20, 20, 80, 30);
  const v2 = insertVertex('World!', 200, 150, 80, 30);
  graph.insertEdge(parent, null, '', v1, v2);
} finally {
  // 结束更新会话
	model.endUpdate();
}

上例这种调用方式非常合理,也是 mxGraph 内部大量使用的代码风格。上例中只有在第一次调用 beginUpdate ,也就是第14行会触发 startEdit 事件;第一次调用 endUpdate也就是第21行会触发 endEditbeforeUndochangenotifyundo 事件;中间的调用只会触发 beginUpdateendUpdate 事件。

为方便讨论,两种情况区分对待,首次调用称作 事务 ,后面的调用统一称为 会话,两种调用时机形成一种栈结构**。**事务过程触发的事件包括:

  1. startEdit: 首次调用 beginUpdate 时触发,标志着一次更新 事务 的开始
  2. beginUpdate: 调用 beginUpdate 时触发,标志一次更新 会话 的开始,一个事务过程可以多次触发 beginUpdate 事件
  3. execute:调用更新接口,并将变更内容保存到 currentEdit 对象后触发
  4. executed: 同上
  5. endUpdate: 调用 endUpdate 时触发,标志一次更新会话的结束
  6. endEdit:最后调用 endUpdate ,即计数器 updateLevel === 0 时触发
  7. beforeUndo: 执行 undo 操作前触发,比较诡异的是,这里的 undo 操作并不是用户代码执行的,而是在事务过程的一部分,我理解这里应该更多的是发出“准备开始”信号,事件名极具迷惑性
  8. change:主要作用 currentEdit 通知渲染层执行更新, mxGraph 会监听该事件,并执行重绘操作
  9. notify:渲染完毕后触发,主要用于框架内执行后处理逻辑
  10. undo: 对应上述 beforeUndo ,实际意义是“渲染完毕”


上面事件列表中,比较重要的是 change 事件,mxGraph 内部监听该事件,执行重绘操作。这种设计能够实现无论嵌套了多少次会话,只有最外层会话执行了 endUpdate ,计数器 updateLevel 归0后才开始将更新绘制到图形上,事务过程中的更新操作不会被立即执行,某种程度上也提升了框架的性能。

提示:

务必保证 beginUpdate 与 endUpdate 是成对调用的,如果事务过程中某个 endUpdate 没有被正确执行,model 内部的计数器没有归0,就无法触发 change 事件,mxgraph 也就无法执行渲染。

3.2 事务回滚

从上面分析可知,model 实例内部维护了一个 currentEdit 对象,用于记录单次事务的所有更新操作,源码:

mxGraphModel.prototype.execute = function(change) {
  ...
	this.currentEdit.add(change);
	...
};

currentEdit 是 mxUndoableEdit 类型的实例,通过 changes 属性记录了事务中的更新操作,并提供undoredo接口用于撤销或重做该次事务变更。我们可以通过 model.currentEdit 或监听事件在事件参数中获取:

// 1. 直接获取 currentEdit 对象
model.currentEdit.undo();

// 2. 监听change事件
model.addListener('change', (model, event)=>{
  const {edit} = event.properties;
  edit.undo();
});