fix(editor): 丢弃过期 root 快照,避免画布回退与 modelValue 回写死循环

This commit is contained in:
roymondchen 2026-09-07 19:00:53 +08:00
parent 7edb01d330
commit 66b54a52f8
2 changed files with 65 additions and 7 deletions

View File

@ -427,12 +427,26 @@ export const initServiceEvents = (
});
};
/**
* root root
*
* `editorService.set('root', v)` `root-change` `v`
* root `root-change` stage / runtime / root
* DSL root `v`
* root
*/
const isStaleRoot = (value: MApp | null) => toRaw(editorService.get('root')) !== toRaw(value);
const updateStageDsl = async (value: MApp | null) => {
const stage = await getStage();
const runtime = await stage.renderer?.getRuntime();
const app = await getTMagicApp();
// 等 stage / runtime 就绪期间 root 已被替换:新 root 的刷新可能已经完成,
// 再把旧 dsl 推给 runtime 会让画布回退到旧内容
if (isStaleRoot(value)) return;
if (!app?.dataSourceManager) {
runtime?.updateRootConfig?.(cloneDeep(toRaw(value))!);
}
@ -449,6 +463,8 @@ export const initServiceEvents = (
await (typeof Worker === 'undefined' ? collectIdle(value.items, true) : depService.collectByWorker(value));
if (isStaleRoot(value)) return;
const dsl = cloneDeep(toRaw(value));
if (dsl.dataSources && dsl.dataSourceDeps && app?.dataSourceManager) {
for (const node of getNodes(getDepNodeIds(dsl.dataSourceDeps), dsl.items)) {
@ -466,7 +482,7 @@ export const initServiceEvents = (
depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
};
const rootChangeHandler = (value: MApp | null, preValue?: MApp | null) => {
const rootChangeHandler = (value: MApp | null) => {
if (!value) return;
value.codeBlocks = value.codeBlocks || {};
@ -509,7 +525,13 @@ export const initServiceEvents = (
editorService.set('page', null);
}
if (toRaw(value) !== toRaw(preValue)) {
// 上面的 select 是异步的,期间 root 可能已被替换,过期快照不能再回写给外部:
// 两次整体替换各自持有一个快照时,回写会把对方的 root 顶掉,外部 modelValue 变化又会
// 重新 set root两条链路无休止地交替下去表现为编辑器卡死
if (isStaleRoot(value)) return;
// 外部已经持有这个 root如本次变化就是 modelValue 传进来的)时无需回写
if (toRaw(props.modelValue) !== toRaw(value)) {
emit('update:modelValue', value);
}
})();

View File

@ -357,7 +357,6 @@ describe('initServiceEvents', () => {
});
test('rootChange 处理代码块和数据源', async () => {
services.editorService.state.root = { id: 'r' };
mount(WrapEvents({} as any, emit, services));
const value: any = {
id: 'r',
@ -365,6 +364,8 @@ describe('initServiceEvents', () => {
dataSources: [{ id: 'd1', type: 'base' }],
items: [],
};
// set('root', v) 是先赋值再派发事件mock 中同样先对齐 state 再 emit
services.editorService.state.root = value;
services.editorService.emit('root-change', value, null);
await new Promise((r) => setTimeout(r, 0));
expect(services.codeBlockService.setCodeDsl).toHaveBeenCalled();
@ -593,13 +594,15 @@ describe('initServiceEvents', () => {
services.editorService.state.node = { id: 'n1' };
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('root-change', {
const value: any = {
id: 'r',
items: [{ id: 'n1', type: 'text' }],
dataSources: [],
dataSourceDeps: { d1: {} },
codeBlocks: {},
});
};
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 10));
expect(stage.runtime.updatePageId).toHaveBeenCalledWith('p1');
@ -608,10 +611,10 @@ describe('initServiceEvents', () => {
});
test('rootChange items 不是数组时清空依赖', async () => {
services.editorService.state.root = { id: 'r' };
mount(WrapEvents({} as any, emit, services));
const value: any = { id: 'r', dataSourceDeps: { a: {} }, dataSourceCondDeps: { b: {} } };
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 0));
@ -625,6 +628,7 @@ describe('initServiceEvents', () => {
mount(WrapEvents({} as any, emit, services));
const value: any = { id: 'r', items: [] };
services.editorService.state.root = value;
services.editorService.emit('root-change', value, { id: 'prev' });
await new Promise((r) => setTimeout(r, 0));
@ -638,12 +642,44 @@ describe('initServiceEvents', () => {
services.editorService.getNodeById.mockReturnValue(null);
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('root-change', { id: 'r', items: [{ id: 'first', type: 'page' }] });
const value: any = { id: 'r', items: [{ id: 'first', type: 'page' }] };
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 0));
expect(services.editorService.select).toHaveBeenCalledWith({ id: 'first', type: 'page' });
});
test('rootChange 外部已持有该 root 时不回写 modelValue', async () => {
services.editorService.getNodeById.mockReturnValue(null);
const value: any = { id: 'r', items: [] };
mount(WrapEvents({ modelValue: value } as any, emit, services));
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 0));
expect(emit).not.toHaveBeenCalled();
});
test('rootChange 处理期间 root 被替换:不刷旧 dsl 也不回写过期快照', async () => {
const app: any = { dsl: {}, dataSourceManager: mkDataSourceManager() };
const stage = mkReadyStage(app);
services.editorService.state.stage = stage;
services.editorService.getNodeById.mockReturnValue(null);
mount(WrapEvents({} as any, emit, services));
const value: any = { id: 'r', items: [{ id: 'n1', type: 'text' }], dataSources: [], codeBlocks: {} };
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
// 异步处理还没跑完root 已被新的一次整体替换顶掉
services.editorService.state.root = { id: 'r', items: [], dataSources: [], codeBlocks: {} };
await new Promise((r) => setTimeout(r, 10));
expect(stage.runtime.updateRootConfig).not.toHaveBeenCalled();
expect(emit).not.toHaveBeenCalled();
});
test('update 事件ROOT 节点、无 propPath、命中已收集依赖三种分支', async () => {
services.editorService.state.root = { id: 'r', items: [] };
services.depService.getTargets.mockReturnValue({