roymondchen 025cca365c perf(dep): 依赖收集改为单次遍历批量处理多 target
将 collectItems/removeTargetsDep 改为整棵树只遍历一次、在每个属性上检查所有
target,把结构遍历开销从 ×targets 降到 ×1,收集结果保持一致。

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:55:13 +08:00

355 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NODE_DISABLE_CODE_BLOCK_KEY, NODE_DISABLE_DATA_SOURCE_KEY } from '@tmagic/schema';
import { isObject } from '@tmagic/utils';
import type Target from './Target';
import { type DepExtendedData, DepTargetType, type TargetList, TargetNode } from './types';
import { traverseTarget } from './utils';
const DATA_SOURCE_TARGET_TYPES: Set<string> = new Set([
DepTargetType.DATA_SOURCE,
DepTargetType.DATA_SOURCE_COND,
DepTargetType.DATA_SOURCE_METHOD,
]);
export default class Watcher {
private targetsList: TargetList = {};
private childrenProp = 'items';
private idProp = 'id';
private nameProp = 'name';
constructor(options?: { initialTargets?: TargetList; childrenProp?: string }) {
if (options?.initialTargets) {
this.targetsList = options.initialTargets;
}
if (options?.childrenProp) {
this.childrenProp = options.childrenProp;
}
}
public getTargetsList() {
return this.targetsList;
}
/**
* 获取指定类型中的所有target
* @param type 分类
* @returns Target[]
*/
public getTargets(type: string = DepTargetType.DEFAULT) {
return this.targetsList[type] || {};
}
/**
* 添加新的目标
* @param target Target
*/
public addTarget(target: Target) {
const targets = this.getTargets(target.type) || {};
this.targetsList[target.type] = targets;
targets[target.id] = target;
}
/**
* 获取指定id的target
* @param id target id
* @returns Target
*/
public getTarget(id: string | number, type: string = DepTargetType.DEFAULT) {
return this.getTargets(type)[id];
}
/**
* 判断是否存在指定id的target
* @param id target id
* @returns boolean
*/
public hasTarget(id: string | number, type: string = DepTargetType.DEFAULT) {
return Boolean(this.getTarget(id, type));
}
/**
* 判断是否存在指定类型的target
* @param type target type
* @returns boolean
*/
public hasSpecifiedTypeTarget(type: string = DepTargetType.DEFAULT): boolean {
return Object.keys(this.getTargets(type)).length > 0;
}
/**
* 删除指定id的target
* @param id target id
*/
public removeTarget(id: string | number, type: string = DepTargetType.DEFAULT) {
const targets = this.getTargets(type);
if (targets[id]) {
targets[id].destroy();
delete targets[id];
}
}
/**
* 删除指定分类的所有target
* @param type 分类
* @returns void
*/
public removeTargets(type: string = DepTargetType.DEFAULT) {
const targets = this.targetsList[type];
if (!targets) return;
for (const target of Object.values(targets)) {
target.destroy();
}
delete this.targetsList[type];
}
/**
* 删除所有target
*/
public clearTargets() {
for (const key of Object.keys(this.targetsList)) {
delete this.targetsList[key];
}
}
/**
* 收集依赖
* @param nodes 需要收集的节点
* @param deep 是否需要收集子节点
* @param type 强制收集指定类型的依赖
*/
public collect(
nodes: TargetNode[],
depExtendedData: DepExtendedData = {},
deep = false,
type?: DepTargetType | string,
) {
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(
nodes: TargetNode[],
type: DepTargetType | string | undefined,
cb: (data: { node: TargetNode; target: Target }) => void,
) {
traverseTarget(
this.targetsList,
(target) => {
if (!type && !target.isCollectByDefault) {
return;
}
for (const node of nodes) {
cb({ node, target });
}
},
type,
);
}
/**
* 清除所有目标的依赖
* @param nodes 需要清除依赖的节点
*/
public clear(nodes?: TargetNode[], type?: DepTargetType | string) {
let { targetsList } = this;
if (type) {
targetsList = {
[type]: this.getTargets(type),
};
}
const clearedItemsNodeIds = new Set<string | number>();
traverseTarget(targetsList, (target) => {
if (nodes) {
for (const node of nodes) {
target.removeDep(node[this.idProp]);
if (
Array.isArray(node[this.childrenProp]) &&
node[this.childrenProp].length &&
!clearedItemsNodeIds.has(node[this.idProp])
) {
clearedItemsNodeIds.add(node[this.idProp]);
this.clear(node[this.childrenProp]);
}
}
} else {
target.removeDep();
}
});
}
/**
* 清除指定类型的依赖
* @param type 类型
* @param nodes 需要清除依赖的节点
*/
public clearByType(type: DepTargetType | string, nodes?: TargetNode[]) {
this.clear(nodes, type);
}
/**
* 收集单个 target 的依赖,等价于 collectItems(node, [target], ...)
*/
public collectItem(node: TargetNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
this.collectItems(node, [target], depExtendedData, deep);
}
public removeTargetDep(target: Target, node: TargetNode, key?: string | number) {
target.removeDep(node[this.idProp], key);
if (typeof key === 'undefined' && Array.isArray(node[this.childrenProp]) && node[this.childrenProp].length) {
for (const item of node[this.childrenProp] as TargetNode[]) {
this.removeTargetDep(target, item, key);
}
}
}
/**
* 与 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);
}
}
}
}
}