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 参数的值代表了渲染的类型,根据不同的类型调用对应的渲染方法。