在part-1中我们重点讨论了G6启动的全流程。其实这个刚好就是graph文件所包含的内容,今天我们来讨论G6如何把用户的数据转化为节点实例,这里就涉及两个概念 Item 与 shape
- Item是宏观的,nodes数据转化为Node实例,edges数据转化为Edge实例
- shape是微观的,每个Node实例中包含多个shape图形,具体到在canvas层绘制的。
Item
Graph.addItem
通过part1的介绍,我们知道节点数据 通过addItem转化为G6的Item实例,核心代码如下
addItem(type, model) {return this.get('itemController').addItem(type, model);}
debug断点进入,发现它是将用户的数据node 作为model传入Node类中,同时将Node实例存储在itemMap中
PS:这块我是建议直接使用ES6的Map

这里有两个关键点
- Item.Node 这个类是如何实现的?
- 创建实例的参数 parent.addroup()做了什么事情?
按照代码的执行顺序,我们先看parent.addGroup()
parent.addGroup()
parent 是什么?
是在initGroup阶段,产生的nodeGroup。
var parent = graph.get(type + 'Group') || graph.get('group');
nodeGroup 是什么?
是canvas.addGroup(“canvas-root”).addGroup(“canvas-node”)
_initGroups() {const canvas = this.get('canvas');const id = this.get('canvas').get('el').id;const group = canvas.addGroup({id: id + '-root',className: Global.rootContainerClassName});if (this.get('groupByTypes')) {const edgeGroup = group.addGroup({id: id + '-edge',className: Global.edgeContainerClassName});const nodeGroup = group.addGroup({id: id + '-node',className: Global.nodeContainerClassName});this.set({ nodeGroup, edgeGroup });}this.set('group', group);}
canvas是什么?
canvs是由new G.canvas()实例化的对象
const canvas = new G.Canvas({containerDOM: container,width: this.get('width'),height: this.get('height'),renderer: this.get('renderer'),pixelRatio: this.get('pixelRatio')});
G.canvas是什么?
G是antv可视化体系的底层绘图引擎,canvas类是由Group继承而来的,Group类是由Element继承而来的,Util.extend 可以理解为ES6的extends继承,Util.augment可以理解为对原型链的merge扩展
const Canvas = function(cfg) {Canvas.superclass.constructor.call(this, cfg);};Util.extend(Canvas, Group);Util.augment(Canvas, Event, {init() {}})-------------------------------------------Util.extend(Group, Element);
这样追究下去,得专门写一篇关于G的源码解读,我们就先忽略,找到我们的需要研究的第二个关键方法addGroup
addGroup是什么?
addGroup是Group类的一个方法,用于图组的分类,目的是产生一个新的group实例,同时将这个实例push到当前的group的children中,便于后续的递归绘图
// 简化后的核心代码addGroup(param, cfg) {const canvas = this.get('canvas');rst = new Group();this.add(rst);return rst;},add(items) {const self = this;const children = self.get('children');children.push(item);return self;},
分析到这里,我们回过头再来看parent.addGroup(),它做的事情就是在第二层Group(id:canvas-node)上,将所有的nodes节点,挂载在第二层Group的children中。
我们debug验证下:
- 第一层group是root

- 第二层group是node和edge两个大类

- 第三层group就是用户的nodes节点或者是边的数据

- 第四层是group是具体绘图的shape。也是G绘图的最小单元了,它的添加是通过addShape完成的

