简介
本文原文地址: x6 源码深入解析 (x6.fosp.cc)
X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。
如果你想要了解 x6 的使用,那么请参考官方文档 X6·图编辑引擎
本文对 x6 源码的解析,可以帮助您理解 x6 的实现原理,具备更深入的使用 x6 的能力,也可以为您向 x6 贡献代码起到指南的作用。
源仓库地址: github.com/antvis/X6
代码目录结构
.
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── CONTRIBUTING.zh-CN.md
├── CONTRIBUTORS
├── CONTRIBUTORS.svg
├── LICENSE
├── README.en-us.md
├── README.md
├── SECURITY.md
├── examples
│ └── x6-example-features
├── flow.svg
├── karma.conf.js
├── package.json
├── packages
│ ├── x6 # 核心包
│ ├── x6-angular-shape # 适用于 angular
│ ├── x6-common # 公共包
│ ├── x6-devtool # 浏览器开发工具
│ ├── x6-geometry # 几何图形工具
│ ├── x6-plugin-clipboard # 剪切板插件
│ ├── x6-plugin-dnd # 拖拽创建插件
│ ├── x6-plugin-export # 导出插件
│ ├── x6-plugin-history # 历史记录插件
│ ├── x6-plugin-keyboard # 键盘快捷键插件
│ ├── x6-plugin-minimap # 缩略图插件
│ ├── x6-plugin-scroller # 滚动视图插件
│ ├── x6-plugin-selection # 选择插件
│ ├── x6-plugin-snapline # 对齐线插件
│ ├── x6-plugin-stencil # 对 dnd 插件的进一步的封装
│ ├── x6-plugin-transform # 变换插件
│ ├── x6-react-components # 适用于 react 的组件
│ ├── x6-react-shape # 适用于 react
│ └── x6-vue-shape # 适用于 vue
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── rollup.config.js
├── scripts
│ ├── build-less
│ ├── run-test
│ ├── sync-gitee
│ └── x6-build-tools
├── sites
│ └── x6-sites
├── tsconfig.json
└── turbo.json
可以看出来,目录结构非常清晰,是基于 pnpm
的 Monorepo
,下面我们分别对 packages
下的几个包进行分析。
x6
当前部分基于:
v2.11.1
x6 包是 X6 项目的核心。我们忽略其他部分,关注 src
的目录结构:
./packages/x6/src
├── __tests__
│ └── util
├── config
│ └── index.ts
├── graph # 核心
│ ├── background.ts # 背景
│ ├── base.ts
│ ├── coord.ts # 坐标系
│ ├── css.ts # css
│ ├── defs.ts # svg 的 defs
│ ├── events.ts # 事件类型定义
│ ├── graph.ts # Graph
│ ├── grid.ts # 网格
│ ├── highlight.ts # 高亮
│ ├── index.ts
│ ├── mousewheel.ts # 鼠标滚轮
│ ├── options.ts # 选项类型定义
│ ├── panning.ts # 画布平移
│ ├── size.ts # 画布尺寸
│ ├── transform.ts # 画布变换,包括缩放、旋转、过渡、定位
│ ├── view.ts # GraphView 是 Graph 的视图
│ └── virtual-render.ts # 虚拟渲染器,收集 200ms 内的变更,统一提交到渲染器
├── index.less
├── index.ts
├── model # 渲染模型
│ ├── animation.ts
│ ├── cell.ts
│ ├── collection.ts
│ ├── edge.ts
│ ├── index.ts
│ ├── model.ts
│ ├── node.ts
│ ├── port.ts
│ ├── registry.ts
│ └── store.ts
├── registry # 注册表
│ ├── attr
│ ├── background
│ ├── connection-point
│ ├── connection-strategy
│ ├── connector
│ ├── edge-anchor
│ ├── filter
│ ├── grid
│ ├── highlighter
│ ├── index.ts
│ ├── marker
│ ├── node-anchor
│ ├── port-label-layout
│ ├── port-layout
│ ├── registry.ts
│ ├── router
│ └── tool
├── renderer # 渲染器
│ ├── index.ts
│ ├── queueJob.ts
│ ├── renderer.ts
│ └── scheduler.ts
├── shape # 预定义的基础图形
│ ├── base.ts
│ ├── circle.ts
│ ├── edge.ts
│ ├── ellipse.ts
│ ├── html.ts
│ ├── image.ts
│ ├── index.ts
│ ├── path.ts
│ ├── poly.ts
│ ├── polygon.ts
│ ├── polyline.ts
│ ├── rect.ts
│ ├── text-block.ts
│ └── util.ts
├── style # 样式和主题
│ ├── index.less
│ ├── raw.ts
│ └── themes
├── util
│ └── index.ts
└── view # 渲染视图
├── attr.ts
├── cache.ts
├── cell.ts
├── edge.ts
├── flag.ts
├── index.ts
├── markup.ts
├── node.ts
├── tool.ts
└── view.ts
架构设计
x6 包主要分为以下部分:
Graph
画布,位于x6/src/graph/graph.ts
,承载所有的画布的功能,不实现具体功能,仅仅是把所有功能组合起来,提供操作画布的 APImodel
模型,位于x6/src/model/
,由画布内的元素构成的抽象模型,模型仅仅是对画布内元素的描述,不负责渲染view
视图,位于x6/src/view/
,负责对模型的实际渲染renderer
渲染器,位于x6/src/renderer/renderer.ts
,用于调度渲染的时机,并将模型和视图关联起来,它会在适合的时候将模型使用对应的视图进行渲染registry
注册表,位于x6/src/registry/
,将对应的功能使用特定的名称进行注册,以便在使用时直接使用名称,在内部从注册表中查询对应的实现- 事件驱动,画布的各个功能和插件间通过事件进行交流
Graph
首先关注 x6/src/graph/
目录,其中 graph.ts
是核心,它定义了画布,承载所有其他功能。其中的 base.ts
是此目录除 view.ts
外所有功能共同的父类。
为了方便理解,我们目前仅关心核心实现,那么此目录就只需要关注 graph.ts
和 view.ts
。
对于 graph.ts
,我们观察到 Graph
的大部分都是都其属性功能的封装,我们剔除这些部分后,得到有效代码如下:
export class Graph extends Basecoat<EventArgs> {
private installedPlugins: Set<Graph.Plugin> = new Set()
public model: Model
public readonly options: GraphOptions.Definition
public readonly css: Css
public readonly view: GraphView
public readonly grid: Grid
public readonly defs: Defs
public readonly coord: Coord
public readonly renderer: ViewRenderer
public readonly highlight: Highlight
public readonly transform: Transform
public readonly background: Background
public readonly panning: Panning
public readonly mousewheel: Wheel
public readonly virtualRender: VirtualRender
public readonly size: Size
constructor(options: Partial<GraphOptions.Manual>) {
super()
this.options = GraphOptions.get(options)
this.css = new Css(this)
this.view = new GraphView(this)
this.defs = new Defs(this)
this.coord = new Coord(this)
this.transform = new Transform(this)
this.highlight = new Highlight(this)
this.grid = new Grid(this)
this.background = new Background(this)
if (this.options.model) {
this.model = this.options.model
} else {
this.model = new Model()
this.model.graph = this
}
this.renderer = new ViewRenderer(this)
this.panning = new Panning(this)
this.mousewheel = new Wheel(this)
this.virtualRender = new VirtualRender(this)
this.size = new Size(this)
}
// #region plugin
use(plugin: Graph.Plugin, ...options: any[]) {
if (!this.installedPlugins.has(plugin)) {
this.installedPlugins.add(plugin)
plugin.init(this, ...options)
}
return this
}
getPlugin<T extends Graph.Plugin>(pluginName: string): T | undefined {
return Array.from(this.installedPlugins).find(
(plugin) => plugin.name === pluginName,
) as T
}
getPlugins<T extends Graph.Plugin[]>(pluginName: string[]): T | undefined {
return Array.from(this.installedPlugins).filter((plugin) =>
pluginName.includes(plugin.name),
) as T
}
enablePlugins(plugins: string[] | string) {
let postPlugins = plugins
if (!Array.isArray(postPlugins)) {
postPlugins = [postPlugins]
}
const aboutToChangePlugins = this.getPlugins(postPlugins)
aboutToChangePlugins?.forEach((plugin) => {
plugin?.enable?.()
})
return this
}
disablePlugins(plugins: string[] | string) {
let postPlugins = plugins
if (!Array.isArray(postPlugins)) {
postPlugins = [postPlugins]
}
const aboutToChangePlugins = this.getPlugins(postPlugins)
aboutToChangePlugins?.forEach((plugin) => {
plugin?.disable?.()
})
return this
}
isPluginEnabled(pluginName: string) {
const pluginIns = this.getPlugin(pluginName)
return pluginIns?.isEnabled?.()
}
disposePlugins(plugins: string[] | string) {
let postPlugins = plugins
if (!Array.isArray(postPlugins)) {
postPlugins = [postPlugins]
}
const aboutToChangePlugins = this.getPlugins(postPlugins)
aboutToChangePlugins?.forEach((plugin) => {
plugin.dispose()
})
return this
}
// #endregion
// #region dispose
@Basecoat.dispose()
dispose() {
this.clearCells()
this.off()
this.css.dispose()
this.defs.dispose()
this.grid.dispose()
this.coord.dispose()
this.transform.dispose()
this.highlight.dispose()
this.background.dispose()
this.mousewheel.dispose()
this.panning.dispose()
this.view.dispose()
this.renderer.dispose()
this.installedPlugins.forEach((plugin) => {
plugin.dispose()
})
}
// #endregion
}
GraphView
从上述代码中找到 GraphView
,它是画布的视图。现在我们进入它的实现,位于 view.ts
中。
其中 constructor
方法如下,我们为其添加注释:
constructor(protected readonly graph: Graph) {
// 执行父级的 `constructor`,生成一个 cid 其值是 "v0"
// 并且向 `View.views` 中增加了 {v0: this}
super()
// 解析自身的 markup 生成 DOM, 并在 selectors 中记录 `selector` 和对应的 DOM
// 在编辑器中点击下面的 `markup` 就可以找到描述画布的结构对象
const { selectors, fragment } = Markup.parseJSONMarkup(GraphView.markup)
this.background = selectors.background as HTMLDivElement
this.grid = selectors.grid as HTMLDivElement
this.svg = selectors.svg as SVGSVGElement
this.defs = selectors.defs as SVGDefsElement
this.viewport = selectors.viewport as SVGGElement
this.primer = selectors.primer as SVGGElement
this.stage = selectors.stage as SVGGElement
this.decorator = selectors.decorator as SVGGElement
this.overlay = selectors.overlay as SVGGElement
this.container = this.options.container
// 记录一个快照,可以在调用 restore 时还原,前提是不能直接修改子节点
this.restore = GraphView.snapshoot(this.container)
// 给 container 添加 x6-graph 的 class
Dom.addClass(this.container, this.prefixClassName('graph'))
// 将生成的 DOM 元素挂载到 container 中
Dom.append(this.container, fragment)
// 委托事件,让事件和对应的处理方法进行绑定
this.delegateEvents()
}
经过初始化,画布被渲染到文档中,并且开始监听鼠标键盘事件。
model
模型是画布内图像的抽象表示。
Model
Model
是由画布内的元素构成的抽象模型,其基本单位是 Cell
。
省略部分方法及其实现,简化后的 Model
定义如下:
export class Model extends Basecoat<Model.EventArgs> {
// 所有的 cell 存储在其中,并提供一些便捷的操作方法
public readonly collection: Collection
// 批处理计数器
protected readonly batches: KeyValue<number> = {}
// 批量新增时,正在被添加的节点
protected readonly addings: WeakMap<Cell, boolean> = new WeakMap()
public graph: Graph
// 模型中的节点 id
protected nodes: KeyValue<boolean> = {}
// 模型中的边 id
protected edges: KeyValue<boolean> = {}
// 从指定节点出发的边的 id 列表
protected outgoings: KeyValue<string[]> = {}
// 指向指定节点的边的 id 列表
protected incomings: KeyValue<string[]> = {}
constructor(cells: Cell[] = []) {
super()
this.collection = new Collection(cells)
this.setup()
}
/** 发出通知,触发 Model 上的事件以及触发 Graph 上的事件 */
notify<Key extends keyof Model.EventArgs>(
name: Key,
args: Model.EventArgs[Key],
) {...}
/** 监听 collection 属性上的事件,进行进一步的事件分发或触发对应处理方法 */
protected setup() {...}
// 其他增删查改方法
}
在 Model
中,nodes
, edges
, outgoings
和 incomings
抽象的描述了此模型。所有的增删改都需要维持模型的稳定,例如 outgoings
属性和 incomings
属性,在节点或边增加或删除后,仍然有效。
在 Model
模型中只有两种元素,即 Node
和 Edge
,它们都是基于 Cell
的扩展。下面我们来看 Cell
。
Cell
Cell
是所有元素的基类。它定义了画布中的元素的基本属性。
同样的,我们看一个简化后的 Cell
定义:
export class Cell<
Properties extends Cell.Properties = Cell.Properties,
> extends Basecoat<Cell.EventArgs> {
protected static markup: Markup
protected static defaults: Cell.Defaults = {}
protected static attrHooks: Attr.Definitions = {}
protected static propHooks: Cell.PropHook[] = []
// 配置 markup, defaults, attrHooks 和 propHooks
public static config<C extends Cell.Config = Cell.Config>(presets: C) {...}
public readonly id: string
// 存储处理后的 metadata, 包括 props, attr 等,以及提供便捷的增删改查方法
protected readonly store: Store<Cell.Properties>
// 动画相关
protected readonly animation: Animation
constructor(metadata: Cell.Metadata = {}) {
super()
const ctor = this.constructor as typeof Cell
const defaults = ctor.getDefaults(true)
const props = ObjectExt.merge(
{},
this.preprocess(defaults),
this.preprocess(metadata),
)
this.id = props.id || StringExt.uuid()
this.store = new Store(props)
this.animation = new Animation(this)
this.setup()
this.init()
this.postprocess(metadata)
}
/** 预留的初始化方法 */
init() {}
/** 预处理方法,对 metadata 应用 propHooks,增加 id 属性 */
protected preprocess(
metadata: Cell.Metadata,
ignoreIdCheck?: boolean,
): Properties {
const id = metadata.id
const ctor = this.constructor as typeof Cell
const props = ctor.applyPropHooks(this, metadata)
if (id == null && ignoreIdCheck !== true) {
props.id = StringExt.uuid()
}
return props as Properties
}
/** 预留的后处理方法 */
protected postprocess(metadata: Cell.Metadata) {} // eslint-disable-line
/** 监听 store 上的 change 和 changed 事件并进行分发,触发实例的事件 */
protected setup() {...}
/** 发出通知,触发模型上的事件 */
notify<Key extends keyof Cell.EventArgs>(
name: Key,
args: Cell.EventArgs[Key],
) {
this.trigger(name, args)
const model = this.model
if (model) {
model.notify(`cell:${name}`, args)
if (this.isNode()) {
model.notify(`node:${name}`, { ...args, node: this })
} else if (this.isEdge()) {
model.notify(`edge:${name}`, { ...args, edge: this })
}
}
return this
}
// 其他对 store 和 animation 提供操作的方法
}
Cell
中静态属性 markup
,只能通过静态方法 config
进行设置。因此,Cell
定义了: 不能直接使用 new
进行创建,在创建前必须先调用 config
进行配置。
对 Cell
的操作就是对其上存储的 store
中的数据进行操作,所有操作都会发出事件,以便后续渲染器对操作进行渲染。
Node
Node
是模型中的节点,继承自 Cell
,它相比 Cell
增加了 角度(angle
),位置(position
),尺寸(size
),连接桩(port
)属性,以及与这些属性相关的方法,下面,我们看它简化后的定义:
export class Node<
Properties extends Node.Properties = Node.Properties,
> extends Cell<Properties> {
protected static defaults: Node.Defaults = {
angle: 0,
position: { x: 0, y: 0 },
size: { width: 1, height: 1 },
}
protected readonly store: Store<Node.Properties>
protected port: PortManager
constructor(metadata: Node.Metadata = {}) {
super(metadata)
this.initPorts()
}
/** 对 metadata 中的 x,y,width,height,统一为 position 和 size */
protected preprocess(
metadata: Node.Metadata,
ignoreIdCheck?: boolean,
): Properties {...}
// 对属性的操作方法
}
与 Cell
相同,所有数据都存储在 store
中,Node
提供的所有操作方法本质上都是对 store
中的数据进行增删查改的封装。
Edge
Edge
是模型中用于连接 Node
的有向线条,被称为边。Edge
也是继承自 Cell
,它相比 Cell
增加了 source
和 target
属性以及与这些属性相关的方法,下面,我们看它简化后的定义:
export class Edge<
Properties extends Edge.Properties = Edge.Properties,
> extends Cell<Properties> {
protected static defaults: Edge.Defaults = {}
protected readonly store: Store<Edge.Properties>
constructor(metadata: Edge.Metadata = {}) {
super(metadata)
}
/** 对 metadata 中的 source 和 target 的不同表示统一为同一种表示 */
protected preprocess(metadata: Edge.Metadata, ignoreIdCheck?: boolean) {...}
protected setup() {
super.setup()
this.on('change:labels', (args) => this.onLabelsChanged(args))
this.on('change:vertices', (args) => this.onVertexsChanged(args))
}
// 对属性的操作方法
}
Edge
的 source
和 target
分别表示边的两个端点,也是存储在 store
中,Edge
主要提供了修改这两个属性的方法。
view
视图负责对抽象模型的实际渲染。
View
View
是视图的抽象类,定义于 x6/src/view/view.ts
。
我们省略一些用于便捷操作的方法,只关注核心部分,如下:
export abstract class View<A extends EventArgs = any> extends Basecoat<A> {
// 视图的唯一 id
public readonly cid: string
// 视图渲染的容器
public container: Element
// 包含 DOM 元素的选择器
protected selectors: Markup.Selectors
// 渲染优先级,值越大越高
public get priority() {
return 2
}
constructor() {
super()
this.cid = Private.uniqueId()
View.views[this.cid] = this
}
// 确认更新,由调度器调用,是渲染的入口,flag 是表示渲染的类型
confirmUpdate(flag: number, options: any): number {
return 0
}
}
View
定义了渲染的入口 confirmUpdate
, 继承 View
时需要重写此方法来进行实际的渲染。
CellView
我们首先来看 CellView
, CellView
是 Cell
对应的视图。
CellView
定义了 Cell
如何渲染,但实际上 Cell
并不会直接渲染,所以,它只是定义了一个渲染的框架,由实际继承 Cell
的元素实现一个对应的继承自 CellView
的视图来负责渲染。
下面我们对 CellView
进行简化,如下:
export class CellView<
Entity extends Cell = Cell,
Options extends CellView.Options = CellView.Options,
> extends View<CellView.EventArgs> {
protected static defaults: Partial<CellView.Options> = {
isSvgElement: true,
rootSelector: 'root',
priority: 0,
bootstrap: [],
actions: {},
}
public static getDefaults() {
return this.defaults
}
public static config<T extends CellView.Options = CellView.Options>(
options: Partial<T>,
) {
this.defaults = this.getOptions(options)
}
public graph: Graph
// 视图对应的模型
public cell: Entity
// 包含实际 DOM 元素的选择器
protected selectors: Markup.Selectors
// 选项
protected readonly options: Options
// 标记管理器,flag 决定了渲染的类型和渲染的方式
protected readonly flag: FlagManager
// 属性管理器,提供操作 DOM 属性的方法
protected readonly attr: AttrManager
// 缓存
protected readonly cache: Cache
constructor(cell: Entity, options: Partial<Options> = {}) {
super()
this.cell = cell
this.options = this.ensureOptions(options)
this.graph = this.options.graph
this.attr = new AttrManager(this)
this.flag = new FlagManager(
this,
this.options.actions,
this.options.bootstrap,
)
this.cache = new Cache(this)
this.setContainer(this.ensureContainer())
this.setup()
this.init()
}
protected init() {}
protected setup() {
this.cell.on('changed', ({ options }) => this.onAttrsChange(options))
}
protected onAttrsChange(options: Cell.MutateOptions) {
let flag = this.flag.getChangedFlag()
if (options.updated || !flag) {
return
}
if (options.dirty && this.hasAction(flag, 'update')) {
flag |= this.getFlag('render') // eslint-disable-line no-bitwise
}
// tool changes should be sync render
if (options.toolId) {
options.async = false
}
if (this.graph != null) {
// 请求渲染器调用当前视图的 confirmUpdate 方法进行视图更新
this.graph.renderer.requestViewUpdate(this, flag, options)
}
}
}
CellView
中定义了属性,并且当接收到属性变化完成事件后,请求渲染器进行视图更新,渲染器会在适合的时机调用当前视图的 confirmUpdate
方法,CellView
并没有实现 confirmUpdate
方法,因为需要继承 CellView
的视图根据实际模型渲染不同的结果。
NodeView
NodeView
是 Node
对应的视图,它继承自 CellView
。
下面我们对 NodeView
进行简化,如下:
export class NodeView<
Entity extends Node = Node,
Options extends NodeView.Options = NodeView.Options,
> extends CellView<Entity, Options> {
protected portsCache: { [id: string]: NodeView.PortCache } = {}
// 渲染的入口,flag 代表了不同的渲染方式
confirmUpdate(flag: number, options: any = {}) {
let ret = flag
if (this.hasAction(ret, 'ports')) {
this.removePorts()
this.cleanPortsCache()
}
if (this.hasAction(ret, 'render')) {
this.render()
ret = this.removeAction(ret, [
'render',
'update',
'resize',
'translate',
'rotate',
'ports',
'tools',
])
} else {
ret = this.handleAction(
ret,
'resize',
() => this.resize(),
'update', // Resize method is calling `update()` internally
)
ret = this.handleAction(
ret,
'update',
() => this.update(),
// `update()` will render ports when useCSSSelectors are enabled
Config.useCSSSelector ? 'ports' : null,
)
ret = this.handleAction(ret, 'translate', () => this.translate())
ret = this.handleAction(ret, 'rotate', () => this.rotate())
ret = this.handleAction(ret, 'ports', () => this.renderPorts())
ret = this.handleAction(ret, 'tools', () => {
if (this.getFlag('tools') === flag) {
this.renderTools()
} else {
this.updateTools(options)
}
})
}
return ret
}
// 全量渲染
render() {
// 清空容器
this.empty()
// 首先渲染骨架
this.renderMarkup()
// 设置尺寸
this.resize()
// 设置位置
this.updateTransform()
if (!Config.useCSSSelector) {
// 渲染连接桩
this.renderPorts()
}
// 渲染工具
this.renderTools()
return this
}
...
}
NodeView
实现了 confirmUpdate
,这是视图渲染的入口,无论是初始化渲染还是更新都是从这里开始的,flag
参数的值代表了渲染的类型,根据不同的类型调用对应的渲染方法。
这里省略了很多对属性的操作方法,他们最终都会修改 DOM,这里的所说的属性也就是 DOM 元素的属性。
EdgeView
EdgeView
是 Edge
对应的视图,它继承自 CellView
。
下面我们对 EdgeView
进行简化,如下:
export class EdgeView<
Entity extends Edge = Edge,
Options extends EdgeView.Options = EdgeView.Options,
> extends CellView<Entity, Options> {
protected readonly POINT_ROUNDING = 2
// 边的路径
public path: Path
// 路由点
public routePoints: Point[]
// 源锚点
public sourceAnchor: Point
// 目标锚点
public targetAnchor: Point
// 源点实际所在的点
public sourcePoint: Point
// 目标点实际所在的点
public targetPoint: Point
// 源点箭头所在的点
public sourceMarkerPoint: Point
// 目标箭头所在的点
public targetMarkerPoint: Point
// 源视图,如果边的源是视图
public sourceView: CellView | null
// 目标视图,如果边的目标是视图
public targetView: CellView | null
// 源 Magnet,通常指连接桩
public sourceMagnet: Element | null
// 目标 Magnet,通常指连接桩
public targetMagnet: Element | null
// 标签容器
protected labelContainer: Element | null
protected labelCache: { [index: number]: Element }
protected labelSelectors: { [index: number]: Markup.Selectors }
// 渲染的入口,flag 代表了不同的渲染方式
confirmUpdate(flag: number, options: any = {}) {
let ref = flag
if (this.hasAction(ref, 'source')) {
if (!this.updateTerminalProperties('source')) {
return ref
}
ref = this.removeAction(ref, 'source')
}
if (this.hasAction(ref, 'target')) {
if (!this.updateTerminalProperties('target')) {
return ref
}
ref = this.removeAction(ref, 'target')
}
const graph = this.graph
const sourceView = this.sourceView
const targetView = this.targetView
if (
graph &&
((sourceView && !graph.renderer.isViewMounted(sourceView)) ||
(targetView && !graph.renderer.isViewMounted(targetView)))
) {
// Wait for the sourceView and targetView to be rendered.
return ref
}
if (this.hasAction(ref, 'render')) {
this.render()
ref = this.removeAction(ref, ['render', 'update', 'labels', 'tools'])
return ref
}
ref = this.handleAction(ref, 'update', () => this.update(options))
ref = this.handleAction(ref, 'labels', () => this.onLabelsChange(options))
ref = this.handleAction(ref, 'tools', () => this.renderTools())
return ref
}
// 全量渲染
render() {
// 清空容器
this.empty()
// 渲染骨架
this.renderMarkup()
// 渲染标签
this.labelContainer = null
this.renderLabels()
// 更新
this.update()
// 渲染工具
this.renderTools()
return this
}
// 更新
update(options: any = {}) {
// 清空缓存
this.cleanCache()
// 更新连接
this.updateConnection(options)
// 更新属性
const attrs = this.cell.getAttrs()
if (attrs != null) {
this.updateAttrs(this.container, attrs, {
selectors: this.selectors,
})
}
// 更新标签位置
this.updateLabelPositions()
// 更新工具
this.updateTools(options)
return this
}
...
}
EdgeView
实现了 confirmUpdate
,这是视图渲染的入口,无论是初始化渲染还是更新都是从这里开始的,flag
参数的值代表了渲染的类型,根据不同的类型调用对应的渲染方法。
renderer
渲染器是用于渲染 Cell
的。渲染器提供了 requestViewUpdate
方法用于外部调用,以请求视图更新,并且提供了视图的获取方法。
渲染器分为三个部分:
renderer.ts
-Renderer
渲染器scheduler.ts
-Scheduler
调度器queueJob.ts
-JobQueue
任务队列,向任务队列添加任务,任务队列会在页面空闲时执行任务中的回调函数
Renderer
Renderer
仅仅是一个入口,是对调度器的封装,其上的 requestViewUpdate
方法是调用的调度器的 requestViewUpdate
方法,外部可以调用此方法请求视图更新。
Render
的代码非常简单,这里不做过多讲解。
Scheduler
调度器是渲染器的核心,其在内部监听模型上的 cell
增删事件,然后请求视图更新。
Scheduler
简化后定义如下:
export class Scheduler extends Disposable {
public views: KeyValue<Scheduler.View> = {}
public willRemoveViews: KeyValue<Scheduler.View> = {}
protected zPivots: KeyValue<Comment>
private graph: Graph
private renderArea?: Rectangle
private queue: JobQueue
constructor(graph: Graph) {
super()
this.queue = new JobQueue()
this.graph = graph
this.init()
}
protected init() {
this.startListening()
this.renderViews(this.model.getCells())
}
protected startListening() {
this.model.on('reseted', this.onModelReseted, this)
this.model.on('cell:added', this.onCellAdded, this)
this.model.on('cell:removed', this.onCellRemoved, this)
this.model.on('cell:change:zIndex', this.onCellZIndexChanged, this)
this.model.on('cell:change:visible', this.onCellVisibleChanged, this)
}
protected stopListening() {
this.model.off('reseted', this.onModelReseted, this)
this.model.off('cell:added', this.onCellAdded, this)
this.model.off('cell:removed', this.onCellRemoved, this)
this.model.off('cell:change:zIndex', this.onCellZIndexChanged, this)
this.model.off('cell:change:visible', this.onCellVisibleChanged, this)
}
// cell 新增时会调用此方法
protected renderViews(cells: Cell[], options: any = {}) {
cells.sort((c1, c2) => {
if (c1.isNode() && c2.isEdge()) {
return -1
}
return 0
})
cells.forEach((cell) => {
const id = cell.id
const views = this.views
let flag = 0
let viewItem = views[id]
if (viewItem) {
flag = Scheduler.FLAG_INSERT
} else {
// 此处创建 cell 对应的视图
const cellView = this.createCellView(cell)
if (cellView) {
cellView.graph = this.graph
flag = Scheduler.FLAG_INSERT | cellView.getBootstrapFlag()
viewItem = {
view: cellView,
flag,
options,
state: Scheduler.ViewState.CREATED,
}
this.views[id] = viewItem
}
}
if (viewItem) {
// 调用请求视图更新
this.requestViewUpdate(
viewItem.view,
flag,
options,
this.getRenderPriority(viewItem.view),
false,
)
}
})
this.flush()
}
// 最重要的方法,请求视图更新,对模型上的事件的监听函数(例如 `onModelReseted`)最终都是调用此函数
requestViewUpdate(
view: CellView,
flag: number,
options: any = {},
priority: JOB_PRIORITY = JOB_PRIORITY.Update,
flush = true,
) {
const id = view.cell.id
const viewItem = this.views[id]
if (!viewItem) {
return
}
viewItem.flag = flag
viewItem.options = options
const priorAction = view.hasAction(flag, ['translate', 'resize', 'rotate'])
if (view.isNodeView() && priorAction) {
priority = JOB_PRIORITY.PRIOR // eslint-disable-line
flush = false // eslint-disable-line
}
// 向任务队列增加任务,任务队列会在空闲的时候执行 cb 回调
this.queue.queueJob({
id,
priority,
cb: () => {
this.renderViewInArea(view, flag, options)
},
})
// 受影响的边递归调用 requestViewUpdate
const effectedEdges = this.getEffectedEdges(view)
effectedEdges.forEach((edge) => {
this.requestViewUpdate(edge.view, edge.flag, options, priority, false)
})
if (flush) {
this.flush()
}
}
// 渲染处于渲染范围内的视图
protected renderViewInArea(view: CellView, flag: number, options: any = {}) {
const cell = view.cell
const id = cell.id
const viewItem = this.views[id]
if (!viewItem) {
return
}
let result = 0
// 判断是否处于渲染范围内
if (this.isUpdateable(view)) {
// 更新视图
result = this.updateView(view, flag, options)
viewItem.flag = result
} else {
// 已经被渲染了,那么也需要更新
if (viewItem.state === Scheduler.ViewState.MOUNTED) {
result = this.updateView(view, flag, options)
viewItem.flag = result
} else {
// 等待进行渲染范围
viewItem.state = Scheduler.ViewState.WAITTING
}
}
if (result) {
if (
cell.isEdge() &&
(result & view.getFlag(['source', 'target'])) === 0
) {
this.queue.queueJob({
id,
priority: JOB_PRIORITY.RenderEdge,
cb: () => {
this.updateView(view, flag, options)
},
})
}
}
}
// 更新视图
protected updateView(view: View, flag: number, options: any = {}) {
if (view == null) {
return 0
}
if (CellView.isCellView(view)) {
if (flag & Scheduler.FLAG_REMOVE) {
this.removeView(view.cell as any)
return 0
}
if (flag & Scheduler.FLAG_INSERT) {
this.insertView(view)
flag ^= Scheduler.FLAG_INSERT // eslint-disable-line
}
}
if (!flag) {
return 0
}
// 调用视图上的 `confirmUpdate` 进行实际的渲染
return view.confirmUpdate(flag, options)
}
}
Scheduler
在 model
上监听的事件最终是调用 requestViewUpdate
方法。
首先我们关注 renderViews
方法,在节点新增时,此方法会被调用。我们可以看到当 cell.id
对应的视图不存在于缓存中时,会调用 createCellView
创建视图,此方法按照以下优先级创建对应的视图:
options.createCellView
如果提供了此选项,那么使用此选项创建cell.view
如果在指定了view
的名称,那么从CellView.registry
注册表中获取- 如果是
node
,默认为NodeView
- 如果是
edge
,默认为EdgeView
创建视图后,最终调用 requestViewUpdate
方法,将创建的视图作为参数传入。
requestViewUpdate
方法向任务队列增加了一个任务,在回调中调用了 renderViewInArea
方法。
renderViewInArea
方法是用于渲染处于渲染范围内的视图,如果处于渲染范围内,它会调用 updateView
方法。
updateView
是更新视图的方法。如果视图被标记删除,那么会执行删除操作;如果视图需要插入,那么会执行插入操作。最后调用视图上的 confirmUpdate
方法进行实际的渲染。
registry
注册表主要位于 x6/src/registry
目录下,此目录下的所有注册表都有预设的值。
注册表还在 Edge
, Node
, CellView
的命名空间中存在。
注册表都在对应的命名空间中导出 registry
。例如 Node
的命名空间中导出 Node.registry
。
下面列举出 x6/src/registry
下的所有注册表:
- attr - 属性
- background - 背景
- connection-point - 连接点
- connector - 连接器
- filter - svg 过滤器
- grid - 网格
- highlighter - 高亮
- marker - 箭头
- node-anchor = 节点锚点
- port-label-layout - 连接桩标签布局
- port-layout - 连接桩布局
- router - 边的路由
- tool - 工具
事件驱动
x6 是通过事件进行驱动的,事件分为以下几类:
-
鼠标键盘事件
-
模型变更事件
-
自定义事件
鼠标键盘事件
鼠标键盘事件的入口在 GraphView。
GraphView.events
定义了事件和绑定的方法。它在 GraphView
初始化时作为 delegateEvents
的参数传递给了 View
,然后将其绑定在了 graph.container
上。
模型变更事件
Cell 级的事件
首先观察 Cell
的 setup
方法以及 notify
方法,如下:
export class Cell<
Properties extends Cell.Properties = Cell.Properties,
> extends Basecoat<Cell.EventArgs> {
protected setup() {
this.store.on('change:*', (metadata) => {
const { key, current, previous, options } = metadata
this.notify('change:*', {
key,
options,
current,
previous,
cell: this,
})
this.notify(`change:${key}` as keyof Cell.EventArgs, {
options,
current,
previous,
cell: this,
})
const type = key as Edge.TerminalType
if (type === 'source' || type === 'target') {
this.notify(`change:terminal`, {
type,
current,
previous,
options,
cell: this,
})
}
})
this.store.on('changed', ({ options }) =>
this.notify('changed', { options, cell: this }),
)
}
notify<Key extends keyof Cell.EventArgs>(
name: Key,
args: Cell.EventArgs[Key],
): this
notify(name: Exclude<string, keyof Cell.EventArgs>, args: any): this
notify<Key extends keyof Cell.EventArgs>(
name: Key,
args: Cell.EventArgs[Key],
) {
this.trigger(name, args)
const model = this.model
if (model) {
model.notify(`cell:${name}`, args)
if (this.isNode()) {
model.notify(`node:${name}`, { ...args, node: this })
} else if (this.isEdge()) {
model.notify(`edge:${name}`, { ...args, edge: this })
}
}
return this
}
}
store
中存储的是 Cell
的 props, attrs 等元数据,当元数据发生变化时,首先会触发 change:*
事件,所有更改完成后会触发 changed
事件。Cell
监听了 store
的 change:*
和 changed
事件,并将其重新分发为 Cell
上的事件和 Model
上的事件。
Cell
有两种类型:Node
和 Edge
。
Node
只在 updatePortData
增加了 ports:added
和 ports:removed
事件
Edge
在 onLabelsChanged
增加了 labels:added
和 labels:removed
事件,在 onVertexsChanged
增加了 vertexs:added
和 vertexs:removed
事件
Model 级的事件
现在,我们观察 Model
的 setup
方法及 notify
方法,如下:
export class Model extends Basecoat<Model.EventArgs> {
protected setup() {
const collection = this.collection
collection.on('sorted', () => this.notify('sorted', null))
collection.on('updated', (args) => this.notify('updated', args))
collection.on('cell:change:zIndex', () => this.sortOnChangeZ())
collection.on('added', ({ cell }) => {
this.onCellAdded(cell)
})
collection.on('removed', (args) => {
const cell = args.cell
this.onCellRemoved(cell, args.options)
// Should trigger remove-event manually after cell was removed.
this.notify('cell:removed', args)
if (cell.isNode()) {
this.notify('node:removed', { ...args, node: cell })
} else if (cell.isEdge()) {
this.notify('edge:removed', { ...args, edge: cell })
}
})
collection.on('reseted', (args) => {
this.onReset(args.current)
this.notify('reseted', args)
})
collection.on('edge:change:source', ({ edge }) =>
this.onEdgeTerminalChanged(edge, 'source'),
)
collection.on('edge:change:target', ({ edge }) => {
this.onEdgeTerminalChanged(edge, 'target')
})
}
notify<Key extends keyof Model.EventArgs>(
name: Key,
args: Model.EventArgs[Key],
): this
notify(name: Exclude<string, keyof Model.EventArgs>, args: any): this
notify<Key extends keyof Model.EventArgs>(
name: Key,
args: Model.EventArgs[Key],
) {
this.trigger(name, args)
const graph = this.graph
if (graph) {
if (name === 'sorted' || name === 'reseted' || name === 'updated') {
graph.trigger(`model:${name}`, args)
} else {
graph.trigger(name, args)
}
}
return this
}
}
Model
上的 collection
是存储 Cell
的容器,当 collection
中的 Cell
发生变更时(例如,增加,删除,排序等),它会触发事件,Model
监听这些事件并进行重新分发为 Model
上的事件和 Graph
上的事件。
注意,Cell
也调用了 Model
的 notify
,因此,所有 Cell
上的事件加上前缀后同样也会在 Model
和 Graph
上出现。
监听事件
-
Graph
上的功能组件通过监听事件来进行变更BackgroundManager
监听scale
和translate
事件GridManager
监听scale
和translate
事件HighlightManager
监听cell:highlight
和cell:unhighlight
事件PanningManager
监听blank:mousedown
,node:unhandled:mousedown
和edge:unhandled:mousedown
事件VirtualRenderManager
监听translate
,scale
和resize
事件
-
注册表中的内置工具通过监听事件来进行变更,例如
jumpover
,stroke
,CellEditor
,Segments
和Vertices
-
CellView
视图中,观察CellView
的setup
,如下:export class CellView<Entity extends Cell = Cell, Options extends CellView.Options = CellView.Options> extends View<CellView.EventArgs> { protected setup() { this.cell.on('changed', ({ options }) => this.onAttrsChange(options)) } protected onAttrsChange(options: Cell.MutateOptions) { let flag = this.flag.getChangedFlag() if (options.updated || !flag) { return } if (options.dirty && this.hasAction(flag, 'update')) { flag |= this.getFlag('render') // eslint-disable-line no-bitwise } // tool changes should be sync render if (options.toolId) { options.async = false } if (this.graph != null) { this.graph.renderer.requestViewUpdate(this, flag, options) } } }
当监听到
Cell
上的changed
事件时,请求视图更新。 -
渲染器的调度器中监听了
reseted
,cell:added
,cell:removed
,cell:change:zIndex
,cell:change:visible
事件,然后请求视图更新。