From 025cca365c87d755abfc047786ac9a75758019f5 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Fri, 29 May 2026 17:55:13 +0800 Subject: [PATCH] =?UTF-8?q?perf(dep):=20=E4=BE=9D=E8=B5=96=E6=94=B6?= =?UTF-8?q?=E9=9B=86=E6=94=B9=E4=B8=BA=E5=8D=95=E6=AC=A1=E9=81=8D=E5=8E=86?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=A4=84=E7=90=86=E5=A4=9A=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 collectItems/removeTargetsDep 改为整棵树只遍历一次、在每个属性上检查所有 target,把结构遍历开销从 ×targets 降到 ×1,收集结果保持一致。 同时修正 dataSourceMethodDeps 字段命名并补充到 MApp schema。 Co-authored-by: Cursor --- packages/data-source/src/utils.ts | 4 +- packages/data-source/tests/utils.spec.ts | 2 +- packages/dep/src/Watcher.ts | 199 +++++++++++++++++------ packages/dep/src/types.ts | 2 +- packages/editor/src/services/dep.ts | 41 ++++- packages/editor/src/utils/dep/worker.ts | 8 +- packages/schema/src/index.ts | 1 + 7 files changed, 191 insertions(+), 66 deletions(-) diff --git a/packages/data-source/src/utils.ts b/packages/data-source/src/utils.ts index 2bb6fbe2..4fa3a303 100644 --- a/packages/data-source/src/utils.ts +++ b/packages/data-source/src/utils.ts @@ -241,7 +241,7 @@ export const registerDataSourceOnDemand = async ( dsl: MApp, dataSourceModules: Record Promise>, ) => { - const { dataSourceMethodsDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl; + const { dataSourceMethodDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl; const dsModuleMap: Record Promise> = {}; @@ -253,7 +253,7 @@ export const registerDataSourceOnDemand = async ( } if (!Object.keys(dep).length) { - dep = dataSourceMethodsDeps[ds.id] || {}; + dep = dataSourceMethodDeps[ds.id] || {}; } if (Object.keys(dep).length && dataSourceModules[ds.type]) { diff --git a/packages/data-source/tests/utils.spec.ts b/packages/data-source/tests/utils.spec.ts index 940ab27d..07961474 100644 --- a/packages/data-source/tests/utils.spec.ts +++ b/packages/data-source/tests/utils.spec.ts @@ -377,7 +377,7 @@ describe('registerDataSourceOnDemand', () => { ], dataSourceDeps: { a: { node1: { name: 'n', keys: ['x'] } } }, dataSourceCondDeps: { c: { node2: { name: 'n', keys: ['y'] } } }, - dataSourceMethodsDeps: {}, + dataSourceMethodDeps: {}, }; const httpModule = { default: class HttpDS {} }; const mockModule = { default: class MockDS {} }; diff --git a/packages/dep/src/Watcher.ts b/packages/dep/src/Watcher.ts index 0b483d84..4bb3de32 100644 --- a/packages/dep/src/Watcher.ts +++ b/packages/dep/src/Watcher.ts @@ -127,10 +127,38 @@ export default class Watcher { deep = false, type?: DepTargetType | string, ) { - this.collectByCallback(nodes, type, ({ node, target }) => { - this.removeTargetDep(target, node); - this.collectItem(node, target, depExtendedData, deep); - }); + const targets = this.getCollectableTargets(type); + + if (!targets.length) { + return; + } + + // 整棵树只遍历一次、在每个属性上检查所有 target,把结构遍历开销从 ×targets 降到 ×1(详见 collectItems) + for (const node of nodes) { + this.removeTargetsDep(targets, node); + this.collectItems(node, targets, depExtendedData, deep); + } + } + + /** + * 获取本次需要参与收集的 target(过滤规则与 collectByCallback 一致) + * + * 注:供 editor 的 dep service / worker 跨包批量收集时复用,因此为 public。 + * @param type 强制收集指定类型的依赖 + */ + public getCollectableTargets(type?: DepTargetType | string): Target[] { + const targets: Target[] = []; + traverseTarget( + this.targetsList, + (target) => { + if (!type && !target.isCollectByDefault) { + return; + } + targets.push(target); + }, + type, + ); + return targets; } public collectByCallback( @@ -195,53 +223,11 @@ export default class Watcher { this.clear(nodes, type); } + /** + * 收集单个 target 的依赖,等价于 collectItems(node, [target], ...) + */ public collectItem(node: TargetNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) { - if (node[NODE_DISABLE_DATA_SOURCE_KEY] && DATA_SOURCE_TARGET_TYPES.has(target.type)) { - return; - } - - if (node[NODE_DISABLE_CODE_BLOCK_KEY] && target.type === DepTargetType.CODE_BLOCK) { - return; - } - - const collectTarget = (config: Record, prop = '') => { - const doCollect = (key: string, value: any) => { - const keyIsItems = key === this.childrenProp; - const fullKey = prop ? `${prop}.${key}` : key; - - if (target.isTarget(fullKey, value)) { - target.updateDep({ - id: node[this.idProp], - name: `${node[this.nameProp] || node[this.idProp]}`, - data: depExtendedData, - key: fullKey, - }); - } else if (!keyIsItems && Array.isArray(value)) { - for (let i = 0, l = value.length; i < l; i++) { - const item = value[i]; - if (isObject(item)) { - collectTarget(item, `${fullKey}[${i}]`); - } - } - } else if (isObject(value)) { - collectTarget(value, fullKey); - } - - if (keyIsItems && deep && Array.isArray(value)) { - for (const child of value) { - this.collectItem(child, target, depExtendedData, deep); - } - } - }; - - for (const [key, value] of Object.entries(config)) { - if (typeof value === 'undefined' || value === '') continue; - - doCollect(key, value); - } - }; - - collectTarget(node); + this.collectItems(node, [target], depExtendedData, deep); } public removeTargetDep(target: Target, node: TargetNode, key?: string | number) { @@ -252,4 +238,117 @@ export default class Watcher { } } } + + /** + * 与 removeTargetDep 等价,但一次子树递归同时处理多个 target, + * 把删除阶段的结构遍历从 ×targets 降到 ×1。 + * + * 注:供 editor 的 dep service 跨包批量删除时复用,因此为 public。 + */ + public removeTargetsDep(targets: Target[], node: TargetNode, key?: string | number) { + const id = node[this.idProp]; + for (const target of targets) { + target.removeDep(id, key); + } + if (typeof key === 'undefined' && Array.isArray(node[this.childrenProp]) && node[this.childrenProp].length) { + for (const item of node[this.childrenProp] as TargetNode[]) { + this.removeTargetsDep(targets, item, key); + } + } + } + + /** + * 与 collectItem 等价,但一次遍历同时处理多个 target(不含删除阶段)。 + * + * 关键优化:原实现对每个 target 都完整遍历一遍节点树(O(targets × 树规模)),大页面 + 大量数据源时, + * 结构遍历(Object.entries / 递归 / fullKey 字符串拼接)会被重复 targets 次。这里改为「整棵树只遍历一次, + * 在每个属性上检查所有 target」,把结构遍历开销从 ×targets 降到 ×1,isTarget 调用次数不变,收集结果完全一致。 + * + * 注:供 editor 的 dep service / worker 跨包批量收集时复用,因此为 public。 + */ + public collectItems(node: TargetNode, targets: Target[], depExtendedData: DepExtendedData = {}, deep = false) { + // 对应 collectItem 开头的 NODE_DISABLE_* 判断:被禁用的 target 在该节点及其子树都不收集 + const activeTargets = this.filterTargetsByNode(node, targets); + + if (!activeTargets.length) { + return; + } + + this.collectTargetForTargets(node, node, '', activeTargets, depExtendedData, deep); + } + + private filterTargetsByNode(node: TargetNode, targets: Target[]): Target[] { + const disableDataSource = Boolean(node[NODE_DISABLE_DATA_SOURCE_KEY]); + const disableCodeBlock = Boolean(node[NODE_DISABLE_CODE_BLOCK_KEY]); + + if (!disableDataSource && !disableCodeBlock) { + return targets; + } + + return targets.filter((target) => { + if (disableDataSource && DATA_SOURCE_TARGET_TYPES.has(target.type)) { + return false; + } + if (disableCodeBlock && target.type === DepTargetType.CODE_BLOCK) { + return false; + } + return true; + }); + } + + private collectTargetForTargets( + node: TargetNode, + config: Record, + prop: string, + targets: Target[], + depExtendedData: DepExtendedData, + deep: boolean, + ) { + const id = node[this.idProp]; + const name = `${node[this.nameProp] || node[this.idProp]}`; + + for (const [key, value] of Object.entries(config)) { + if (typeof value === 'undefined' || value === '') continue; + + const keyIsItems = key === this.childrenProp; + const fullKey = prop ? `${prop}.${key}` : key; + + // 在该属性上检查所有 target:命中的更新依赖;未命中的留待递归到更深层 + let notMatched: Target[] | null = null; + for (let i = 0, l = targets.length; i < l; i++) { + const target = targets[i]; + if (target.isTarget(fullKey, value, config)) { + target.updateDep({ + id, + name, + data: depExtendedData, + key: fullKey, + }); + } else { + (notMatched || (notMatched = [])).push(target); + } + } + + // 对应原 doCollect 的 else-if 分支:仅未命中的 target 才继续往 value 内部递归 + if (notMatched) { + if (!keyIsItems && Array.isArray(value)) { + for (let i = 0, l = value.length; i < l; i++) { + const item = value[i]; + if (isObject(item)) { + this.collectTargetForTargets(node, item, `${fullKey}[${i}]`, notMatched, depExtendedData, deep); + } + } + } else if (isObject(value)) { + this.collectTargetForTargets(node, value, fullKey, notMatched, depExtendedData, deep); + } + } + + // 对应原 doCollect 末尾的无条件子节点递归 + if (keyIsItems && deep && Array.isArray(value)) { + for (const child of value) { + this.collectItems(child, targets, depExtendedData, deep); + } + } + } + } } diff --git a/packages/dep/src/types.ts b/packages/dep/src/types.ts index 79c8da2a..f42b3acc 100644 --- a/packages/dep/src/types.ts +++ b/packages/dep/src/types.ts @@ -15,7 +15,7 @@ export enum DepTargetType { DATA_SOURCE_COND = 'data-source-cond', } -export type IsTarget = (key: string | number, value: any) => boolean; +export type IsTarget = (key: string | number, value: any, data?: Record) => boolean; export interface TargetOptions { isTarget: IsTarget; diff --git a/packages/editor/src/services/dep.ts b/packages/editor/src/services/dep.ts index df0d4214..1cb81d27 100644 --- a/packages/editor/src/services/dep.ts +++ b/packages/editor/src/services/dep.ts @@ -109,9 +109,23 @@ class Dep extends BaseService { public collect(nodes: MNode[], depExtendedData: DepExtendedData = {}, deep = false, type?: DepTargetType) { this.set('collecting', true); - this.watcher.collectByCallback(nodes, type, ({ node, target }) => { - this.collectNode(node, target, depExtendedData, deep); - }); + + const targets = this.watcher.getCollectableTargets(type); + + if (targets.length) { + for (const node of nodes) { + // 先删除原有依赖,再批量收集 + if (isPage(node)) { + for (const target of targets) { + this.removePageDep(target, depExtendedData); + } + } else { + this.watcher.removeTargetsDep(targets, node); + } + this.watcher.collectItems(node, targets, depExtendedData, deep); + } + } + this.set('collecting', false); this.emit('collected', nodes, deep); @@ -176,7 +190,7 @@ class Dep extends BaseService { dsl.dataSourceDeps[target.id] = target.deps; } else if (target.type === DepTargetType.DATA_SOURCE_COND && dsl.dataSourceCondDeps) { dsl.dataSourceCondDeps[target.id] = target.deps; - } else if (target.type === DepTargetType.DATA_SOURCE_METHOD) { + } else if (target.type === DepTargetType.DATA_SOURCE_METHOD && dsl.dataSourceMethodDeps) { dsl.dataSourceMethodDeps[target.id] = target.deps; } } @@ -194,11 +208,7 @@ class Dep extends BaseService { public collectNode(node: MNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) { // 先删除原有依赖,重新收集 if (isPage(node)) { - for (const [depKey, dep] of Object.entries(target.deps)) { - if (dep.data?.pageId && dep.data.pageId === depExtendedData.pageId) { - delete target.deps[depKey]; - } - } + this.removePageDep(target, depExtendedData); } else { this.watcher.removeTargetDep(target, node); } @@ -263,6 +273,19 @@ class Dep extends BaseService { return super.emit(eventName, ...args); } + /** + * 删除指定 page 在该 target 下的旧依赖: + * 按 pageId 匹配,可清掉页面内已被删除节点的残留依赖 + */ + private removePageDep(target: Target, depExtendedData: DepExtendedData = {}) { + for (const depKey of Object.keys(target.deps)) { + const dep = target.deps[depKey]; + if (dep.data?.pageId && dep.data.pageId === depExtendedData.pageId) { + delete target.deps[depKey]; + } + } + } + private enqueueTask(node: MNode, target: Target, depExtendedData: DepExtendedData, deep: boolean) { this.idleTask.enqueueTask( ({ node, deep, target }) => { diff --git a/packages/editor/src/utils/dep/worker.ts b/packages/editor/src/utils/dep/worker.ts index 274acfb2..e5710e22 100644 --- a/packages/editor/src/utils/dep/worker.ts +++ b/packages/editor/src/utils/dep/worker.ts @@ -42,9 +42,11 @@ onmessage = (e) => { } } - watcher.collectByCallback(mApp.items, undefined, ({ node, target }) => { - watcher.collectItem(node, target, { pageId: node.id }, true); - }); + // worker 中 target 均为新建(deps 为空),无需删除阶段,直接批量收集 + const targets = watcher.getCollectableTargets(); + for (const page of mApp.items) { + watcher.collectItems(page, targets, { pageId: page.id }, true); + } const data: Record> = { [DepTargetType.DATA_SOURCE]: {}, diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index d0df3986..a9dc1ae0 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -207,6 +207,7 @@ export interface MApp extends MComponent { dataSourceDeps?: DataSourceDeps; dataSourceCondDeps?: DataSourceDeps; + dataSourceMethodDeps?: DataSourceDeps; } // #endregion MApp