通过addGroup方法,我们可以将用户输入的节点数据转化为canvas的group,挂载在canvas的每层group的children中。这样后续我们使用canvas API就可以通过children,递归绘制图形
Item.Node()
constructor
了解完参数group的由来,我们接下来将重点放在Node类本身的实现上。源码地址 src/item/node.js
class Node extends Item {}
我们在Node class中没有发现constructor函数,那这是ES6 class的一种简写,得先走一遍父类的constructor函数。通过查看父类Item的构造函数,我们发现
class Item {constructor(cfg) {const defaultCfg = {};this._cfg = Util.mix(defaultCfg, this.getDefaultCfg(), cfg);const group = cfg.group;group.set('item', this);let id = this.get('model').id;if (!id || id === '') {id = Util.uniqueId(this.get('type'));}this.set('id', id);group.set('id', id);this.init();this.draw();}
- 写法和Graph实例风格保持一致,所有的配置存在_cfg中
- group.set(‘item’, this);说明Item实例对象被存储在canvas的group的item(key值)下。这也就能解释为什么G6的事件,参数e中有item对象
- id 如果没有在model(数据)中设置,那么就会在内部生成一个uuid
init /draw
初始化是要得到ShapeFactory对象,type只有两种类型,node和edge。绘制就是拿到这个shapeFactory对象,调用其draw方法
init() {const shapeFactory = Shape.getFactory(this.get('type'));this.set('shapeFactory', shapeFactory);}_drawInner() {const self = this;const shapeFactory = self.get('shapeFactory');const model = self.get('model');self.updatePosition(model);const cfg = self.getShapeCfg(model);shapeFactory.draw(cfg.shape, cfg, group);}
估计大家看到这儿,头一次觉得晕乎乎,Shape是什么?shapeFactory是什么?
Shape
Shape初始化是一个空对象,它的核心方法有两个 registerFactory和 getFactory
**
registerFactory
注册Shape的工厂方法,比如用户想要注册”Node”,那么将在Shape这个对象上,添加一个Node类,这个类的基类是ShapeFactoryBase,同时产生registerNode方法,挂载在Shape对象上。
建议大家直接debug理解,因为它的注册方法不是genRegisterFactory,是通过一个函数,修改一个外部的对象,追加上注册方法,还用到了mixin,工厂方法。对于习惯了函数式编程和数据不可变的同学,可能会抓狂,感觉像goto语句,稍不留神就不知道跑到哪里去了,所以建议直接debug查看比较好。

// 获得 ShapeFactoryShape.getFactory = function(factoryType) {const self = this;factoryType = ucfirst(factoryType);return self[factoryType];};
读到这里,我们需要回到item的init/draw阶段,init阶段拿到的是NodeFactory,draw阶段调用的是NodeFactory的draw方法
shape.draw是什么?
接上图,shape.draw 对于我们自定义节点,就是registerNode(‘my-node’,{draw:{} })中的draw方法。这个时候,我们将demo中的自定义节点减少,只保留“hsf” 这个自定义节点,核心思路是通过canvas中的group.addShape将用户自定义的shape存放在当前group的children下。
G6.registerNode('hsf', {draw(cfg,group) {group.addShape('circle', {attrs: {x: 36,y: 40,r:55,stroke:'#71cd00',lineWidth: 5,}})return group}})

summary
思路梳理
- Item是宏观上的,定义了Node和Edge的数据结构
- Shape是微观上的,定义了每个Node实例下的每个Shape是如何组合和定义的
用户把数据 {nodes,edges} 通过addItem方法灌入G6,nodes转化为Node实例,edges转化为Edge实例,也通过group.addGroup,分别加入nodeGroup的children和edgeGroup的children中。用户通过官方定义的节点或者自己定义的节点,在draw方法里调用group.addShape方法,将每个图形shape存储在所属group的children中。
类表如下
| 分类 | 方法名 | 底层实现 | 功能描述 |
|---|---|---|---|
| Item | getBBox | group.getBBox() | 获取元素的包围盒 |
| toFront | group.toFront() | 绘制元素到最前面 | |
| toBack | group.toBack() | 绘制元素到最后面 | |
| show | group.show() | 显示元素 | |
| hide | group.hide() | 隐藏元素 | |
| destory | group.remove | 销毁元素 | |
| updatePosition | group.resetMatrix() group.translate(model.x,model.y) |
更新位置 | |
| refresh/update | - x,y改变,调用updatePostion - model改变,调用draw方法 |
更新/刷新元素 | |
| setState | ShapeFactory.setState | 更改元素状态 | |
| clearStates | ShapeFactory.setState | 清除元素状态 | |
| Node | getInEdges | ||
| getOutEdges | |||
| Edge | setSource | ||
| setTarget |
思维导图
后续
接下来,我们需要完成最后一步:G6.paint(),通过G6完成图的绘制,详见Part-3
