mirror of
https://github.com/ecomfe/vue-echarts.git
synced 2026-03-13 08:41:05 +08:00
refactor(graphic): simplify runtime internals
This commit is contained in:
@@ -11,43 +11,43 @@ export function mergeProps(
|
||||
keys: readonly string[],
|
||||
props: Record<string, unknown>,
|
||||
): void {
|
||||
keys.forEach((key) => {
|
||||
for (const key of keys) {
|
||||
if (props[key] !== undefined) {
|
||||
target[key] = props[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function buildStyle(
|
||||
props: Record<string, unknown>,
|
||||
extraKeys: readonly string[],
|
||||
): Record<string, unknown> | undefined {
|
||||
const style = { ...(props.style as Record<string, unknown> | undefined) };
|
||||
mergeProps(style, BASE_STYLE_KEYS, props);
|
||||
mergeProps(style, extraKeys, props);
|
||||
const out = { ...(props.style as Record<string, unknown> | undefined) };
|
||||
mergeProps(out, BASE_STYLE_KEYS, props);
|
||||
mergeProps(out, extraKeys, props);
|
||||
|
||||
if (props.styleTransition !== undefined) {
|
||||
style.transition = props.styleTransition;
|
||||
out.transition = props.styleTransition;
|
||||
}
|
||||
|
||||
return Object.keys(style).length > 0 ? style : undefined;
|
||||
return Object.keys(out).length ? out : undefined;
|
||||
}
|
||||
|
||||
export function buildShape(
|
||||
type: string,
|
||||
props: Record<string, unknown>,
|
||||
): Record<string, unknown> | undefined {
|
||||
const shape = { ...(props.shape as Record<string, unknown> | undefined) };
|
||||
const shapeKeys = SHAPE_KEYS_BY_TYPE[type];
|
||||
if (shapeKeys) {
|
||||
mergeProps(shape, shapeKeys, props);
|
||||
const out = { ...(props.shape as Record<string, unknown> | undefined) };
|
||||
const keys = SHAPE_KEYS_BY_TYPE[type];
|
||||
if (keys) {
|
||||
mergeProps(out, keys, props);
|
||||
}
|
||||
|
||||
if (props.shapeTransition !== undefined) {
|
||||
shape.transition = props.shapeTransition;
|
||||
out.transition = props.shapeTransition;
|
||||
}
|
||||
|
||||
return Object.keys(shape).length > 0 ? shape : undefined;
|
||||
return Object.keys(out).length ? out : undefined;
|
||||
}
|
||||
|
||||
export function buildInfo(node: GraphicNode): unknown {
|
||||
|
||||
@@ -16,45 +16,43 @@ type BuildResult = {
|
||||
snapshot: GraphicSnapshot;
|
||||
};
|
||||
|
||||
function buildElementOption(node: GraphicNode, children: Option[] | undefined): Option {
|
||||
const element: Record<string, unknown> = {
|
||||
function toElement(node: GraphicNode, children?: Option[]): Option {
|
||||
const out: Record<string, unknown> = {
|
||||
type: node.type,
|
||||
id: node.id,
|
||||
};
|
||||
|
||||
const common = pruneCommonPropsByType(node.type, pickCommonProps(node.props));
|
||||
Object.assign(element, common);
|
||||
Object.assign(out, pruneCommonPropsByType(node.type, pickCommonProps(node.props)));
|
||||
|
||||
if (isGroupGraphic(node.type)) {
|
||||
if (children) {
|
||||
element.children = children;
|
||||
if (children?.length) {
|
||||
out.children = children;
|
||||
}
|
||||
const info = buildInfo(node);
|
||||
if (info !== undefined) {
|
||||
element.info = info;
|
||||
out.info = info;
|
||||
}
|
||||
return element as Option;
|
||||
return out as Option;
|
||||
}
|
||||
|
||||
const shapeKeys = SHAPE_KEYS_BY_TYPE[node.type];
|
||||
if (shapeKeys) {
|
||||
if (SHAPE_KEYS_BY_TYPE[node.type]) {
|
||||
const shape = buildShape(node.type, node.props);
|
||||
if (shape) {
|
||||
element.shape = shape;
|
||||
out.shape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
const style = buildStyle(node.props, styleKeysByType(node.type));
|
||||
if (style) {
|
||||
element.style = style;
|
||||
out.style = style;
|
||||
}
|
||||
|
||||
const info = buildInfo(node);
|
||||
if (info !== undefined) {
|
||||
element.info = info;
|
||||
out.info = info;
|
||||
}
|
||||
|
||||
return element as Option;
|
||||
return out as Option;
|
||||
}
|
||||
|
||||
export function buildGraphicOption(nodes: Iterable<GraphicNode>, rootId: string): BuildResult {
|
||||
@@ -65,9 +63,12 @@ export function buildGraphicOption(nodes: Iterable<GraphicNode>, rootId: string)
|
||||
let hasDuplicateId = false;
|
||||
|
||||
for (const node of nodes) {
|
||||
const list = byParent.get(node.parentId) ?? [];
|
||||
list.push(node);
|
||||
byParent.set(node.parentId, list);
|
||||
const list = byParent.get(node.parentId);
|
||||
if (list) {
|
||||
list.push(node);
|
||||
} else {
|
||||
byParent.set(node.parentId, [node]);
|
||||
}
|
||||
|
||||
if (ids.has(node.id)) {
|
||||
hasDuplicateId = true;
|
||||
@@ -82,24 +83,24 @@ export function buildGraphicOption(nodes: Iterable<GraphicNode>, rootId: string)
|
||||
|
||||
const snapshot: GraphicSnapshot = { ids, parentById, hasDuplicateId };
|
||||
|
||||
const buildChildren = (parentId: string | null): Option[] => {
|
||||
const children = byParent.get(parentId) ?? [];
|
||||
return children.map((child) =>
|
||||
buildElementOption(child, child.type === "group" ? buildChildren(child.id) : undefined),
|
||||
const childrenOf = (parentId: string | null): Option[] => {
|
||||
const list = byParent.get(parentId) ?? [];
|
||||
return list.map((node) =>
|
||||
toElement(node, node.type === "group" ? childrenOf(node.id) : undefined),
|
||||
);
|
||||
};
|
||||
|
||||
const root: Record<string, unknown> = {
|
||||
type: "group",
|
||||
id: rootId,
|
||||
$action: "replace",
|
||||
children: buildChildren(null),
|
||||
};
|
||||
|
||||
return {
|
||||
option: {
|
||||
graphic: {
|
||||
elements: [root],
|
||||
elements: [
|
||||
{
|
||||
type: "group",
|
||||
id: rootId,
|
||||
$action: "replace",
|
||||
children: childrenOf(null),
|
||||
},
|
||||
],
|
||||
},
|
||||
} as Option,
|
||||
snapshot,
|
||||
|
||||
@@ -29,49 +29,39 @@ export type GraphicCollector = {
|
||||
optionRef: Ref<Option | null>;
|
||||
getNodes: () => Iterable<GraphicNode>;
|
||||
getSnapshot: () => GraphicSnapshot;
|
||||
setSnapshot: (snapshot: GraphicSnapshot) => void;
|
||||
requestFlush: () => void;
|
||||
getStructureVersion: () => number;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export function createStableSerializer() {
|
||||
type UnknownFn = (...args: unknown[]) => unknown;
|
||||
const functionIds = new WeakMap<UnknownFn, number>();
|
||||
const symbolIds = new Map<symbol, number>();
|
||||
const objectIds = new WeakMap<object, number>();
|
||||
let functionCursor = 0;
|
||||
let symbolCursor = 0;
|
||||
let objectCursor = 0;
|
||||
let symbolId = 0;
|
||||
let objectId = 0;
|
||||
|
||||
const ensureFunctionId = (fn: UnknownFn): number => {
|
||||
let id = functionIds.get(fn);
|
||||
const getSymbolId = (value: symbol): number => {
|
||||
let id = symbolIds.get(value);
|
||||
if (id == null) {
|
||||
functionCursor += 1;
|
||||
id = functionCursor;
|
||||
functionIds.set(fn, id);
|
||||
symbolId += 1;
|
||||
id = symbolId;
|
||||
symbolIds.set(value, id);
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const ensureSymbolId = (symbol: symbol): number => {
|
||||
let id = symbolIds.get(symbol);
|
||||
const getObjectId = (value: object): number => {
|
||||
let id = objectIds.get(value);
|
||||
if (id == null) {
|
||||
symbolCursor += 1;
|
||||
id = symbolCursor;
|
||||
symbolIds.set(symbol, id);
|
||||
objectId += 1;
|
||||
id = objectId;
|
||||
objectIds.set(value, id);
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const ensureObjectId = (object: object): number => {
|
||||
let id = objectIds.get(object);
|
||||
if (id == null) {
|
||||
objectCursor += 1;
|
||||
id = objectCursor;
|
||||
objectIds.set(object, id);
|
||||
}
|
||||
return id;
|
||||
const isPlainObject = (value: object): boolean => {
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
};
|
||||
|
||||
const stringify = (value: unknown): string => {
|
||||
@@ -82,37 +72,36 @@ export function createStableSerializer() {
|
||||
return "n";
|
||||
}
|
||||
|
||||
const valueType = typeof value;
|
||||
if (valueType === "string") {
|
||||
const t = typeof value;
|
||||
if (t === "string") {
|
||||
return `s:${JSON.stringify(value)}`;
|
||||
}
|
||||
if (valueType === "number") {
|
||||
if (t === "number") {
|
||||
return `d:${value}`;
|
||||
}
|
||||
if (valueType === "boolean") {
|
||||
if (t === "boolean") {
|
||||
return value ? "b:1" : "b:0";
|
||||
}
|
||||
if (valueType === "bigint") {
|
||||
if (t === "bigint") {
|
||||
return `g:${String(value)}`;
|
||||
}
|
||||
if (valueType === "symbol") {
|
||||
return `y:${ensureSymbolId(value as symbol)}`;
|
||||
if (t === "symbol") {
|
||||
return `y:${getSymbolId(value as symbol)}`;
|
||||
}
|
||||
if (valueType === "function") {
|
||||
return `f:${ensureFunctionId(value as UnknownFn)}`;
|
||||
if (t === "function") {
|
||||
return `o:${getObjectId(value as object)}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((item) => stringify(item)).join(",")}]`;
|
||||
}
|
||||
|
||||
const objectValue = value as object;
|
||||
const prototype = Object.getPrototypeOf(objectValue);
|
||||
if (prototype !== Object.prototype && prototype !== null) {
|
||||
return `o:${ensureObjectId(objectValue)}`;
|
||||
const obj = value as object;
|
||||
if (!isPlainObject(obj)) {
|
||||
return `o:${getObjectId(obj)}`;
|
||||
}
|
||||
|
||||
const record = objectValue as Record<string, unknown>;
|
||||
const record = obj as Record<string, unknown>;
|
||||
const keys = Object.keys(record).sort();
|
||||
return `{${keys.map((key) => `${key}:${stringify(record[key])}`).join(",")}}`;
|
||||
};
|
||||
@@ -120,7 +109,7 @@ export function createStableSerializer() {
|
||||
return stringify;
|
||||
}
|
||||
|
||||
function buildNodeFingerprint(
|
||||
function nodeSig(
|
||||
stringify: (value: unknown) => string,
|
||||
node: Omit<GraphicNode, "order"> & { order: number },
|
||||
): string {
|
||||
@@ -140,25 +129,18 @@ export function createGraphicCollector(options: {
|
||||
const nodes = new Map<string, GraphicNode>();
|
||||
const warnedKeys = new Set<string>();
|
||||
const optionRef = shallowRef<Option | null>(null);
|
||||
const fingerprintById = new Map<string, string>();
|
||||
const sigById = new Map<string, string>();
|
||||
const passById = new Map<string, number>();
|
||||
const stringify = createStableSerializer();
|
||||
|
||||
let order = 0;
|
||||
let currentPass = 0;
|
||||
let pass = 0;
|
||||
let pending = false;
|
||||
let disposed = false;
|
||||
let structureVersion = 0;
|
||||
|
||||
const snapshot: GraphicSnapshot = {
|
||||
ids: new Set(),
|
||||
parentById: new Map(),
|
||||
hasDuplicateId: false,
|
||||
};
|
||||
|
||||
function beginPass(): void {
|
||||
order = 0;
|
||||
currentPass += 1;
|
||||
pass += 1;
|
||||
}
|
||||
|
||||
function warnOnce(key: string, message: string): void {
|
||||
@@ -176,8 +158,7 @@ export function createGraphicCollector(options: {
|
||||
|
||||
const existing = nodes.get(node.id);
|
||||
const existingPass = passById.get(node.id);
|
||||
if (existing && existing.sourceId !== node.sourceId && existingPass === currentPass) {
|
||||
snapshot.hasDuplicateId = true;
|
||||
if (existing && existing.sourceId !== node.sourceId && existingPass === pass) {
|
||||
warnOnce(`duplicate-id:${node.id}`, warnDuplicateId(node.id));
|
||||
}
|
||||
|
||||
@@ -186,14 +167,14 @@ export function createGraphicCollector(options: {
|
||||
order = node.order + 1;
|
||||
}
|
||||
|
||||
const fingerprint = buildNodeFingerprint(stringify, { ...node, order: nextOrder });
|
||||
const sig = nodeSig(stringify, { ...node, order: nextOrder });
|
||||
if (
|
||||
existing &&
|
||||
existing.sourceId === node.sourceId &&
|
||||
existing.order === nextOrder &&
|
||||
fingerprintById.get(node.id) === fingerprint
|
||||
sigById.get(node.id) === sig
|
||||
) {
|
||||
passById.set(node.id, currentPass);
|
||||
passById.set(node.id, pass);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,9 +183,8 @@ export function createGraphicCollector(options: {
|
||||
...node,
|
||||
order: nextOrder,
|
||||
});
|
||||
fingerprintById.set(node.id, fingerprint);
|
||||
passById.set(node.id, currentPass);
|
||||
structureVersion += 1;
|
||||
sigById.set(node.id, sig);
|
||||
passById.set(node.id, pass);
|
||||
requestFlush();
|
||||
}
|
||||
|
||||
@@ -222,8 +202,7 @@ export function createGraphicCollector(options: {
|
||||
}
|
||||
nodes.delete(id);
|
||||
passById.delete(id);
|
||||
fingerprintById.delete(id);
|
||||
structureVersion += 1;
|
||||
sigById.delete(id);
|
||||
requestFlush();
|
||||
}
|
||||
|
||||
@@ -257,22 +236,12 @@ export function createGraphicCollector(options: {
|
||||
return nodes.values();
|
||||
}
|
||||
|
||||
function setSnapshot(next: GraphicSnapshot): void {
|
||||
snapshot.ids = next.ids;
|
||||
snapshot.parentById = next.parentById;
|
||||
snapshot.hasDuplicateId = next.hasDuplicateId;
|
||||
}
|
||||
|
||||
function getStructureVersion(): number {
|
||||
return structureVersion;
|
||||
}
|
||||
|
||||
function dispose(): void {
|
||||
disposed = true;
|
||||
pending = false;
|
||||
nodes.clear();
|
||||
passById.clear();
|
||||
fingerprintById.clear();
|
||||
sigById.clear();
|
||||
warnedKeys.clear();
|
||||
optionRef.value = null;
|
||||
}
|
||||
@@ -285,9 +254,7 @@ export function createGraphicCollector(options: {
|
||||
optionRef,
|
||||
getNodes,
|
||||
getSnapshot,
|
||||
setSnapshot,
|
||||
requestFlush,
|
||||
getStructureVersion,
|
||||
dispose,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,35 +23,23 @@ const graphicProps = {
|
||||
...graphicShapeProps,
|
||||
} as const;
|
||||
|
||||
function resolveId(
|
||||
function parseIdentity(
|
||||
props: { id?: string | number },
|
||||
instance: NonNullable<ReturnType<typeof getCurrentInstance>>,
|
||||
): string {
|
||||
instance: { uid: number; vnode: { key: unknown } },
|
||||
): { id: string; key: string | null; missing: boolean } {
|
||||
if (props.id != null) {
|
||||
return String(props.id);
|
||||
const id = String(props.id);
|
||||
return { id, key: `id:${id}`, missing: false };
|
||||
}
|
||||
const key = instance?.vnode.key;
|
||||
if (key != null) {
|
||||
return String(key);
|
||||
const vnodeKey = instance.vnode.key;
|
||||
if (vnodeKey != null) {
|
||||
const id = String(vnodeKey);
|
||||
return { id, key: `key:${id}`, missing: false };
|
||||
}
|
||||
return `__ve_graphic_${instance.uid}`;
|
||||
return { id: `__ve_graphic_${instance.uid}`, key: null, missing: true };
|
||||
}
|
||||
|
||||
function resolveIdentity(
|
||||
props: { id?: string | number },
|
||||
instance: NonNullable<ReturnType<typeof getCurrentInstance>>,
|
||||
): string | null {
|
||||
if (props.id != null) {
|
||||
return `id:${String(props.id)}`;
|
||||
}
|
||||
const key = instance.vnode.key;
|
||||
if (key != null) {
|
||||
return `key:${String(key)}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cloneProps(props: AnyProps): AnyProps {
|
||||
function copyProps(props: AnyProps): AnyProps {
|
||||
const raw = toRaw(props) as AnyProps;
|
||||
const clone: AnyProps = { ...raw };
|
||||
if (raw.shape && typeof raw.shape === "object") {
|
||||
@@ -64,13 +52,13 @@ function cloneProps(props: AnyProps): AnyProps {
|
||||
}
|
||||
|
||||
function extractHandlers(attrs: AnyProps): AnyProps {
|
||||
const handlers: AnyProps = {};
|
||||
const out: AnyProps = {};
|
||||
for (const key of Object.keys(attrs)) {
|
||||
if (key.startsWith("on")) {
|
||||
handlers[key] = attrs[key];
|
||||
out[key] = attrs[key];
|
||||
}
|
||||
}
|
||||
return handlers;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function createGraphicComponent(name: string, type: string) {
|
||||
@@ -93,23 +81,22 @@ export function createGraphicComponent(name: string, type: string) {
|
||||
const currentId = shallowRef<string | null>(null);
|
||||
|
||||
function register(): void {
|
||||
const nextId = resolveId(props, instance);
|
||||
if (!props.id && instance.vnode.key == null) {
|
||||
const next = parseIdentity(props, instance);
|
||||
if (next.missing) {
|
||||
graphicCollector.warnOnce(`missing-id:${instance.uid}`, warnMissingIdentity(name));
|
||||
}
|
||||
if (currentId.value && currentId.value !== nextId) {
|
||||
if (currentId.value && currentId.value !== next.id) {
|
||||
graphicCollector.unregister(currentId.value, instance.uid);
|
||||
}
|
||||
currentId.value = nextId;
|
||||
const identity = resolveIdentity(props, instance);
|
||||
const hintedOrder = identity != null ? unref(orderRef)?.get(identity) : undefined;
|
||||
currentId.value = next.id;
|
||||
const hintedOrder = next.key ? unref(orderRef)?.get(next.key) : undefined;
|
||||
|
||||
graphicCollector.register({
|
||||
id: nextId,
|
||||
id: next.id,
|
||||
type,
|
||||
parentId: parentIdRef ? unref(parentIdRef) : null,
|
||||
order: hintedOrder,
|
||||
props: cloneProps(props as AnyProps),
|
||||
props: copyProps(props as AnyProps),
|
||||
handlers: extractHandlers(attrs as AnyProps),
|
||||
sourceId: instance.uid,
|
||||
});
|
||||
|
||||
@@ -16,13 +16,12 @@ type NormalizedHandlers = Record<string, Array<(...args: unknown[]) => void>>;
|
||||
export function registerGraphicExtension(): void {
|
||||
registerVChartExtension(
|
||||
(ctx: VChartExtensionContext) => {
|
||||
const handlersById = new Map<string, NormalizedHandlers>();
|
||||
const boundEvents = new Map<string, (params: unknown) => void>();
|
||||
let boundChart: EChartsType | null = null;
|
||||
let warnedOptionGraphicOverride = false;
|
||||
let lastHandledStructureVersion = -1;
|
||||
const handlers = new Map<string, NormalizedHandlers>();
|
||||
const eventFns = new Map<string, (params: unknown) => void>();
|
||||
let chart: EChartsType | null = null;
|
||||
let warnedOverride = false;
|
||||
|
||||
const normalizeEvent = (key: string): string | null => {
|
||||
const toEventName = (key: string): string | null => {
|
||||
if (!key.startsWith("on") || key.length <= 2) {
|
||||
return null;
|
||||
}
|
||||
@@ -30,73 +29,71 @@ export function registerGraphicExtension(): void {
|
||||
return raw.charAt(0).toLowerCase() + raw.slice(1);
|
||||
};
|
||||
|
||||
const normalizeHandlers = (
|
||||
rawHandlers: Record<string, unknown>,
|
||||
): Record<string, Array<(...args: unknown[]) => void>> => {
|
||||
const result: Record<string, Array<(...args: unknown[]) => void>> = {};
|
||||
for (const [key, value] of Object.entries(rawHandlers)) {
|
||||
const event = normalizeEvent(key);
|
||||
const toHandlers = (input: Record<string, unknown>): NormalizedHandlers => {
|
||||
const out: NormalizedHandlers = {};
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
const event = toEventName(key);
|
||||
if (!event) {
|
||||
continue;
|
||||
}
|
||||
const handlers: Array<(...args: unknown[]) => void> = Array.isArray(value)
|
||||
const list: Array<(...args: unknown[]) => void> = Array.isArray(value)
|
||||
? (value as unknown[]).filter(
|
||||
(item): item is (...args: unknown[]) => void => typeof item === "function",
|
||||
)
|
||||
: typeof value === "function"
|
||||
? [value as (...args: unknown[]) => void]
|
||||
: [];
|
||||
if (handlers.length > 0) {
|
||||
result[event] = handlers;
|
||||
if (list.length > 0) {
|
||||
out[event] = list;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return out;
|
||||
};
|
||||
|
||||
const dispatchEvent = (event: string, params: any) => {
|
||||
const emit = (event: string, params: any) => {
|
||||
const id = params?.info?.__veGraphicId;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const handlers = handlersById.get(String(id))?.[event];
|
||||
if (!handlers) {
|
||||
const list = handlers.get(String(id))?.[event];
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
handlers.forEach((handler) => handler(params));
|
||||
list.forEach((fn) => fn(params));
|
||||
};
|
||||
|
||||
const syncEventBindings = (chart: EChartsType, activeEvents: Set<string>) => {
|
||||
boundEvents.forEach((handler, event) => {
|
||||
if (!activeEvents.has(event)) {
|
||||
chart.off(event, handler as any);
|
||||
boundEvents.delete(event);
|
||||
const syncEvents = (chartInst: EChartsType, active: Set<string>) => {
|
||||
eventFns.forEach((fn, event) => {
|
||||
if (!active.has(event)) {
|
||||
chartInst.off(event, fn as any);
|
||||
eventFns.delete(event);
|
||||
}
|
||||
});
|
||||
|
||||
activeEvents.forEach((event) => {
|
||||
if (boundEvents.has(event)) {
|
||||
active.forEach((event) => {
|
||||
if (eventFns.has(event)) {
|
||||
return;
|
||||
}
|
||||
const handler = (params: unknown) => dispatchEvent(event, params);
|
||||
chart.on(event, handler as any);
|
||||
boundEvents.set(event, handler);
|
||||
const fn = (params: unknown) => emit(event, params);
|
||||
chartInst.on(event, fn as any);
|
||||
eventFns.set(event, fn);
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => ctx.chart.value,
|
||||
(chart, prev) => {
|
||||
if (prev && boundEvents.size > 0) {
|
||||
boundEvents.forEach((handler, event) => prev.off(event, handler as any));
|
||||
boundEvents.clear();
|
||||
(next, prev) => {
|
||||
if (prev && eventFns.size > 0) {
|
||||
eventFns.forEach((fn, event) => prev.off(event, fn as any));
|
||||
eventFns.clear();
|
||||
}
|
||||
boundChart = chart ?? null;
|
||||
if (boundChart) {
|
||||
const activeEvents = new Set<string>();
|
||||
handlersById.forEach((handlers) => {
|
||||
Object.keys(handlers).forEach((event) => activeEvents.add(event));
|
||||
chart = next ?? null;
|
||||
if (chart) {
|
||||
const active = new Set<string>();
|
||||
handlers.forEach((entry) => {
|
||||
Object.keys(entry).forEach((event) => active.add(event));
|
||||
});
|
||||
syncEventBindings(boundChart, activeEvents);
|
||||
syncEvents(chart, active);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -105,37 +102,30 @@ export function registerGraphicExtension(): void {
|
||||
const collector = createGraphicCollector({
|
||||
warn: ctx.warn,
|
||||
onFlush: () => {
|
||||
const structureVersion = collector.getStructureVersion();
|
||||
if (structureVersion === lastHandledStructureVersion) {
|
||||
return;
|
||||
}
|
||||
lastHandledStructureVersion = structureVersion;
|
||||
|
||||
const nodes = Array.from(collector.getNodes());
|
||||
const nextHandlersById = new Map<string, NormalizedHandlers>();
|
||||
const activeEvents = new Set<string>();
|
||||
const next = new Map<string, NormalizedHandlers>();
|
||||
const active = new Set<string>();
|
||||
|
||||
for (const node of nodes) {
|
||||
const handlers = normalizeHandlers(node.handlers);
|
||||
if (Object.keys(handlers).length > 0) {
|
||||
nextHandlersById.set(node.id, handlers);
|
||||
Object.keys(handlers).forEach((event) => activeEvents.add(event));
|
||||
const nodeHandlers = toHandlers(node.handlers);
|
||||
if (Object.keys(nodeHandlers).length > 0) {
|
||||
next.set(node.id, nodeHandlers);
|
||||
Object.keys(nodeHandlers).forEach((event) => active.add(event));
|
||||
}
|
||||
}
|
||||
|
||||
handlersById.clear();
|
||||
nextHandlersById.forEach((handlers, id) => {
|
||||
handlersById.set(id, handlers);
|
||||
handlers.clear();
|
||||
next.forEach((entry, id) => {
|
||||
handlers.set(id, entry);
|
||||
});
|
||||
|
||||
if (boundChart) {
|
||||
syncEventBindings(boundChart, activeEvents);
|
||||
if (chart) {
|
||||
syncEvents(chart, active);
|
||||
}
|
||||
|
||||
const { option, snapshot } = buildGraphicOption(nodes, ROOT_ID);
|
||||
const { option } = buildGraphicOption(nodes, ROOT_ID);
|
||||
|
||||
collector.optionRef.value = option;
|
||||
collector.setSnapshot(snapshot);
|
||||
|
||||
const updated = ctx.requestUpdate({
|
||||
updateOptions: {
|
||||
@@ -151,12 +141,12 @@ export function registerGraphicExtension(): void {
|
||||
|
||||
onScopeDispose(() => {
|
||||
collector.dispose();
|
||||
if (boundChart && boundEvents.size > 0) {
|
||||
boundEvents.forEach((handler, event) => boundChart?.off(event, handler as any));
|
||||
if (chart && eventFns.size > 0) {
|
||||
eventFns.forEach((fn, event) => chart?.off(event, fn as any));
|
||||
}
|
||||
boundEvents.clear();
|
||||
handlersById.clear();
|
||||
boundChart = null;
|
||||
eventFns.clear();
|
||||
handlers.clear();
|
||||
chart = null;
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -164,17 +154,13 @@ export function registerGraphicExtension(): void {
|
||||
if (!ctx.slots.graphic) {
|
||||
return option;
|
||||
}
|
||||
if (option.graphic && !warnedOptionGraphicOverride) {
|
||||
if (option.graphic && !warnedOverride) {
|
||||
ctx.warn(warnOptionGraphicOverride());
|
||||
warnedOptionGraphicOverride = true;
|
||||
warnedOverride = true;
|
||||
}
|
||||
if (!collector.optionRef.value) {
|
||||
const { option: initialOption, snapshot } = buildGraphicOption(
|
||||
collector.getNodes(),
|
||||
ROOT_ID,
|
||||
);
|
||||
const { option: initialOption } = buildGraphicOption(collector.getNodes(), ROOT_ID);
|
||||
collector.optionRef.value = initialOption;
|
||||
collector.setSnapshot(snapshot);
|
||||
}
|
||||
const graphicOption = collector.optionRef.value!;
|
||||
return {
|
||||
|
||||
@@ -17,17 +17,13 @@ function getGraphicIdentity(vnode: VNode): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isGraphicComponent(vnode: VNode): boolean {
|
||||
function getGraphicType(vnode: VNode): string | null {
|
||||
const type = vnode.type as Record<string, unknown> | string | symbol;
|
||||
return Boolean(type && typeof type === "object" && GRAPHIC_COMPONENT_MARKER in type);
|
||||
}
|
||||
|
||||
function isGraphicGroup(vnode: VNode): boolean {
|
||||
const type = vnode.type as Record<string, unknown> | string | symbol;
|
||||
return (
|
||||
Boolean(type && typeof type === "object") &&
|
||||
(type as Record<string | symbol, unknown>)[GRAPHIC_COMPONENT_MARKER] === "group"
|
||||
);
|
||||
if (!type || typeof type !== "object") {
|
||||
return null;
|
||||
}
|
||||
const mark = (type as Record<string | symbol, unknown>)[GRAPHIC_COMPONENT_MARKER];
|
||||
return typeof mark === "string" ? mark : null;
|
||||
}
|
||||
|
||||
function collectGraphicOrder(
|
||||
@@ -41,7 +37,8 @@ function collectGraphicOrder(
|
||||
}
|
||||
|
||||
const vnode = value as VNode;
|
||||
if (isGraphicComponent(vnode)) {
|
||||
const graphicType = getGraphicType(vnode);
|
||||
if (graphicType) {
|
||||
const identity = getGraphicIdentity(vnode);
|
||||
if (identity) {
|
||||
orderMap.set(identity, cursor.value);
|
||||
@@ -50,7 +47,12 @@ function collectGraphicOrder(
|
||||
}
|
||||
|
||||
const children = vnode.children;
|
||||
if (isGraphicGroup(vnode) && children && typeof children === "object" && "default" in children) {
|
||||
if (
|
||||
graphicType === "group" &&
|
||||
children &&
|
||||
typeof children === "object" &&
|
||||
"default" in children
|
||||
) {
|
||||
const slot = (children as { default?: () => unknown }).default;
|
||||
if (slot) {
|
||||
collectGraphicOrder(slot(), orderMap, cursor);
|
||||
|
||||
@@ -16,8 +16,6 @@ type CollectorMock = {
|
||||
optionRef: { value: unknown };
|
||||
getNodes: () => Iterable<unknown>;
|
||||
getSnapshot: () => unknown;
|
||||
setSnapshot: ReturnType<typeof vi.fn>;
|
||||
getStructureVersion: () => number;
|
||||
};
|
||||
|
||||
function createCollectorMock(): CollectorMock {
|
||||
@@ -31,8 +29,6 @@ function createCollectorMock(): CollectorMock {
|
||||
optionRef: { value: null },
|
||||
getNodes: () => [],
|
||||
getSnapshot: () => ({}),
|
||||
setSnapshot: vi.fn(),
|
||||
getStructureVersion: () => 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ describe("graphic extension", () => {
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it("keeps event bindings stable when handlers are unchanged and skips no-op flush", async () => {
|
||||
it("keeps event bindings stable when handlers are unchanged", async () => {
|
||||
registerGraphicExtension();
|
||||
|
||||
const requestUpdate = vi.fn(() => true);
|
||||
@@ -317,7 +317,6 @@ describe("graphic extension", () => {
|
||||
const vnode = extensions.render()[0] as any;
|
||||
const collector = vnode.props.collector as {
|
||||
register: (node: any) => void;
|
||||
requestFlush: () => void;
|
||||
};
|
||||
const onClick = vi.fn();
|
||||
|
||||
@@ -348,9 +347,6 @@ describe("graphic extension", () => {
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
collector.requestFlush();
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(requestUpdate).toHaveBeenCalledTimes(2);
|
||||
expect(chart.on).toHaveBeenCalledTimes(1);
|
||||
expect(chart.off).not.toHaveBeenCalled();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createSSRApp, h } from "vue";
|
||||
import { renderToString } from "@vue/server-renderer";
|
||||
|
||||
import { GraphicMount } from "../src/graphic/mount";
|
||||
import { GRAPHIC_COMPONENT_MARKER } from "../src/graphic/marker";
|
||||
|
||||
describe("GraphicMount (node)", () => {
|
||||
it("returns null without browser root but still drives collector pass", async () => {
|
||||
@@ -42,7 +43,14 @@ describe("GraphicMount (node)", () => {
|
||||
GraphicMount as any,
|
||||
{ collector },
|
||||
{
|
||||
default: () => [42 as any, "text" as any],
|
||||
default: () => [
|
||||
42 as any,
|
||||
"text" as any,
|
||||
h({
|
||||
[GRAPHIC_COMPONENT_MARKER]: 1,
|
||||
render: () => null,
|
||||
} as any),
|
||||
],
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
@@ -383,8 +383,6 @@ describe("graphic", () => {
|
||||
|
||||
expect(onFlush).toHaveBeenCalledTimes(0);
|
||||
|
||||
const versionAfterDispose = collector.getStructureVersion();
|
||||
|
||||
collector.register({
|
||||
id: "after-dispose",
|
||||
type: "rect",
|
||||
@@ -397,7 +395,6 @@ describe("graphic", () => {
|
||||
collector.requestFlush();
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(collector.getStructureVersion()).toBe(versionAfterDispose);
|
||||
expect(Array.from(collector.getNodes())).toEqual([]);
|
||||
expect(collector.optionRef.value).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user