事务 是 mxGraph
内部实现的一套更新控制方法,在一次事务过程会持续收集所有变更请求,直到事务结束后一次性推送到渲染器 mxCellRenderer
执行图形UI更新,或者在出现异常时允执行回滚。技术上来说, 事务 并不是必要的,我们可以直接使用图形更新接口,结果会被即时渲染到图形上,但是这种操作违背了 mxGraph
的设计理念,可能出现性能问题,而且更新过程出现异常时无法撤回已经执行的变更。
mxGraph
事务 接口其实非常简单,但理解接口背后的实现将有助于编写出性能更佳,更健壮的代码;也有助于理解调用更新接口后, mxGraph
内部如何处理变更并最终渲染到页面。本文主要内容包括:
- 事务的作用与实现
- 异常回滚
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();
}
修改前后效果对比:
可以看出,默认情况下在
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》一节中有更详细介绍。
**
事务 的实现代码其实特别简单,主要完成:
- 执行
beginUpdate
时,this.updateLevel++
endUpdate
则this.updateLevel--
- 在
endUpdate
函数内部,判断若this.udpateLevel === 0
则:- 触发
endUpdate
事件 - 调用
this.currentEdit.notify()
通知mxGraph
执行图形更新流程 - 重置
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行会触发 endEdit
、 beforeUndo
、 change
、 notify
、 undo
事件;中间的调用只会触发 beginUpdate
、 endUpdate
事件。
为方便讨论,两种情况区分对待,首次调用称作 事务 ,后面的调用统一称为 会话,两种调用时机形成一种栈结构**。**事务过程触发的事件包括:
- startEdit: 首次调用
beginUpdate
时触发,标志着一次更新 事务 的开始 - beginUpdate: 调用
beginUpdate
时触发,标志一次更新 会话 的开始,一个事务过程可以多次触发beginUpdate
事件 - execute:调用更新接口,并将变更内容保存到
currentEdit
对象后触发 - executed: 同上
- endUpdate: 调用
endUpdate
时触发,标志一次更新会话的结束 - endEdit:最后调用
endUpdate
,即计数器updateLevel === 0
时触发 - beforeUndo: 执行
undo
操作前触发,比较诡异的是,这里的undo
操作并不是用户代码执行的,而是在事务过程的一部分,我理解这里应该更多的是发出“准备开始”信号,事件名极具迷惑性 - change:主要作用
currentEdit
通知渲染层执行更新,mxGraph
会监听该事件,并执行重绘操作 - notify:渲染完毕后触发,主要用于框架内执行后处理逻辑
- 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
属性记录了事务中的更新操作,并提供undo
、redo
接口用于撤销或重做该次事务变更。我们可以通过 model.currentEdit
或监听事件在事件参数中获取:
// 1. 直接获取 currentEdit 对象
model.currentEdit.undo();
// 2. 监听change事件
model.addListener('change', (model, event)=>{
const {edit} = event.properties;
edit.undo();
});