fix(editor): 修复修改页面配置后可能改造页面卡死问题

This commit is contained in:
roymondchen 2024-09-29 14:44:13 +08:00 committed by roymondchen
parent 5dad9f0ec9
commit fc38fc3957
6 changed files with 178 additions and 113 deletions

View File

@ -1,5 +1,5 @@
import { onBeforeUnmount, reactive, toRaw, watch } from 'vue';
import { cloneDeep, debounce } from 'lodash-es';
import { cloneDeep } from 'lodash-es';
import type {
CodeBlockContent,
@ -19,7 +19,7 @@ import {
DepTargetType,
Target,
} from '@tmagic/core';
import { isPage, traverseNode } from '@tmagic/utils';
import { getNodes, isPage, traverseNode } from '@tmagic/utils';
import PropsPanel from './layouts/PropsPanel.vue';
import { EditorProps } from './editorProps';
@ -252,62 +252,23 @@ export const initServiceEvents = (
return stage?.renderer?.runtime?.getApp?.();
};
const updateDataSourceSchema = () => {
const updateDataSourceSchema = (nodes: MNode[], deep: boolean) => {
const root = editorService.get('root');
if (root?.dataSources) {
getApp()?.dataSourceManager?.updateSchema(root.dataSources);
}
};
const targetAddHandler = (target: Target) => {
const root = editorService.get('root');
if (!root) return;
if (target.type === DepTargetType.DATA_SOURCE) {
if (!root.dataSourceDeps) {
root.dataSourceDeps = {};
}
root.dataSourceDeps[target.id] = target.deps;
}
if (target.type === DepTargetType.DATA_SOURCE_COND) {
if (!root.dataSourceCondDeps) {
root.dataSourceCondDeps = {};
}
root.dataSourceCondDeps[target.id] = target.deps;
}
};
const targetRemoveHandler = (id: string | number) => {
const root = editorService.get('root');
if (root?.dataSourceDeps) {
delete root.dataSourceDeps[id];
}
if (root?.dataSourceCondDeps) {
delete root.dataSourceCondDeps[id];
}
};
const collectedHandler = debounce((nodes: MNode[], deep: boolean) => {
const root = editorService.get('root');
const stage = editorService.get('stage');
if (!root || !stage) return;
const app = getApp();
if (!app) return;
if (app.dsl) {
if (root && app?.dsl) {
app.dsl.dataSourceDeps = root.dataSourceDeps;
app.dsl.dataSourceCondDeps = root.dataSourceCondDeps;
app.dsl.dataSources = root.dataSources;
}
updateDataSourceSchema();
if (root?.dataSources) {
getApp()?.dataSourceManager?.updateSchema(root.dataSources);
}
const stage = editorService.get('stage');
if (!root || !stage) return;
const allNodes: MNode[] = [];
@ -335,11 +296,60 @@ export const initServiceEvents = (
});
});
});
}, 300);
};
const afterUpdateNodes = (nodes: MNode[]) => {
const root = editorService.get('root');
if (!root) return;
const stage = editorService.get('stage');
const app = getApp();
if (app?.dsl) {
app.dsl.dataSourceDeps = root.dataSourceDeps;
}
for (const node of nodes) {
stage?.update({
config: cloneDeep(node),
parentId: editorService.getParentById(node.id)?.id,
root: cloneDeep(root),
});
}
};
const targetAddHandler = (target: Target) => {
const root = editorService.get('root');
if (!root) return;
if (target.type === DepTargetType.DATA_SOURCE) {
if (!root.dataSourceDeps) {
root.dataSourceDeps = {};
}
root.dataSourceDeps[target.id] = target.deps;
}
if (target.type === DepTargetType.DATA_SOURCE_COND) {
if (!root.dataSourceCondDeps) {
root.dataSourceCondDeps = {};
}
root.dataSourceCondDeps[target.id] = target.deps;
}
};
const targetRemoveHandler = (id: string | number) => {
const root = editorService.get('root');
if (!root) return;
if (root.dataSourceDeps) {
delete root.dataSourceDeps[id];
}
if (root.dataSourceCondDeps) {
delete root.dataSourceCondDeps[id];
}
};
depService.on('add-target', targetAddHandler);
depService.on('remove-target', targetRemoveHandler);
depService.on('collected', collectedHandler);
const initDataSourceDepTarget = (ds: DataSourceSchema) => {
depService.addTarget(createDataSourceTarget(ds, reactive({})));
@ -347,28 +357,33 @@ export const initServiceEvents = (
depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
};
const collectIdle = (nodes: MNode[], deep: boolean) => {
nodes.forEach((node) => {
let pageId: Id | undefined;
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;
}
depService.collectIdle([node], { pageId }, deep);
});
};
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);
collectIdle(nodes, true).then(() => {
afterUpdateNodes(nodes);
});
};
// 节点更新,收集依赖
const nodeUpdateHandler = (nodes: MNode[]) => {
collectIdle(nodes, true);
collectIdle(nodes, true).then(() => {
afterUpdateNodes(nodes);
});
};
// 节点删除,清除对齐的依赖收集
@ -378,7 +393,9 @@ export const initServiceEvents = (
// 由于历史记录变化是更新整个page所以历史记录变化时需要重新收集依赖
const historyChangeHandler = (page: MPage | MPageFragment) => {
collectIdle([page], true);
collectIdle([page], true).then(() => {
updateDataSourceSchema([page], true);
});
};
editorService.on('history-change', historyChangeHandler);
@ -413,7 +430,9 @@ export const initServiceEvents = (
removeDataSourceTarget(config.id);
initDataSourceDepTarget(config);
collectIdle(root?.items || [], true);
collectIdle(root?.items || [], true).then(() => {
updateDataSourceSchema(root?.items || [], true);
});
};
const removeDataSourceTarget = (id: string) => {
@ -423,8 +442,14 @@ export const initServiceEvents = (
};
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(nodes, false);
});
removeDataSourceTarget(id);
getApp()?.dataSourceManager?.removeDataSource(id);
};
dataSourceService.on('add', dataSourceAddHandler);
@ -434,7 +459,6 @@ export const initServiceEvents = (
onBeforeUnmount(() => {
depService.off('add-target', targetAddHandler);
depService.off('remove-target', targetRemoveHandler);
depService.off('collected', collectedHandler);
editorService.off('history-change', historyChangeHandler);
editorService.off('root-change', rootChangeHandler);

View File

@ -132,9 +132,20 @@ watch(zoom, (zoom) => {
stage.setZoom(zoom);
});
let timeoutId: NodeJS.Timeout | null = null;
watch(page, (page) => {
if (runtime && page) {
services?.editorService.set('stageLoading', true);
if (timeoutId) {
globalThis.clearTimeout(timeoutId);
}
timeoutId = globalThis.setTimeout(() => {
services?.editorService.set('stageLoading', false);
timeoutId = null;
}, 3000);
runtime.updatePageId?.(page.id);
nextTick(() => {
stage?.select(page.id);

View File

@ -92,8 +92,11 @@ class Dep extends BaseService {
);
});
idleTask.once('finish', () => {
this.emit('collected', nodes, deep);
return new Promise<void>((resolve) => {
idleTask.once('finish', () => {
this.emit('collected', nodes, deep);
resolve();
});
});
}

View File

@ -20,10 +20,19 @@ import { reactive, toRaw } from 'vue';
import { cloneDeep, get, isObject, mergeWith, uniq } from 'lodash-es';
import type { Writable } from 'type-fest';
import type { Id, MApp, MComponent, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
import { NodeType, Target, Watcher } from '@tmagic/core';
import { isFixed } from '@tmagic/stage';
import { calcValueByFontsize, getElById, getNodePath, isNumber, isPage, isPageFragment, isPop } from '@tmagic/utils';
import {
calcValueByFontsize,
getElById,
getNodeInfo,
getNodePath,
isNumber,
isPage,
isPageFragment,
isPop,
} from '@tmagic/utils';
import BaseService from '@editor/services//BaseService';
import propsService from '@editor/services//props';
@ -175,36 +184,7 @@ class Editor extends BaseService {
root = toRaw(root);
}
const info: EditorNodeInfo = {
node: null,
parent: null,
page: null,
};
if (!root) return info;
if (id === root.id) {
info.node = root;
return info;
}
const path = getNodePath(id, root.items);
if (!path.length) return info;
path.unshift(root);
info.node = path[path.length - 1] as MComponent;
info.parent = path[path.length - 2] as MContainer;
path.forEach((item) => {
if (isPage(item) || isPageFragment(item)) {
info.page = item as MPage | MPageFragment;
return;
}
});
return info;
return getNodeInfo(id, root);
}
/**
@ -586,11 +566,7 @@ class Editor extends BaseService {
nodes.splice(targetIndex, 1, newConfig);
this.set('nodes', [...nodes]);
this.get('stage')?.update({
config: cloneDeep(newConfig),
parentId: parent.id,
root: cloneDeep(root),
});
// update后会触发依赖收集收集完后会掉stage.update方法
if (isPage(newConfig) || isPageFragment(newConfig)) {
this.set('page', newConfig as MPage | MPageFragment);
@ -875,7 +851,11 @@ class Editor extends BaseService {
await stage.select(targetId);
const targetParent = this.getParentById(target.id);
await stage.update({ config: cloneDeep(target), parentId: targetParent?.id, root: cloneDeep(root) });
await stage.update({
config: cloneDeep(target),
parentId: targetParent?.id,
root: cloneDeep(root),
});
await this.select(newConfig);
stage.select(newConfig.id);

View File

@ -56,13 +56,14 @@ export class IdleTask<T = any> extends EventEmitter {
}
private runTaskQueue(deadline: IdleDeadline) {
while ((deadline.timeRemaining() > 15 || deadline.didTimeout) && this.taskList.length) {
// 动画会占用空闲时间,当任务一直无法执行时,看看是否有动画正在播放
while ((deadline.timeRemaining() > 10 || deadline.didTimeout) && this.taskList.length) {
const task = this.taskList.shift();
task!.handler(task!.data);
}
if (this.taskList.length) {
this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 10000 });
this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 300 });
} else {
this.taskHandle = 0;

View File

@ -18,9 +18,22 @@
import { cloneDeep, set as objectSet } from 'lodash-es';
import type { DataSchema, DataSourceDeps, Id, MComponent, MNode, MNodeInstance } from '@tmagic/schema';
import type {
DataSchema,
DataSourceDeps,
Id,
MApp,
MComponent,
MContainer,
MNode,
MNodeInstance,
MPage,
MPageFragment,
} from '@tmagic/schema';
import { NodeType } from '@tmagic/schema';
import type { EditorNodeInfo } from '@editor/type';
export * from './dom';
export const sleep = (ms: number): Promise<void> =>
@ -78,6 +91,39 @@ export const getNodePath = (id: Id, data: MNode[] = []): MNode[] => {
return path;
};
export const getNodeInfo = (id: Id, root: MApp | null) => {
const info: EditorNodeInfo = {
node: null,
parent: null,
page: null,
};
if (!root) return info;
if (id === root.id) {
info.node = root;
return info;
}
const path = getNodePath(id, root.items);
if (!path.length) return info;
path.unshift(root);
info.node = path[path.length - 1] as MComponent;
info.parent = path[path.length - 2] as MContainer;
path.forEach((item) => {
if (isPage(item) || isPageFragment(item)) {
info.page = item as MPage | MPageFragment;
return;
}
});
return info;
};
export const filterXSS = (str: string) =>
str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');