Skip to content

Commit

Permalink
feat(utils): add isElementDataEqual to optimize compare time cost
Browse files Browse the repository at this point in the history
  • Loading branch information
antv committed Sep 2, 2024
1 parent f576afc commit 2f132ef
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 16 deletions.
54 changes: 53 additions & 1 deletion packages/g6/__tests__/unit/utils/data.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
16 changes: 8 additions & 8 deletions packages/g6/src/runtime/data.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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';
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
Expand All @@ -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 });
Expand Down
8 changes: 3 additions & 5 deletions packages/g6/src/runtime/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
49 changes: 49 additions & 0 deletions packages/g6/src/utils/data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { get } from '@antv/util';
import type { ComboData, EdgeData, GraphData, NodeData } from '../spec';
import type { ElementDatum, ID } from '../types';

/**
* <zh/> 合并两个 节点/边/Combo 的数据
Expand Down Expand Up @@ -62,3 +63,51 @@ export function cloneElementData<T extends NodeData | EdgeData | ComboData>(data
export function isEmptyData(data: GraphData) {
return !get(data, ['nodes', 'length']) && !get(data, ['edges', 'length']) && !get(data, ['combos', 'length']);
}

/**
* <zh/> 判断两个元素数据是否相等
*
* <en/> Determine if two element data are equal
* @param original - <zh/> 原始数据 | <en/> original data
* @param modified - <zh/> 修改后的数据 | <en/> modified data
* @returns <zh/> 是否相等 | <en/> is equal
* @remarks
* <zh/> 相比于 isEqual,这个方法不会比较更下层的数据
*
* <en/> Compared to isEqual, this method does not compare data at a lower level
*/
export function isElementDataEqual(original: Partial<ElementDatum> = {}, modified: Partial<ElementDatum> = {}) {
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<string, unknown>, obj2: Record<string, unknown>) => {
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;
}
10 changes: 8 additions & 2 deletions packages/g6/src/utils/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ import { isEqual } from '@antv/util';
* @param original - <zh/> 原始数组 | <en/> original array
* @param modified - <zh/> 修改后的数组 | <en/> modified array
* @param key - <zh/> 比较的 key | <en/> key to compare
* @param comparator - <zh/> 比较函数 | <en/> compare function
* @returns <zh/> 数组差异 | <en/> array diff
*/
export function arrayDiff<T>(original: T[], modified: T[], key: (d: T) => string | number) {
export function arrayDiff<T>(
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]));

Expand All @@ -23,7 +29,7 @@ export function arrayDiff<T>(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)!);
Expand Down

0 comments on commit 2f132ef

Please sign in to comment.