mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-29 19:58:05 +00:00
perf(dep): 依赖收集改为单次遍历批量处理多 target
将 collectItems/removeTargetsDep 改为整棵树只遍历一次、在每个属性上检查所有 target,把结构遍历开销从 ×targets 降到 ×1,收集结果保持一致。 同时修正 dataSourceMethodDeps 字段命名并补充到 MApp schema。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a3333e2b4e
commit
025cca365c
@ -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]) {
|
||||
|
||||
@ -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 {} };
|
||||
|
||||
@ -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 降到 ×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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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]: {},
|
||||
|
||||
@ -207,6 +207,7 @@ export interface MApp extends MComponent {
|
||||
|
||||
dataSourceDeps?: DataSourceDeps;
|
||||
dataSourceCondDeps?: DataSourceDeps;
|
||||
dataSourceMethodDeps?: DataSourceDeps;
|
||||
}
|
||||
// #endregion MApp
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user