antv-x6 源码解析(1) - x6 核心包解析

492 阅读8分钟

简介

本文原文地址: x6 源码深入解析 (x6.fosp.cc)

X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。

如果你想要了解 x6 的使用,那么请参考官方文档 X6·图编辑引擎

flow转存失败,建议直接上传图片文件

本文对 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

可以看出来,目录结构非常清晰,是基于 pnpmMonorepo,下面我们分别对 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,承载所有的画布的功能,不实现具体功能,仅仅是把所有功能组合起来,提供操作画布的 API
  • model 模型,位于 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.tsview.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, outgoingsincomings 抽象的描述了此模型。所有的增删改都需要维持模型的稳定,例如 outgoings 属性和 incomings 属性,在节点或边增加或删除后,仍然有效。

Model 模型中只有两种元素,即 NodeEdge,它们都是基于 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 增加了 sourcetarget 属性以及与这些属性相关的方法,下面,我们看它简化后的定义:

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))
  }

  // 对属性的操作方法
}

Edgesourcetarget 分别表示边的两个端点,也是存储在 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, CellViewCell 对应的视图。

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

NodeViewNode 对应的视图,它继承自 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

EdgeViewEdge 对应的视图,它继承自 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)
  }
}

Schedulermodel 上监听的事件最终是调用 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 级的事件

首先观察 Cellsetup 方法以及 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 监听了 storechange:*changed 事件,并将其重新分发为 Cell 上的事件和 Model 上的事件。

Cell 有两种类型:NodeEdge

Node 只在 updatePortData 增加了 ports:addedports:removed 事件

EdgeonLabelsChanged 增加了 labels:addedlabels:removed 事件,在 onVertexsChanged 增加了 vertexs:addedvertexs:removed 事件

Model 级的事件

现在,我们观察 Modelsetup 方法及 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 也调用了 Modelnotify,因此,所有 Cell 上的事件加上前缀后同样也会在 ModelGraph 上出现。

监听事件

  1. Graph 上的功能组件通过监听事件来进行变更

    • BackgroundManager 监听 scaletranslate 事件
    • GridManager 监听 scaletranslate 事件
    • HighlightManager 监听 cell:highlightcell:unhighlight 事件
    • PanningManager 监听 blank:mousedown, node:unhandled:mousedownedge:unhandled:mousedown 事件
    • VirtualRenderManager 监听 translate, scaleresize 事件
  2. 注册表中的内置工具通过监听事件来进行变更,例如 jumpover, stroke, CellEditor, SegmentsVertices

  3. CellView 视图中,观察 CellViewsetup,如下:

    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 事件时,请求视图更新。

  4. 渲染器的调度器中监听了 reseted, cell:added, cell:removed, cell:change:zIndex, cell:change:visible 事件,然后请求视图更新。

下一章:antv-x6 源码解析(2) - markup