From 2f132ef07e97da92dfcf132237c52bb41706eccb Mon Sep 17 00:00:00 2001 From: antv Date: Mon, 2 Sep 2024 15:52:04 +0800 Subject: [PATCH] feat(utils): add isElementDataEqual to optimize compare time cost --- packages/g6/__tests__/unit/utils/data.spec.ts | 54 ++++++++++++++++++- packages/g6/src/runtime/data.ts | 16 +++--- packages/g6/src/runtime/element.ts | 8 ++- packages/g6/src/utils/data.ts | 49 +++++++++++++++++ packages/g6/src/utils/diff.ts | 10 +++- 5 files changed, 121 insertions(+), 16 deletions(-) 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)!);