feat(editor): 优化依赖收集,数据源代码块分开收集

This commit is contained in:
roymondchen 2025-01-14 16:27:04 +08:00
parent b215062177
commit 040b787e8f
4 changed files with 336 additions and 305 deletions

View File

@ -1,4 +1,4 @@
import { computed, onBeforeUnmount, reactive, toRaw, watch } from 'vue';
import { onBeforeUnmount, reactive, toRaw, watch } from 'vue';
import { cloneDeep } from 'lodash-es';
import type TMagicCore from '@tmagic/core';
@ -8,7 +8,7 @@ import type {
EventOption,
Id,
MApp,
MNode,
MComponent,
MPage,
MPageFragment,
} from '@tmagic/core';
@ -22,7 +22,7 @@ import {
Target,
} from '@tmagic/core';
import { ChangeRecord } from '@tmagic/form';
import { getNodes, isPage, isValueIncludeDataSource, traverseNode } from '@tmagic/utils';
import { getNodes, isPage, isValueIncludeDataSource } from '@tmagic/utils';
import PropsPanel from './layouts/PropsPanel.vue';
import { isIncludeDataSource } from './utils/editor';
@ -106,11 +106,12 @@ export const initServiceState = (
const eventsList: Record<string, EventOption[]> = {};
const methodsList: Record<string, EventOption[]> = {};
eventMethodList &&
Object.keys(eventMethodList).forEach((type: string) => {
if (eventMethodList) {
for (const type of Object.keys(eventMethodList)) {
eventsList[type] = eventMethodList[type].events;
methodsList[type] = eventMethodList[type].methods;
});
}
}
eventsService.setEvents(eventsList);
eventsService.setMethods(methodsList);
@ -123,10 +124,13 @@ export const initServiceState = (
watch(
() => props.datasourceConfigs,
(configs) => {
configs &&
Object.entries(configs).forEach(([key, value]) => {
dataSourceService.setFormConfig(key, value);
});
if (!configs) {
return;
}
for (const [key, value] of Object.entries(configs)) {
dataSourceService.setFormConfig(key, value);
}
},
{
immediate: true,
@ -136,10 +140,13 @@ export const initServiceState = (
watch(
() => props.datasourceValues,
(values) => {
values &&
Object.entries(values).forEach(([key, value]) => {
dataSourceService.setFormValue(key, value);
});
if (!values) {
return;
}
for (const [key, value] of Object.entries(values)) {
dataSourceService.setFormValue(key, value);
}
},
{
immediate: true,
@ -152,18 +159,20 @@ export const initServiceState = (
const eventsList: Record<string, EventOption[]> = {};
const methodsList: Record<string, EventOption[]> = {};
eventMethodList &&
Object.keys(eventMethodList).forEach((type: string) => {
if (eventMethodList) {
for (const type of Object.keys(eventMethodList)) {
eventsList[type] = eventMethodList[type].events;
methodsList[type] = eventMethodList[type].methods;
});
}
}
Object.entries(eventsList).forEach(([key, value]) => {
for (const [key, value] of Object.entries(eventsList)) {
dataSourceService.setFormEvent(key, value);
});
Object.entries(methodsList).forEach(([key, value]) => {
}
for (const [key, value] of Object.entries(methodsList)) {
dataSourceService.setFormMethod(key, value);
});
}
},
{
immediate: true,
@ -203,6 +212,113 @@ export const initServiceEvents = (
((event: 'update:modelValue', value: MApp | null) => void),
{ editorService, codeBlockService, dataSourceService, depService }: Services,
) => {
const getTMagicApp = () => {
const renderer = editorService.get('stage')?.renderer;
if (!renderer) {
return undefined;
}
if (renderer.runtime) {
return renderer.runtime.getApp?.();
}
return new Promise<TMagicCore | undefined>((resolve) => {
// 设置 10s 超时
const timeout = globalThis.setTimeout(() => {
resolve(undefined);
}, 10000);
renderer.on('runtime-ready', () => {
if (timeout) {
globalThis.clearTimeout(timeout);
}
resolve(renderer.runtime?.getApp?.());
});
});
};
const updateStageNodes = (nodes: MComponent[]) => {
for (const node of nodes) {
updateStageNode(node);
}
};
const updateStageNode = (node: MComponent) => {
const root = editorService.get('root');
if (!root) return;
return editorService.get('stage')?.update({
config: cloneDeep(node),
parentId: editorService.getParentById(node.id)?.id,
root: cloneDeep(root),
});
};
const updateDataSourceSchema = async () => {
const root = editorService.get('root');
const app = await getTMagicApp();
if (!app || !root) {
return;
}
if (app.dsl) {
app.dsl.dataSources = root.dataSources;
}
if (root.dataSources) {
app.dataSourceManager?.updateSchema(root.dataSources);
}
};
const dsDepCollectedHandler = async () => {
const root = editorService.get('root');
const app = await getTMagicApp();
if (root && app?.dsl) {
app.dsl.dataSourceDeps = root.dataSourceDeps;
}
};
const collectIdle = (nodes: MComponent[], deep: boolean, type?: DepTargetType) =>
Promise.all(
nodes.map((node) => {
let pageId: Id | undefined;
if (isPage(node)) {
pageId = node.id;
} else {
const info = editorService.getNodeInfo(node.id);
pageId = info.page?.id;
}
return depService.collectIdle([node], { pageId }, deep, type);
}),
);
watch(
() => editorService.get('stage'),
(stage) => {
if (!stage) {
return;
}
stage.on('rerender', async () => {
const node = editorService.get('node');
if (!node) return;
await collectIdle([node], true, DepTargetType.DATA_SOURCE);
updateStageNode(node);
});
},
);
const initDataSourceDepTarget = (ds: DataSourceSchema) => {
depService.addTarget(createDataSourceTarget(ds, reactive({})));
depService.addTarget(createDataSourceMethodTarget(ds, reactive({})));
depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
};
const rootChangeHandler = async (value: MApp | null, preValue?: MApp | null) => {
if (!value) return;
@ -214,17 +330,19 @@ export const initServiceEvents = (
depService.removeTargets(DepTargetType.CODE_BLOCK);
Object.entries(value.codeBlocks).forEach(([id, code]) => {
for (const [id, code] of Object.entries(value.codeBlocks)) {
depService.addTarget(createCodeBlockTarget(id, code));
});
}
dataSourceService.get('dataSources').forEach((ds) => {
for (const ds of dataSourceService.get('dataSources')) {
initDataSourceDepTarget(ds);
});
}
if (Array.isArray(value.items)) {
depService.clearIdleTasks();
collectIdle(value.items, true);
collectIdle(value.items, true).then(() => {
updateStageNodes(value.items);
});
} else {
depService.clear();
delete value.dataSourceDeps;
@ -252,61 +370,203 @@ export const initServiceEvents = (
}
};
const stage = computed(() => editorService.get('stage'));
// 新增节点,收集依赖
const nodeAddHandler = async (nodes: MComponent[]) => {
await collectIdle(nodes, true);
watch(stage, (stage) => {
if (!stage) {
return;
}
stage.on('rerender', () => {
const node = editorService.get('node');
if (!node) return;
collectIdle([node], true);
updateStage([node]);
});
});
const getApp = () => {
const renderer = stage.value?.renderer;
if (!renderer) {
return undefined;
}
if (renderer.runtime) {
return renderer.runtime.getApp?.();
}
return new Promise<TMagicCore | undefined>((resolve) => {
const timeout = globalThis.setTimeout(() => {
resolve(undefined);
}, 10000);
renderer.on('runtime-ready', () => {
if (timeout) {
globalThis.clearTimeout(timeout);
}
resolve(renderer.runtime?.getApp?.());
});
});
updateStageNodes(nodes);
};
const updateDataSourceSchema = async () => {
const root = editorService.get('root');
const app = await getApp();
// 节点更新,收集依赖
// 仅当修改到数据源相关的才收集
const nodeUpdateHandler = async (
data: { newNode: MComponent; oldNode: MComponent; changeRecords?: ChangeRecord[] }[],
) => {
const needRecollectNodes: MComponent[] = [];
const normalNodes: MComponent[] = [];
for (const { newNode, oldNode, changeRecords } of data) {
if (changeRecords?.length) {
// eslint-disable-next-line no-restricted-syntax
forChangeRecords: for (const record of changeRecords) {
if (!record.propPath) {
needRecollectNodes.push(newNode);
break forChangeRecords;
}
if (root && app?.dsl) {
app.dsl.dataSourceDeps = root.dataSourceDeps;
app.dsl.dataSources = root.dataSources;
// NODE_CONDS_KEY为显示条件key
if (
new RegExp(`${NODE_CONDS_KEY}.(\\d)+.cond`).test(record.propPath) ||
new RegExp(`${NODE_CONDS_KEY}.(\\d)+.cond.(\\d)+.value`).test(record.propPath) ||
record.propPath === NODE_CONDS_KEY ||
isValueIncludeDataSource(record.value)
) {
needRecollectNodes.push(newNode);
break forChangeRecords;
}
// 修改的key在收集的依赖中则需要触发重新收集
for (const target of Object.values(depService.getTargets(DepTargetType.DATA_SOURCE))) {
if (!target.deps[newNode.id]) {
continue;
}
if (target.deps[newNode.id].keys.includes(record.propPath)) {
needRecollectNodes.push(newNode);
break forChangeRecords;
}
}
normalNodes.push(newNode);
}
} else if (isIncludeDataSource(newNode, oldNode)) {
needRecollectNodes.push(newNode);
} else {
normalNodes.push(newNode);
}
}
if (root?.dataSources) {
if (needRecollectNodes.length) {
// 有数据源依赖需要等依赖重新收集完才更新stage
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE);
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND);
updateStageNodes(needRecollectNodes);
} else {
updateStageNodes(normalNodes);
// 在上面判断是否需要收集数据源依赖中已经更新stage
Promise.all([
collectIdle(normalNodes, true, DepTargetType.CODE_BLOCK),
collectIdle(normalNodes, true, DepTargetType.DATA_SOURCE_METHOD),
]);
}
};
// 节点删除,清除对齐的依赖收集
const nodeRemoveHandler = (nodes: MComponent[]) => {
depService.clear(nodes);
};
// 由于历史记录变化是更新整个page所以历史记录变化时需要重新收集依赖
const historyChangeHandler = async (page: MPage | MPageFragment) => {
await collectIdle([page], true);
updateStageNode(page);
};
editorService.on('history-change', historyChangeHandler);
editorService.on('root-change', rootChangeHandler);
editorService.on('add', nodeAddHandler);
editorService.on('remove', nodeRemoveHandler);
editorService.on('update', nodeUpdateHandler);
const dataSourceAddHandler = async (config: DataSourceSchema) => {
initDataSourceDepTarget(config);
const app = await getTMagicApp();
app?.dataSourceManager?.addDataSource(config);
};
const dataSourceUpdateHandler = async (
config: DataSourceSchema,
{ changeRecords }: { changeRecords: ChangeRecord[] },
) => {
let needRecollectDep = false;
let isModifyField = false;
let isModifyMock = false;
let isModifyMethod = false;
for (const changeRecord of changeRecords) {
if (!changeRecord.propPath) {
continue;
}
isModifyField =
changeRecord.propPath === 'fields' ||
/fields.(\d)+.name/.test(changeRecord.propPath) ||
/fields.(\d)+$/.test(changeRecord.propPath);
isModifyMock = changeRecord.propPath === 'mocks';
isModifyMethod =
changeRecord.propPath === 'methods' ||
/methods.(\d)+.name/.test(changeRecord.propPath) ||
/methods.(\d)+$/.test(changeRecord.propPath);
needRecollectDep = isModifyField || isModifyMock || isModifyMethod;
if (needRecollectDep) {
break;
}
}
const root = editorService.get('root');
if (needRecollectDep) {
if (Array.isArray(root?.items)) {
depService.clearIdleTasks();
removeDataSourceTarget(config.id);
initDataSourceDepTarget(config);
let collectIdlePromises: Promise<void[]>[] = [];
if (isModifyField) {
collectIdlePromises = [
collectIdle(root.items, true, DepTargetType.DATA_SOURCE),
collectIdle(root.items, true, DepTargetType.DATA_SOURCE_COND),
];
} else if (isModifyMock) {
collectIdlePromises = [collectIdle(root.items, true, DepTargetType.DATA_SOURCE)];
} else if (isModifyMethod) {
collectIdlePromises = [collectIdle(root.items, true, DepTargetType.DATA_SOURCE_METHOD)];
}
Promise.all(collectIdlePromises).then(() => {
updateDataSourceSchema();
updateStageNodes(root.items);
});
}
} else if (root?.dataSources) {
const app = await getTMagicApp();
app?.dataSourceManager?.updateSchema(root.dataSources);
}
};
const removeDataSourceTarget = (id: string) => {
depService.removeTarget(id, DepTargetType.DATA_SOURCE);
depService.removeTarget(id, DepTargetType.DATA_SOURCE_COND);
depService.removeTarget(id, DepTargetType.DATA_SOURCE_METHOD);
};
const dataSourceRemoveHandler = (id: string) => {
const root = editorService.get('root');
const nodeIds = Object.keys(root?.dataSourceDeps?.[id] || {});
const nodes = getNodes(nodeIds, root?.items);
Promise.all([
collectIdle(nodes, false, DepTargetType.DATA_SOURCE),
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_COND),
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_METHOD),
]).then(() => {
updateDataSourceSchema();
updateStageNodes(nodes);
});
removeDataSourceTarget(id);
};
dataSourceService.on('add', dataSourceAddHandler);
dataSourceService.on('update', dataSourceUpdateHandler);
dataSourceService.on('remove', dataSourceRemoveHandler);
const codeBlockAddOrUpdateHandler = (id: Id, codeBlock: CodeBlockContent) => {
if (depService.hasTarget(id, DepTargetType.CODE_BLOCK)) {
depService.getTarget(id, DepTargetType.CODE_BLOCK)!.name = codeBlock.name;
return;
}
depService.addTarget(createCodeBlockTarget(id, codeBlock));
};
const codeBlockRemoveHandler = (id: Id) => {
depService.removeTarget(id, DepTargetType.CODE_BLOCK);
};
codeBlockService.on('addOrUpdate', codeBlockAddOrUpdateHandler);
codeBlockService.on('remove', codeBlockRemoveHandler);
const targetAddHandler = (target: Target) => {
const root = editorService.get('root');
if (!root) return;
@ -340,239 +600,10 @@ export const initServiceEvents = (
}
};
/**
* dsl后会执行依赖收集并调用此方法
* updateStage应当时一次inDeps参数来区分调用时机
* inDeps为true是则更新依赖收集到的节点
* @param nodes
* @param inDeps
* @returns
*/
const updateStage = (nodes: MNode[], inDeps = false) => {
const root = editorService.get('root');
if (!root) return;
const update = (node: MNode) =>
stage.value?.update({
config: cloneDeep(node),
parentId: editorService.getParentById(node.id)?.id,
root: cloneDeep(root),
});
nodes.forEach((node) => {
const inDepsNodeId: Id[] = [];
const deps = Object.values(root.dataSourceDeps || {});
deps.forEach((dep) => {
Object.keys(dep).forEach((id) => {
inDepsNodeId.push(id);
});
});
if (inDeps) {
if (inDepsNodeId.includes(node.id)) {
update(node);
}
} else {
if (!inDepsNodeId.includes(node.id)) {
update(node);
}
}
});
};
const dsDepCollectedHandler = async (nodes: MNode[], deep: boolean) => {
const root = editorService.get('root');
if (!root) return;
const app = await getApp();
if (app?.dsl) {
app.dsl.dataSourceDeps = root.dataSourceDeps;
}
if (deep) {
nodes.forEach((node) => {
traverseNode<MNode>(
node,
(node) => {
updateStage([node], true);
},
[],
true,
);
});
} else {
updateStage(nodes, true);
}
};
depService.on('add-target', targetAddHandler);
depService.on('remove-target', targetRemoveHandler);
depService.on('ds-collected', dsDepCollectedHandler);
const initDataSourceDepTarget = (ds: DataSourceSchema) => {
depService.addTarget(createDataSourceTarget(ds, reactive({})));
depService.addTarget(createDataSourceMethodTarget(ds, reactive({})));
depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
};
const collectIdle = (nodes: MNode[], deep: boolean) =>
Promise.all(
nodes.map((node) => {
let pageId: Id | undefined;
if (isPage(node)) {
pageId = node.id;
} else {
const info = editorService.getNodeInfo(node.id);
pageId = info.page?.id;
}
return depService.collectIdle([node], { pageId }, deep);
}),
);
// 新增节点,收集依赖
const nodeAddHandler = (nodes: MNode[]) => {
collectIdle(nodes, true);
updateStage(nodes);
};
// 节点更新,收集依赖
// 仅当修改到数据源相关的才收集
const nodeUpdateHandler = (data: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]) => {
const needRecollectNodes: MNode[] = [];
const normalNodes: MNode[] = [];
data.forEach(({ newNode, oldNode, changeRecords }) => {
if (changeRecords?.length) {
for (const record of changeRecords) {
// NODE_CONDS_KEY为显示条件key
if (
!record.propPath ||
new RegExp(`${NODE_CONDS_KEY}.(\\d)+.cond`).test(record.propPath) ||
new RegExp(`${NODE_CONDS_KEY}.(\\d)+.cond.(\\d)+.value`).test(record.propPath) ||
record.propPath === NODE_CONDS_KEY ||
isValueIncludeDataSource(record.value)
) {
needRecollectNodes.push(newNode);
} else {
normalNodes.push(newNode);
}
}
} else if (isIncludeDataSource(newNode, oldNode)) {
needRecollectNodes.push(newNode);
} else {
normalNodes.push(newNode);
}
});
if (needRecollectNodes.length) {
collectIdle(needRecollectNodes, true);
updateStage(needRecollectNodes);
} else if (normalNodes.length) {
updateStage(normalNodes);
}
};
// 节点删除,清除对齐的依赖收集
const nodeRemoveHandler = (nodes: MNode[]) => {
depService.clear(nodes);
};
// 由于历史记录变化是更新整个page所以历史记录变化时需要重新收集依赖
const historyChangeHandler = (page: MPage | MPageFragment) => {
collectIdle([page], true).then(() => {
updateDataSourceSchema();
});
};
editorService.on('history-change', historyChangeHandler);
editorService.on('root-change', rootChangeHandler);
editorService.on('add', nodeAddHandler);
editorService.on('remove', nodeRemoveHandler);
editorService.on('update', nodeUpdateHandler);
const codeBlockAddOrUpdateHandler = (id: Id, codeBlock: CodeBlockContent) => {
if (depService.hasTarget(id, DepTargetType.CODE_BLOCK)) {
depService.getTarget(id, DepTargetType.CODE_BLOCK)!.name = codeBlock.name;
return;
}
depService.addTarget(createCodeBlockTarget(id, codeBlock));
};
const codeBlockRemoveHandler = (id: Id) => {
depService.removeTarget(id, DepTargetType.CODE_BLOCK);
};
codeBlockService.on('addOrUpdate', codeBlockAddOrUpdateHandler);
codeBlockService.on('remove', codeBlockRemoveHandler);
const dataSourceAddHandler = async (config: DataSourceSchema) => {
initDataSourceDepTarget(config);
const app = await getApp();
app?.dataSourceManager?.addDataSource(config);
};
const dataSourceUpdateHandler = async (
config: DataSourceSchema,
{ changeRecords }: { changeRecords: ChangeRecord[] },
) => {
let needRecollectDep = false;
for (const changeRecord of changeRecords) {
if (!changeRecord.propPath) {
continue;
}
needRecollectDep =
changeRecord.propPath === 'fields' ||
changeRecord.propPath === 'methods' ||
/fields.(\d)+.name/.test(changeRecord.propPath) ||
/fields.(\d)+$/.test(changeRecord.propPath) ||
/methods.(\d)+.name/.test(changeRecord.propPath) ||
/methods.(\d)+$/.test(changeRecord.propPath);
if (needRecollectDep) {
break;
}
}
const root = editorService.get('root');
if (needRecollectDep) {
if (Array.isArray(root?.items)) {
depService.clearIdleTasks();
removeDataSourceTarget(config.id);
initDataSourceDepTarget(config);
collectIdle(root.items, true).then(() => {
updateDataSourceSchema();
});
}
} else if (root?.dataSources) {
const app = await getApp();
app?.dataSourceManager?.updateSchema(root.dataSources);
}
};
const removeDataSourceTarget = (id: string) => {
depService.removeTarget(id, DepTargetType.DATA_SOURCE);
depService.removeTarget(id, DepTargetType.DATA_SOURCE_COND);
depService.removeTarget(id, DepTargetType.DATA_SOURCE_METHOD);
};
const dataSourceRemoveHandler = (id: string) => {
const root = editorService.get('root');
const nodeIds = Object.keys(root?.dataSourceDeps?.[id] || {});
const nodes = getNodes(nodeIds, root?.items);
collectIdle(nodes, false).then(() => {
updateDataSourceSchema();
});
removeDataSourceTarget(id);
};
dataSourceService.on('add', dataSourceAddHandler);
dataSourceService.on('update', dataSourceUpdateHandler);
dataSourceService.on('remove', dataSourceRemoveHandler);
onBeforeUnmount(() => {
depService.off('add-target', targetAddHandler);
depService.off('remove-target', targetRemoveHandler);

View File

@ -119,10 +119,10 @@ class Dep extends BaseService {
idleTask.once('finish', () => {
this.emit('collected', nodes, deep);
this.set('collecting', false);
resolve();
});
idleTask.once('hight-level-finish', () => {
this.emit('ds-collected', nodes, deep);
resolve();
});
});
}
@ -130,11 +130,11 @@ class Dep extends BaseService {
public collectNode(node: MNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
// 先删除原有依赖,重新收集
if (isPage(node)) {
Object.entries(target.deps).forEach(([depKey, dep]) => {
for (const [depKey, dep] of Object.entries(target.deps)) {
if (dep.data?.pageId && dep.data.pageId === depExtendedData.pageId) {
delete target.deps[depKey];
}
});
}
} else {
this.watcher.removeTargetDep(target, node);
}

View File

@ -80,7 +80,7 @@ export class IdleTask<T = any> extends EventEmitter {
// 动画会占用空闲时间,当任务一直无法执行时,看看是否有动画正在播放
// 根据空闲时间的多少来决定执行的任务数,保证页面不卡死的情况下尽量多执行任务,不然当任务数巨大时,执行时间会很久
// 执行不完不会影响配置,但是会影响画布渲染
while (deadline.timeRemaining() > 0 && taskList.length) {
while (deadline.timeRemaining() > 0 && (taskList.length || hightLevelTaskList.length)) {
const timeRemaining = deadline.timeRemaining();
let times = 0;
if (timeRemaining <= 5) {

View File

@ -508,7 +508,7 @@ export const traverseNode = <T extends NodeItem = NodeItem>(
if (Array.isArray(node.items) && node.items.length) {
parents.push(node);
node.items.forEach((item) => {
traverseNode(item as T, cb, [...parents]);
traverseNode(item as T, cb, [...parents], evalCbAfter);
});
}