fix(data-source): 依赖缓存按节点对象引用判断失效

此前缓存键由节点 id 拼接而成,节点被替换(如修改绑定字段)后 id 不变仍命中旧缓存,
导致新依赖未被收集。改为以首节点为 WeakMap key 并比较节点对象引用,
节点替换后重新收集依赖,节点回收后缓存自动释放
This commit is contained in:
roymondchen 2026-10-30 16:48:30 +08:00
parent dfad691493
commit 6f2ffc3ca1
2 changed files with 96 additions and 18 deletions

View File

@ -1,4 +1,4 @@
import type { DataSourceSchema, Id, MNode } from '@tmagic/core';
import type { DataSourceSchema, MNode } from '@tmagic/core';
import {
DSL_NODE_KEY_COPY_PREFIX,
isDataSourceCondTarget,
@ -8,26 +8,43 @@ import {
Watcher,
} from '@tmagic/core';
const cache = new Map();
interface CacheEntry {
/** 计算依赖时的节点引用,用于判断节点是否被替换过 */
refs: MNode[];
result: any;
}
export const getDeps = (ds: DataSourceSchema, nodes: MNode[], inEditor: boolean) => {
let cacheKey: string;
/**
* 以第一个节点对象作为 WeakMap 的 key,节点被回收后缓存自动释放,
* 不同数据源 / 是否在编辑器中的结果存放在内层 Map。
* 通过比较节点对象引用(而不是 id)来判断缓存是否有效,节点被替换后(如修改了绑定的字段)会重新收集依赖
*/
const cache = new WeakMap<MNode, Map<string, CacheEntry>>();
if (inEditor) {
const ids: Id[] = [];
nodes.forEach((node) => {
traverseNode(node, (node) => {
ids.push(node.id);
});
});
cacheKey = `${ds.id}:${ids.join(':')}`;
} else {
cacheKey = `${ds.id}:${nodes.map((node) => node.id).join(':')}`;
const getRefs = (nodes: MNode[], inEditor: boolean) => {
if (!inEditor) {
return [...nodes];
}
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
const refs: MNode[] = [];
nodes.forEach((node) => {
traverseNode(node, (item) => {
refs.push(item);
});
});
return refs;
};
const isSameRefs = (a: MNode[], b: MNode[]) => a.length === b.length && a.every((item, index) => item === b[index]);
export const getDeps = (ds: DataSourceSchema, nodes: MNode[], inEditor: boolean) => {
const refs = getRefs(nodes, inEditor);
const cacheKey = `${ds.id}:${inEditor}`;
const rootNode = nodes[0];
const entry = rootNode ? cache.get(rootNode)?.get(cacheKey) : undefined;
if (entry && isSameRefs(entry.refs, refs)) {
return entry.result;
}
const watcher = new Watcher();
@ -60,7 +77,11 @@ export const getDeps = (ds: DataSourceSchema, nodes: MNode[], inEditor: boolean)
const result = { deps, condDeps };
cache.set(cacheKey, result);
if (rootNode) {
const entries = cache.get(rootNode) ?? new Map<string, CacheEntry>();
entries.set(cacheKey, { refs, result });
cache.set(rootNode, entries);
}
return result;
};

View File

@ -54,4 +54,61 @@ describe('getDeps', () => {
const r2 = getDeps(ds, nodes, false);
expect(r1).toBe(r2);
});
const ds: any = { id: 'ds_ref', fields: [{ name: 'list', type: 'array', fields: [{ name: 'name' }] }] };
test('节点对象不变(即使放进新数组)时命中缓存', () => {
const node: any = { id: 'n_hit', type: 'text', text: '${ds_ref.list.name}' };
const r1 = getDeps(ds, [node], false);
const r2 = getDeps(ds, [node], false);
expect(r1).toBe(r2);
expect(r1.deps.n_hit.keys).toEqual(['text']);
});
test.each([false, true])('同 id 的节点被替换且绑定的字段变了,重新收集依赖 (inEditor=%s)', (inEditor) => {
const before: any = { id: 'n_replaced', type: 'text', text: '${ds_ref.list.name}', title: 'plain' };
const after: any = { id: 'n_replaced', type: 'text', text: 'plain', title: '${ds_ref.list.name}' };
const r1 = getDeps(ds, [before], inEditor);
const r2 = getDeps(ds, [after], inEditor);
expect(r1.deps.n_replaced.keys).toEqual(['text']);
expect(r2.deps.n_replaced.keys).toEqual(['title']);
});
test('inEditor=true 时子节点被替换(父节点不变)也会重新收集', () => {
const parent: any = {
id: 'n_parent',
type: 'container',
items: [{ id: 'n_child', type: 'text', text: '${ds_ref.list.name}' }],
};
const r1 = getDeps(ds, [parent], true);
expect(r1.deps.n_child.keys).toEqual(['text']);
parent.items.splice(0, 1, { id: 'n_child', type: 'text', title: '${ds_ref.list.name}' });
const r2 = getDeps(ds, [parent], true);
expect(r2.deps.n_child.keys).toEqual(['title']);
});
test('不同数据源、不同 inEditor 的结果互不影响', () => {
const node: any = { id: 'n_multi', type: 'text', text: '${ds_ref.list.name} ${ds_other.list.name}' };
const other: any = { id: 'ds_other', fields: [{ name: 'list', type: 'array', fields: [{ name: 'name' }] }] };
const a = getDeps(ds, [node], false);
const b = getDeps(other, [node], false);
const c = getDeps(ds, [node], true);
expect(a).not.toBe(b);
expect(a).not.toBe(c);
expect(getDeps(ds, [node], false)).toBe(a);
expect(getDeps(other, [node], false)).toBe(b);
expect(getDeps(ds, [node], true)).toBe(c);
});
test('nodes 为空时不缓存也不报错', () => {
const r1 = getDeps(ds, [], false);
const r2 = getDeps(ds, [], false);
expect(r1.deps).toEqual({});
expect(r2.deps).toEqual({});
});
});