From 6f2ffc3ca1a5702270f1d262d4776983d24d4ba0 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Fri, 30 Oct 2026 16:48:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(data-source):=20=E4=BE=9D=E8=B5=96=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=8C=89=E8=8A=82=E7=82=B9=E5=AF=B9=E8=B1=A1=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E5=88=A4=E6=96=AD=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前缓存键由节点 id 拼接而成,节点被替换(如修改绑定字段)后 id 不变仍命中旧缓存, 导致新依赖未被收集。改为以首节点为 WeakMap key 并比较节点对象引用, 节点替换后重新收集依赖,节点回收后缓存自动释放 --- packages/data-source/src/depsCache.ts | 57 +++++++++++++------- packages/data-source/tests/depsCache.spec.ts | 57 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 18 deletions(-) diff --git a/packages/data-source/src/depsCache.ts b/packages/data-source/src/depsCache.ts index d11444a0..494fed35 100644 --- a/packages/data-source/src/depsCache.ts +++ b/packages/data-source/src/depsCache.ts @@ -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>(); - 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(); + entries.set(cacheKey, { refs, result }); + cache.set(rootNode, entries); + } return result; }; diff --git a/packages/data-source/tests/depsCache.spec.ts b/packages/data-source/tests/depsCache.spec.ts index 987866c2..60d3962c 100644 --- a/packages/data-source/tests/depsCache.spec.ts +++ b/packages/data-source/tests/depsCache.spec.ts @@ -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({}); + }); });