diff --git a/packages/g6/__tests__/unit/utils/data.spec.ts b/packages/g6/__tests__/unit/utils/data.spec.ts
index 98a68ff4967..a3e6a8f676d 100644
--- a/packages/g6/__tests__/unit/utils/data.spec.ts
+++ b/packages/g6/__tests__/unit/utils/data.spec.ts
@@ -1,5 +1,5 @@
import type { EdgeData, NodeData } from '@/src';
-import { cloneElementData, isEmptyData, mergeElementsData } from '@/src/utils/data';
+import { cloneElementData, isElementDataEqual, isEmptyData, mergeElementsData } from '@/src/utils/data';
describe('data', () => {
it('mergeElementsData', () => {
@@ -99,4 +99,56 @@ describe('data', () => {
expect(isEmptyData({ edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }] })).toBe(false);
expect(isEmptyData({ combos: [{ id: 'combo-1' }] })).toBe(false);
});
+
+ it('isElementDataEqual', () => {
+ expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1' })).toBe(true);
+ expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-2' })).toBe(false);
+
+ // children
+ expect(isElementDataEqual({ id: 'node-1', children: ['a', 'b'] }, { id: 'node-1', children: ['a', 'b'] })).toBe(
+ true,
+ );
+ expect(isElementDataEqual({ id: 'node-1', children: ['a', 'b'] }, { id: 'node-1', children: ['a', 'c'] })).toBe(
+ false,
+ );
+ expect(
+ isElementDataEqual({ id: 'node-1', children: ['a', 'b'] }, { id: 'node-1', children: ['a', 'b', 'c'] }),
+ ).toBe(false);
+ expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1', data: {} })).toBe(true);
+ expect(isElementDataEqual({ id: 'node-1', data: { value: 1 } }, { id: 'node-1', data: { value: 1 } })).toBe(true);
+
+ // states
+ expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1', states: [] })).toBe(true);
+ expect(isElementDataEqual({ id: 'node-1', states: [] }, { id: 'node-1', states: [] })).toBe(true);
+ expect(isElementDataEqual({ id: 'node-1', states: ['selected'] }, { id: 'node-1', states: ['selected'] })).toBe(
+ true,
+ );
+ expect(
+ isElementDataEqual({ id: 'node-1', states: ['selected'] }, { id: 'node-1', states: ['selected', 'hover'] }),
+ ).toBe(false);
+
+ // too deep
+ const obj = { a: 1 };
+ expect(
+ isElementDataEqual({ id: 'node-1', data: { value: { ...obj } } }, { id: 'node-1', data: { value: { ...obj } } }),
+ ).toBe(false);
+ expect(isElementDataEqual({ id: 'node-1', data: { value: obj } }, { id: 'node-1', data: { value: obj } })).toBe(
+ true,
+ );
+
+ // style
+ expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1', style: {} })).toBe(true);
+ expect(isElementDataEqual({ id: 'node-1', style: { fill: 'red' } }, { id: 'node-1', style: { fill: 'red' } })).toBe(
+ true,
+ );
+ expect(
+ isElementDataEqual({ id: 'node-1', style: { fill: 'red' } }, { id: 'node-1', style: { fill: 'blue' } }),
+ ).toBe(false);
+ expect(
+ isElementDataEqual(
+ { id: 'node-1', style: { fill: 'red' } },
+ { id: 'node-1', style: { fill: 'red', stroke: 'red' } },
+ ),
+ ).toBe(false);
+ });
});
diff --git a/packages/g6/src/runtime/data.ts b/packages/g6/src/runtime/data.ts
index aa062399db9..8126e3c123a 100644
--- a/packages/g6/src/runtime/data.ts
+++ b/packages/g6/src/runtime/data.ts
@@ -1,5 +1,5 @@
import { Graph as GraphLib } from '@antv/graphlib';
-import { isEqual, isUndefined, uniq } from '@antv/util';
+import { isUndefined, uniq } from '@antv/util';
import { COMBO_KEY, ChangeType, TREE_KEY } from '../constants';
import type { ComboData, EdgeData, GraphData, NodeData } from '../spec';
import type {
@@ -20,7 +20,7 @@ import type {
} from '../types';
import type { EdgeDirection } from '../types/edge';
import type { ElementType } from '../types/element';
-import { cloneElementData, mergeElementsData } from '../utils/data';
+import { cloneElementData, isElementDataEqual, mergeElementsData } from '../utils/data';
import { arrayDiff } from '../utils/diff';
import { toG6Data, toGraphlibData } from '../utils/graphlib';
import { idOf, parentIdOf } from '../utils/id';
@@ -292,9 +292,9 @@ export class DataController {
const { nodes: modifiedNodes = [], edges: modifiedEdges = [], combos: modifiedCombos = [] } = data;
const { nodes: originalNodes, edges: originalEdges, combos: originalCombos } = this.getData();
- const nodeDiff = arrayDiff(originalNodes, modifiedNodes, (node) => idOf(node));
- const edgeDiff = arrayDiff(originalEdges, modifiedEdges, (edge) => idOf(edge));
- const comboDiff = arrayDiff(originalCombos, modifiedCombos, (combo) => idOf(combo));
+ const nodeDiff = arrayDiff(originalNodes, modifiedNodes, (node) => idOf(node), isElementDataEqual);
+ const edgeDiff = arrayDiff(originalEdges, modifiedEdges, (edge) => idOf(edge), isElementDataEqual);
+ const comboDiff = arrayDiff(originalCombos, modifiedCombos, (combo) => idOf(combo), isElementDataEqual);
this.batch(() => {
this.addData({
@@ -430,7 +430,7 @@ export class DataController {
nodes.forEach((modifiedNode) => {
const id = idOf(modifiedNode);
const originalNode = toG6Data(model.getNode(id));
- if (isEqual(originalNode, modifiedNode)) return;
+ if (isElementDataEqual(originalNode, modifiedNode)) return;
const value = mergeElementsData(originalNode, modifiedNode);
this.pushChange({ value, original: originalNode, type: ChangeType.NodeUpdated });
@@ -476,7 +476,7 @@ export class DataController {
edges.forEach((modifiedEdge) => {
const id = idOf(modifiedEdge);
const originalEdge = toG6Data(model.getEdge(id));
- if (isEqual(originalEdge, modifiedEdge)) return;
+ if (isElementDataEqual(originalEdge, modifiedEdge)) return;
if (modifiedEdge.source && originalEdge.source !== modifiedEdge.source) {
model.updateEdgeSource(id, modifiedEdge.source);
@@ -499,7 +499,7 @@ export class DataController {
combos.forEach((modifiedCombo) => {
const id = idOf(modifiedCombo);
const originalCombo = toG6Data(model.getNode(id)) as ComboData;
- if (isEqual(originalCombo, modifiedCombo)) return;
+ if (isElementDataEqual(originalCombo, modifiedCombo)) return;
const value = mergeElementsData(originalCombo, modifiedCombo);
this.pushChange({ value, original: originalCombo, type: ChangeType.ComboUpdated });
diff --git a/packages/g6/src/runtime/element.ts b/packages/g6/src/runtime/element.ts
index 05df5a26215..6aa45363b66 100644
--- a/packages/g6/src/runtime/element.ts
+++ b/packages/g6/src/runtime/element.ts
@@ -664,11 +664,9 @@ export class ElementController {
const context = { animation, stage: 'expand', data: drawData } as const;
- // 将新增边添加到更新列表 / Add new edges to the update list
- add.edges.forEach((edge) => {
- const id = idOf(edge);
- if (!update.edges.has(id)) update.edges.set(id, edge);
- });
+ // 将新增节点/边添加到更新列表 / Add new nodes/edges to the update list
+ add.edges.forEach((edge) => update.edges.set(idOf(edge), edge));
+ add.nodes.forEach((node) => update.nodes.set(idOf(node), node));
this.updateElements(update, context);
diff --git a/packages/g6/src/utils/data.ts b/packages/g6/src/utils/data.ts
index 535b4cd77dc..5d8894a0f75 100644
--- a/packages/g6/src/utils/data.ts
+++ b/packages/g6/src/utils/data.ts
@@ -1,5 +1,6 @@
import { get } from '@antv/util';
import type { ComboData, EdgeData, GraphData, NodeData } from '../spec';
+import type { ElementDatum, ID } from '../types';
/**
* 合并两个 节点/边/Combo 的数据
@@ -62,3 +63,51 @@ export function cloneElementData(data
export function isEmptyData(data: GraphData) {
return !get(data, ['nodes', 'length']) && !get(data, ['edges', 'length']) && !get(data, ['combos', 'length']);
}
+
+/**
+ * 判断两个元素数据是否相等
+ *
+ * Determine if two element data are equal
+ * @param original - 原始数据 | original data
+ * @param modified - 修改后的数据 | modified data
+ * @returns 是否相等 | is equal
+ * @remarks
+ * 相比于 isEqual,这个方法不会比较更下层的数据
+ *
+ * Compared to isEqual, this method does not compare data at a lower level
+ */
+export function isElementDataEqual(original: Partial = {}, modified: Partial = {}) {
+ const {
+ states: originalStates = [],
+ data: originalData = {},
+ style: originalStyle = {},
+ children: originalChildren = [],
+ ...originalAttrs
+ } = original;
+ const {
+ states: modifiedStates = [],
+ data: modifiedData = {},
+ style: modifiedStyle = {},
+ children: modifiedChildren = [],
+ ...modifiedAttrs
+ } = modified;
+
+ const isArrayEqual = (arr1: unknown[], arr2: unknown[]) => {
+ if (arr1.length !== arr2.length) return false;
+ return arr1.every((item, index) => item === arr2[index]);
+ };
+ const isObjectEqual = (obj1: Record, obj2: Record) => {
+ const keys1 = Object.keys(obj1);
+ const keys2 = Object.keys(obj2);
+ if (keys1.length !== keys2.length) return false;
+ return keys1.every((key) => obj1[key] === obj2[key]);
+ };
+
+ if (!isObjectEqual(originalAttrs, modifiedAttrs)) return false;
+ if (!isArrayEqual(originalChildren as ID[], modifiedChildren as ID[])) return false;
+ if (!isArrayEqual(originalStates, modifiedStates)) return false;
+ if (!isObjectEqual(originalData, modifiedData)) return false;
+ if (!isObjectEqual(originalStyle, modifiedStyle)) return false;
+
+ return true;
+}
diff --git a/packages/g6/src/utils/diff.ts b/packages/g6/src/utils/diff.ts
index a1d5f0a69ad..ce3fecb2598 100644
--- a/packages/g6/src/utils/diff.ts
+++ b/packages/g6/src/utils/diff.ts
@@ -7,9 +7,15 @@ import { isEqual } from '@antv/util';
* @param original - 原始数组 | original array
* @param modified - 修改后的数组 | modified array
* @param key - 比较的 key | key to compare
+ * @param comparator - 比较函数 | compare function
* @returns 数组差异 | array diff
*/
-export function arrayDiff(original: T[], modified: T[], key: (d: T) => string | number) {
+export function arrayDiff(
+ original: T[],
+ modified: T[],
+ key: (d: T) => string | number,
+ comparator: (a?: T, b?: T) => boolean = isEqual,
+) {
const originalMap = new Map(original.map((d) => [key(d), d]));
const modifiedMap = new Map(modified.map((d) => [key(d), d]));
@@ -23,7 +29,7 @@ export function arrayDiff(original: T[], modified: T[], key: (d: T) => string
modifiedSet.forEach((key) => {
if (originalSet.has(key)) {
- if (!isEqual(originalMap.get(key), modifiedMap.get(key))) {
+ if (!comparator(originalMap.get(key), modifiedMap.get(key))) {
update.push(modifiedMap.get(key)!);
} else {
keep.push(modifiedMap.get(key)!);