roymondchen 35ed2cc0d0 perf(data-source): 迭代器容器数据上下文改为惰性深拷贝
createIteratorContentData 原先整体深拷贝数据源数据,迭代项很多时为 O(N^2)。
改为沿 fields 路径逐层构造新对象,路径之外的对象型字段在首次读取时才拷贝并缓存,
既避免修改原始数据,也保证引用变化可被渲染层感知
2026-10-30 16:49:02 +08:00

314 lines
8.5 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 { cloneDeep } from 'lodash-es';
import type { DepData, DisplayCond, DisplayCondItem, MApp, MNode, MPage, MPageFragment } from '@tmagic/core';
import {
compiledCond,
compiledNode,
DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX,
dataSourceTemplateRegExp,
getValueByKeyPath,
isPageOrFragment,
NODE_CONDS_KEY,
replaceChildNode,
} from '@tmagic/core';
import type { AsyncDataSourceResolveResult, DataSourceManagerData } from './types';
/**
* 编译显示条件
* @param cond 条件配置
* @param data 上下文数据(数据源数据)
* @returns boolean
*/
export const compiledCondition = (cond: DisplayCondItem[], data: DataSourceManagerData) => {
let result = true;
for (const { op, value, range, field } of cond) {
const [sourceId, ...fields] = field;
const dsData = data[sourceId];
if (!dsData || !fields.length) {
break;
}
try {
const fieldValue = getValueByKeyPath(fields.join('.'), dsData);
if (!compiledCond(op, fieldValue, value, range)) {
result = false;
break;
}
} catch (e) {
console.warn(e);
}
}
return result;
};
/**
* 编译数据源条件组
* @param node dsl节点
* @param data 数据源数据
* @returns boolean
*/
export const compliedConditions = (node: { [NODE_CONDS_KEY]?: DisplayCond[] }, data: DataSourceManagerData) => {
if (!node[NODE_CONDS_KEY] || !Array.isArray(node[NODE_CONDS_KEY]) || !node[NODE_CONDS_KEY].length) return true;
for (const { cond } of node[NODE_CONDS_KEY]) {
if (!cond) continue;
if (compiledCondition(cond, data)) {
return true;
}
}
return false;
};
export const updateNode = (node: MNode, dsl: MApp) => {
if (isPageOrFragment(node)) {
const index = dsl.items?.findIndex((child: MNode) => child.id === node.id);
dsl.items.splice(index, 1, node as MPage | MPageFragment);
} else {
replaceChildNode(node, dsl!.items);
}
};
/**
* 将 source 的字段复制到 target,对象型字段使用惰性深拷贝(首次读取时拷贝并缓存)
* @param target 目标对象
* @param source 源对象
* @param skipKey 需要跳过的字段
*/
const assignLazyClone = (target: Record<string, any>, source: Record<string, any> | undefined, skipKey: string) => {
if (!source) return;
for (const key of Object.keys(source)) {
if (key === skipKey) continue;
const value = source[key];
if (typeof value !== 'object' || value === null) {
target[key] = value;
continue;
}
let cloned: any;
let isCloned = false;
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
if (!isCloned) {
cloned = cloneDeep(value);
isCloned = true;
}
return cloned;
},
set(newValue) {
cloned = newValue;
isCloned = true;
},
});
}
};
/**
* 创建迭代器容器编译的数据上下文
* @param itemData 迭代数据
* @param dsId 数据源id
* @param fields dsl节点字段,如a.b.c
* @returns 数据上下文
*/
export const createIteratorContentData = (
itemData: any,
dsId: string,
fields: string[] = [],
dsData: DataSourceManagerData = {},
) => {
const data: DataSourceManagerData = {
...dsData,
[dsId]: {},
};
// 沿 fields 路径逐层构造新对象,不修改原始数据。
// 路径之外的对象型字段不会预先整体深拷贝(迭代项很多时会导致 O(N²)),而是在首次被读取时才拷贝并缓存,
// 这样依然是与数据源隔离的快照(引用每次编译都是新的,保证 vue 组件的 props 能感知到按 path 原地修改的数据)。
let rawData: Record<string, any> | undefined = dsData[dsId];
let obj: Record<string, any> = data[dsId];
fields.forEach((key, index) => {
assignLazyClone(obj, rawData, key);
if (index === fields.length - 1) {
obj[key] = itemData;
return;
}
const next = rawData?.[key];
const nextObj: Record<string, any> = {};
obj[key] = nextObj;
rawData = Array.isArray(next) ? undefined : next;
obj = nextObj;
});
return data;
};
/**
* 编译通过tmagic-editor的数据源源选择器配(data-source-field-select)
* 格式为 [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}${id}`, 'field']
* DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX常量可通过@tmagic/utils获取
*
* @param value dsl节点中的数据源配置
* @param data 数据源数据
* @returns 编译好的配置
*/
export const compliedDataSourceField = (value: any, data: DataSourceManagerData) => {
const [prefixId, ...fields] = value;
const prefixIndex = prefixId.indexOf(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
if (prefixIndex > -1) {
const dsId = prefixId.substring(prefixIndex + DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX.length);
const dsData = data[dsId];
if (!dsData) return value;
try {
return getValueByKeyPath(fields.join('.'), dsData);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return value;
}
}
return value;
};
export const template = (value: string, data?: DataSourceManagerData) =>
value.replace(dataSourceTemplateRegExp, (match, $1) => {
try {
return getValueByKeyPath($1, data);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {
return match;
}
});
/**
* 编译通过tmagic-editor的数据源源选择器(data-source-input,data-source-select,data-source-field-select)配置出来的数据,或者其他符合规范的配置
* @param value dsl节点中的数据源配置
* @param data 数据源数据
* @returns 编译好的配置
*/
export const compiledNodeField = (value: any, data: DataSourceManagerData) => {
// 使用data-source-input等表单控件配置的字符串模板,如:`xxx${id.field}xxx`
if (typeof value === 'string') {
return template(value, data);
}
// 使用data-source-select等表单控件配置的数据源,如:{ isBindDataSource: true, dataSourceId: 'xxx'}
if (value?.isBindDataSource && value.dataSourceId) {
return data[value.dataSourceId];
}
// 指定数据源的字符串模板,如:{ isBindDataSourceField: true, dataSourceId: 'id', template: `xxx${field}xxx`}
if (value?.isBindDataSourceField && value.dataSourceId && typeof value.template === 'string') {
return template(value.template, data[value.dataSourceId]);
}
// 使用data-source-field-select等表单控件的数据源字段,如:[`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}${id}`, 'field']
if (Array.isArray(value) && typeof value[0] === 'string') {
return compliedDataSourceField(value, data);
}
return value;
};
export const compliedIteratorItem = ({
compile,
dsId,
item,
deps,
condDeps,
inEditor,
ctxData,
}: {
compile: (value: any) => any;
dsId: string;
item: MNode;
deps: DepData;
condDeps: DepData;
inEditor: boolean;
ctxData: DataSourceManagerData;
}) => {
const { items, ...node } = item;
const newNode = cloneDeep(node);
if (condDeps[node.id]?.keys.length && !inEditor) {
newNode.condResult = compliedConditions(node, ctxData);
}
if (Array.isArray(items) && items.length) {
newNode.items = items.map((item) =>
compliedIteratorItem({ compile, dsId, item, deps, condDeps, inEditor, ctxData }),
);
} else if (items) {
newNode.items = items;
}
if (!deps[newNode.id]?.keys.length) {
return newNode;
}
return compiledNode(
compile,
newNode,
{
[dsId]: deps,
},
dsId,
);
};
/**
* 按需加载数据源
*/
export const registerDataSourceOnDemand = async (
dsl: MApp,
dataSourceModules: Record<string, () => Promise<AsyncDataSourceResolveResult>>,
) => {
const { dataSourceMethodDeps = {}, dataSourceCondDeps = {}, dataSourceDeps = {}, dataSources = [] } = dsl;
const dsModuleMap: Record<string, () => Promise<AsyncDataSourceResolveResult>> = {};
dataSources.forEach((ds) => {
let dep = dataSourceCondDeps[ds.id] || {};
if (!Object.keys(dep).length) {
dep = dataSourceDeps[ds.id] || {};
}
if (!Object.keys(dep).length) {
dep = dataSourceMethodDeps[ds.id] || {};
}
if (Object.keys(dep).length && dataSourceModules[ds.type]) {
dsModuleMap[ds.type] = dataSourceModules[ds.type];
}
});
const modules = await Promise.all(Object.values(dsModuleMap).map((asyncModule) => asyncModule()));
const moduleMap: Record<string, any> = {};
modules.forEach((module, index) => {
const type = Object.keys(dsModuleMap)[index];
moduleMap[type] = module.default;
});
return moduleMap;
};