@antv/x6的graph增量更新实践

867 阅读2分钟

reactflow商业化之后越来越夸张了,很多基础模块都收费了,而其他支持flow的一些库好用的也都得买License,于是就开始用@antv/x6了,但是x6官方并未提供增量更新的功能,于是考虑自己做个实现。

在使用x6渲染graph的时候存在以下数据转换流程:

graph TD
raw_data --> fn_transfer --> temp_flow_data --> fn_dagre_layout --> flow_data

通过这样的设计,对图进行操作的时候去更新raw_data,然后拿到生成的flow_data去与之前的flow_data进行对比,也就是diff,可以在diff过程中就直接更新graph,也可以通过diff拿到变更信息之后去更新graph。

这里采用第一种方法:

import type { Graph,  Node, Edge } from '@antv/x6'

type Flow = {
	nodes: Node.Metadata[]
	edges: Edge.Metadata[]
}

type Args = {
	graph: Graph
	prev_flow: Flow
	current_flow: Flow
}

export default ({ graph, prev_flow, current_flow }: Args) => {
     graph.batchUpdate(() => {
            //...
     })
}

节点处理流程如下:

第一步

遍历prev_flow.nodes,然后拿对应的id去current_flow.nodes里找,这一步是获取到哪些节点有更新和哪些节点被移除。

graph TD
forEach[prev_flow.nodes.forEach] -->|item| FIND(current_flow.nodes.find)
forEach[prev_flow.nodes.forEach] -->|item| GETNODE(graph.getCellById)
FIND -->|exist_item| EXIST{exist}
GETNODE --> |node|EXIST{exist}
EXIST -->|true| CHECK{props equal?}
EXIST -->|false| REMOVE{remove node}
CHECK{Check Equal} -->|!equal|UPDATE[update]
UPDATE --> END[end loop]
REMOVE --> END[end loop]

第二步

遍历current_flow.nodes,然后拿对应的id去prev_flow.nodes里找,看看新增了哪些节点,然后把这些节点添加到画布中。

graph TD
forEach[current_flow.nodes.forEach] -->|item| FIND(prev_flow.nodes.find)
FIND --> |!exist|PUSH[collect node]
PUSH --> ADDNODE[graph.addNodes]
ADDNODE --> END[end loop]
FIND --> |exist|END[end loop]

边处理流程和节点差不多,就不上流程图了,有一点需要注意的是,transform函数在生成edge时,每条edge的id是由它的source和target组成的,这样才好通过id找到edge,以及验证它在当前画布中是否存在,如果你不想要这么麻烦,可以直接删除所有edge,再全量添加,如果edge的id是自动生成的,使用diff算法更新的话,得考虑更新顺序。

其实之前用just-diff实现过一个基于just-diff最短路径更新算法提供的更新流程,最后写的太复杂了,性能上确实是最佳的,添加删除无任何闪烁,但是针对不同的flow调试起来简直是个灾难,边缘case太多,最后还是回到了这个方案,有利有弊吧。

上代码:

import { deepEqual } from 'fast-equals'

import type { Graph, Node, Edge } from '@antv/x6'

type Flow = {
	nodes: Node.Metadata[]
	edges: Edge.Metadata[]
}

type Args = {
	graph: Graph
	prev_flow: Flow
	current_flow: Flow
}

export default <T>({ graph, prev_flow, current_flow }: Args) => {
	graph.batchUpdate(() => {
		prev_flow.nodes.forEach((item) => {
			const exist_item = current_flow.nodes.find((it) => item.id === it.id)
			const node = graph.getCellById(item.id!)

			if (exist_item) {
				if (exist_item.x !== item.x) node.setPropByPath('position/x', exist_item.x)
				if (exist_item.y !== item.y) node.setPropByPath('position/y', exist_item.y)
				if (!deepEqual(exist_item.data, item.data)) node.setData(exist_item.data)
			} else {
				// 节点已被删除的情形
				node.remove()
			}
		})

		// 处理新增节点的情形
		graph.addNodes(
			current_flow.nodes.reduce((total, item) => {
				if (!prev_flow.nodes.find((it) => item.id === it.id)) total.push(item)

				return total
			}, [] as Array<Node.Metadata>)
		)

		prev_flow.edges.forEach((item) => {
			const exist_item = current_flow.edges.find((it) => item.id === it.id)
			const edge = graph.getCellById(item.id!)

			// 边已被删除的情形
			if (!exist_item && edge) edge.remove()
		})

		// 处理新增边的情形
		graph.addEdges(
			current_flow.edges.reduce((total, item) => {
				if (!prev_flow.edges.find((it) => item.id === it.id)) total.push(item)

				return total
			}, [] as Array<Edge.Metadata>)
		)
	})
}