perf(dep): 依赖收集改为单次遍历批量处理多 target

将 collectItems/removeTargetsDep 改为整棵树只遍历一次、在每个属性上检查所有
target,把结构遍历开销从 ×targets 降到 ×1,收集结果保持一致。

同时修正 dataSourceMethodDeps 字段命名并补充到 MApp schema。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
roymondchen 2026-05-29 17:55:13 +08:00
parent a3333e2b4e
commit 025cca365c
7 changed files with 191 additions and 66 deletions

View File

@ -241,7 +241,7 @@ export const registerDataSourceOnDemand = async (
dsl: MApp,
dataSourceModules: Record<string, () => Promise<AsyncDataSourceResolveResult>>,
) => {
const { dataSourceMethodsDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl;
const { dataSourceMethodDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl;
const dsModuleMap: Record<string, () => Promise<AsyncDataSourceResolveResult>> = {};
@ -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]) {

View File

@ -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 {} };

View File

@ -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<string | number, any>, 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 ×1isTarget
*
* 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<string | number, any>,
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);
}
}
}
}
}

View File

@ -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<string, any>) => boolean;
export interface TargetOptions {
isTarget: IsTarget;

View File

@ -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 }) => {

View File

@ -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<string, Record<Id, DepData>> = {
[DepTargetType.DATA_SOURCE]: {},

View File

@ -207,6 +207,7 @@ export interface MApp extends MComponent {
dataSourceDeps?: DataSourceDeps;
dataSourceCondDeps?: DataSourceDeps;
dataSourceMethodDeps?: DataSourceDeps;
}
// #endregion MApp