Skip to content

Commit

Permalink
refactor(runtime): add element lifecycle event (#5611)
Browse files Browse the repository at this point in the history
* test: remove async from createGraph

* refactor(runtime): add element lifecycle event
  • Loading branch information
Aarebecca authored Apr 3, 2024
1 parent d0c3c7d commit 61fa3d1
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 30 deletions.
4 changes: 0 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
"afterelementstatechange",
"afterelementtranslate",
"afterelementupdate",
"afterelementvisibilitychange",
"afterelementzindexchange",
"afterlayout",
"afterrender",
"aftersizechange",
Expand All @@ -24,8 +22,6 @@
"beforeelementstatechange",
"beforeelementtranslate",
"beforeelementupdate",
"beforeelementvisibilitychange",
"beforeelementzindexchange",
"beforelayout",
"beforerender",
"beforesizechange",
Expand Down
2 changes: 1 addition & 1 deletion packages/g6/__tests__/unit/runtime/element.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('ElementController', () => {
let graph: Graph;

beforeAll(async () => {
graph = await createGraph({
graph = createGraph({
data: {
nodes: [
{ id: 'node-1', style: { x: 100, y: 100, fill: 'red', stroke: 'pink', lineWidth: 1 }, data: { value: 100 } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('element visibility', () => {
let graph: Graph;

beforeAll(async () => {
graph = await createGraph({
graph = createGraph({
data: {
nodes: [
{ id: 'node-1', style: { x: 50, y: 50 } },
Expand Down
2 changes: 1 addition & 1 deletion packages/g6/__tests__/unit/runtime/element/z-index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('element z-index', () => {
let graph: Graph;

beforeAll(async () => {
graph = await createGraph({
graph = createGraph({
data: {
nodes: [
{ id: 'node-1', style: { x: 150, y: 150, color: 'red' } },
Expand Down
101 changes: 101 additions & 0 deletions packages/g6/__tests__/unit/runtime/graph/event.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { GraphEvent } from '@/src';
import { createGraph } from '@@/utils';

describe('event', () => {
it('graph lifecycle event', async () => {
const graph = createGraph({
data: {
nodes: [{ id: 'node-1' }, { id: 'node-2' }],
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],
},
layout: {
type: 'grid',
},
});

const sequence: string[] = [];

const addSequence = (type: string) => () => {
sequence.push(type);
};

const beforeDraw = jest.fn(addSequence('beforeDraw'));
const afterDraw = jest.fn(addSequence('afterDraw'));
const beforeRender = jest.fn(addSequence('beforeRender'));
const afterRender = jest.fn(addSequence('afterRender'));
const beforeLayout = jest.fn(addSequence('beforeLayout'));
const afterLayout = jest.fn(addSequence('afterLayout'));

graph.on(GraphEvent.BEFORE_DRAW, beforeDraw);
graph.on(GraphEvent.AFTER_DRAW, afterDraw);
graph.on(GraphEvent.BEFORE_RENDER, beforeRender);
graph.on(GraphEvent.AFTER_RENDER, afterRender);
graph.on(GraphEvent.BEFORE_LAYOUT, beforeLayout);
graph.on(GraphEvent.AFTER_LAYOUT, afterLayout);

await graph.render();

expect(beforeDraw).toHaveBeenCalledTimes(1);
expect(afterDraw).toHaveBeenCalledTimes(1);
expect(beforeRender).toHaveBeenCalledTimes(1);
expect(afterRender).toHaveBeenCalledTimes(1);
expect(beforeLayout).toHaveBeenCalledTimes(1);
expect(afterLayout).toHaveBeenCalledTimes(1);

expect(sequence).toEqual(['beforeRender', 'beforeDraw', 'afterDraw', 'beforeLayout', 'afterLayout', 'afterRender']);

graph.destroy();
});

it('element lifecycle event', async () => {
const graph = createGraph({
data: {
nodes: [{ id: 'node-1' }, { id: 'node-2' }],
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],
},
});

const create = jest.fn();
const update = jest.fn();
const destroy = jest.fn();

graph.on(GraphEvent.AFTER_ELEMENT_CREATE, create);
graph.on(GraphEvent.AFTER_ELEMENT_UPDATE, update);
graph.on(GraphEvent.AFTER_ELEMENT_DESTROY, destroy);

await graph.draw();

expect(create).toHaveBeenCalledTimes(3);
expect(update).toHaveBeenCalledTimes(0);
expect(destroy).toHaveBeenCalledTimes(0);

expect(create.mock.calls[0][0].elementType).toEqual('node');
expect(create.mock.calls[0][0].data.id).toEqual('node-1');
expect(create.mock.calls[1][0].elementType).toEqual('node');
expect(create.mock.calls[1][0].data.id).toEqual('node-2');
expect(create.mock.calls[2][0].elementType).toEqual('edge');
expect(create.mock.calls[2][0].data.id).toEqual('edge-1');

create.mockClear();

graph.addNodeData([{ id: 'node-3' }]);
graph.updateData({
nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-3' }],
});
graph.removeNodeData(['node-2']);

await graph.draw();

expect(create).toHaveBeenCalledTimes(1);
expect(update).toHaveBeenCalledTimes(2);
expect(destroy).toHaveBeenCalledTimes(1);

expect(create.mock.calls[0][0].data.id).toEqual('node-3');
expect(update.mock.calls[0][0].data.id).toEqual('node-1');
expect(update.mock.calls[1][0].data.id).toEqual('edge-1');
expect(destroy.mock.calls[0][0].data.id).toEqual('node-2');

graph.destroy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('getPluginInstance', () => {
}

register('plugin', 'custom', CustomPlugin);
const graph = await createGraph({
const graph = createGraph({
plugins: [
{
key: 'custom-plugin',
Expand Down
2 changes: 1 addition & 1 deletion packages/g6/__tests__/utils/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function createDemoGraph(demo: TestCase, context?: Partial<TestCont
return demo({ animation: false, container, theme: 'light', ...context });
}

export async function createGraph(options: G6Spec) {
export function createGraph(options: G6Spec) {
const container = createGraphCanvas(document.getElementById('container'));
return new Graph({
container,
Expand Down
8 changes: 0 additions & 8 deletions packages/g6/src/constants/events/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ export enum GraphEvent {
BEFORE_TRANSFORM = 'beforetransform',
/** <zh/> 可视区域变化之后 | <en/> After the visible area changes */
AFTER_TRANSFORM = 'aftertransform',
/** <zh/> 元素可见性变化之前 | <en/> Before the visibility of the element changes */
BEFORE_ELEMENT_VISIBILITY_CHANGE = 'beforeelementvisibilitychange',
/** <zh/> 元素可见性变化之后 | <en/> After the visibility of the element changes */
AFTER_ELEMENT_VISIBILITY_CHANGE = 'afterelementvisibilitychange',
/** <zh/> 元素层级变化之前 | <en/> Before the layer of the element changes */
BEFORE_ELEMENT_Z_INDEX_CHANGE = 'beforeelementzindexchange',
/** <zh/> 元素层级变化之后 | <en/> After the layer of the element changes */
AFTER_ELEMENT_Z_INDEX_CHANGE = 'afterelementzindexchange',
/** <zh/> 状态变化之前 | <en/> Before the state changes */
BEFORE_ELEMENT_STATE_CHANGE = 'beforeelementstatechange',
/** <zh/> 状态变化之后 | <en/> After the state changes */
Expand Down
48 changes: 38 additions & 10 deletions packages/g6/src/runtime/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { reduceDataChanges } from '../utils/change';
import { getSubgraphRelatedEdges } from '../utils/edge';
import { updateStyle } from '../utils/element';
import type { BaseEvent } from '../utils/event';
import { AnimateEvent, GraphLifeCycleEvent } from '../utils/event';
import { AnimateEvent, ElementLifeCycleEvent, GraphLifeCycleEvent, emit } from '../utils/event';
import { idOf } from '../utils/id';
import { assignColorByPalette, parsePalette } from '../utils/palette';
import { computeElementCallbackStyle } from '../utils/style';
Expand Down Expand Up @@ -68,8 +68,7 @@ export class ElementController {
}

private emit(event: BaseEvent) {
const { graph } = this.context;
graph.emit(event.type, event);
emit(this.context.graph, event);
}

private forEachElementData(callback: (elementType: ElementType, elementData: ElementData) => void) {
Expand Down Expand Up @@ -545,6 +544,9 @@ export class ElementController {
// get shape constructor
const Ctor = getExtension(elementType, type);
if (!Ctor) return () => null;

this.emit(new ElementLifeCycleEvent(GraphEvent.BEFORE_ELEMENT_CREATE, elementType, datum));

const shape = this.container[elementType].appendChild(
new Ctor({
id,
Expand All @@ -558,7 +560,12 @@ export class ElementController {
this.shapeTypeMap[id] = type;
this.elementMap[id] = shape;

return () => animator?.(id, shape, { ...shape.attributes, opacity: 0 }) || null;
return () =>
withAnimationCallbacks(animator?.(id, shape, { ...shape.attributes, opacity: 0 }) || null, {
after: () => {
this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_CREATE, elementType, datum));
},
});
}

private getCreateTasks(data: ProcedureData, context: Omit<DrawContext, 'animator'>): AnimatableTask[] {
Expand Down Expand Up @@ -606,14 +613,20 @@ export class ElementController {
const id = idOf(datum);
const shape = this.getElement(id);
if (!shape) return () => null;

this.emit(new ElementLifeCycleEvent(GraphEvent.BEFORE_ELEMENT_UPDATE, elementType, datum));
const emitAfterUpdate = () =>
this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_UPDATE, elementType, datum));

const { type, ...style } = this.getElementComputedStyle(elementType, datum);

// 如果类型不同,需要先销毁原有元素,再创建新元素
// If the type is different, you need to destroy the original element first, and then create a new element
if (this.shapeTypeMap[id] !== type) {
return () => {
this.destroyElement(datum, { ...context, animation: false })();
this.destroyElement(elementType, datum, { ...context, animation: false })();
this.createElement(elementType, datum, { ...context, animation: false })();
emitAfterUpdate();
return null;
};
}
Expand All @@ -629,15 +642,22 @@ export class ElementController {
// show
if (style.visibility !== 'hidden') {
updateStyle(shape, { visibility: 'visible' });
return () => animator?.(id, shape, { ...shape.attributes, opacity: 0 }, { opacity: originalOpacity }) || null;
return () =>
withAnimationCallbacks(
animator?.(id, shape, { ...shape.attributes, opacity: 0 }, { opacity: originalOpacity }) || null,
{ after: emitAfterUpdate },
);
}
// hide
else if (style.visibility === 'hidden') {
return () =>
withAnimationCallbacks(
animator?.(id, shape, { ...shape.attributes, opacity: originalOpacity }, { opacity: 0 }) || null,
{
after: () => updateStyle(shape, { visibility: this.latestElementVisibilityMap.get(shape) }),
after: () => {
updateStyle(shape, { visibility: this.latestElementVisibilityMap.get(shape) });
emitAfterUpdate();
},
},
);
}
Expand All @@ -646,7 +666,10 @@ export class ElementController {
const originalStyle = { ...shape.attributes };
updateStyle(shape, style);

return () => animator?.(id, shape, originalStyle) || null;
return () =>
withAnimationCallbacks(animator?.(id, shape, originalStyle) || null, {
after: emitAfterUpdate,
});
}

private getUpdateTasks(data: ProcedureData, context: Omit<DrawContext, 'animator'>): AnimatableTask[] {
Expand All @@ -670,18 +693,21 @@ export class ElementController {
return tasks;
}

private destroyElement(datum: ElementDatum, context: DrawContext) {
private destroyElement(elementType: ElementType, datum: ElementDatum, context: DrawContext) {
const { animator } = context;
const id = idOf(datum);
const element = this.elementMap[id];
if (!element) return () => null;

this.emit(new ElementLifeCycleEvent(GraphEvent.BEFORE_ELEMENT_DESTROY, elementType, datum));

return () => {
const result = animator?.(id, element, { ...element.attributes }, { opacity: 0 }) || null;
withAnimationCallbacks(result, {
after: () => {
this.clearElement(id);
element.destroy();
this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_DESTROY, elementType, datum));
},
});
return result;
Expand All @@ -701,7 +727,9 @@ export class ElementController {
iteration.forEach(([elementType, elementData]) => {
if (elementData.size === 0) return [];
const animator = this.getAnimationExecutor(elementType, stage || 'exit', animation);
elementData.forEach((datum) => tasks.push(() => this.destroyElement(datum, { ...context, animator })));
elementData.forEach((datum) =>
tasks.push(() => this.destroyElement(elementType, datum, { ...context, animator })),
);
});

// TODO 重新计算色板样式,如果是分组色板,则不需要重新计算
Expand Down
6 changes: 3 additions & 3 deletions packages/g6/src/utils/event/events.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { IAnimation } from '@antv/g';
import type { ID } from '@antv/graphlib';
import type { AnimationType, GraphEvent } from '../../constants';
import type { GraphData } from '../../spec';
import type { State, TransformOptions } from '../../types';
import type { ElementDatum, ElementType, State, TransformOptions } from '../../types';

export class BaseEvent {
constructor(public type: string) {}
Expand Down Expand Up @@ -45,7 +44,8 @@ export class ElementLifeCycleEvent extends BaseEvent {
| GraphEvent.AFTER_ELEMENT_UPDATE
| GraphEvent.BEFORE_ELEMENT_DESTROY
| GraphEvent.AFTER_ELEMENT_DESTROY,
public data: GraphData,
public elementType: ElementType,
public data: ElementDatum,
) {
super(type);
}
Expand Down

0 comments on commit 61fa3d1

Please sign in to comment.