mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-11-01 07:20:39 +00:00
perf(data-source): 迭代器容器数据上下文改为惰性深拷贝
createIteratorContentData 原先整体深拷贝数据源数据,迭代项很多时为 O(N^2)。 改为沿 fields 路径逐层构造新对象,路径之外的对象型字段在首次读取时才拷贝并缓存, 既避免修改原始数据,也保证引用变化可被渲染层感知
This commit is contained in:
parent
6f2ffc3ca1
commit
35ed2cc0d0
@ -75,6 +75,45 @@ export const updateNode = (node: MNode, dsl: MApp) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 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 迭代数据
|
||||
@ -93,24 +132,26 @@ export const createIteratorContentData = (
|
||||
[dsId]: {},
|
||||
};
|
||||
|
||||
let rawData = cloneDeep(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) => {
|
||||
Object.assign(obj, rawData);
|
||||
assignLazyClone(obj, rawData, key);
|
||||
|
||||
if (index === fields.length - 1) {
|
||||
obj[key] = itemData;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(rawData[key])) {
|
||||
rawData[key] = {};
|
||||
obj[key] = {};
|
||||
}
|
||||
const next = rawData?.[key];
|
||||
const nextObj: Record<string, any> = {};
|
||||
obj[key] = nextObj;
|
||||
|
||||
rawData = rawData[key];
|
||||
obj = obj[key];
|
||||
rawData = Array.isArray(next) ? undefined : next;
|
||||
obj = nextObj;
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
156
packages/data-source/tests/iterator-reactivity.spec.ts
Normal file
156
packages/data-source/tests/iterator-reactivity.spec.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { afterAll, describe, expect, test } from 'vitest';
|
||||
import { computed, createApp, defineComponent, h, nextTick, ref } from 'vue';
|
||||
|
||||
import TMagicApp, { NodeType } from '@tmagic/core';
|
||||
|
||||
import { DataSourceManager } from '@data-source/index';
|
||||
|
||||
/**
|
||||
* 模拟 IteratorContainer.vue:
|
||||
* configs 是依赖 props.config 的 computed,内部调用 dataSourceManager.compliedIteratorItems,
|
||||
* 每项渲染一个 Item,Item 把绑定到的“兄弟字段对象”作为 prop 继续传给 Cell。
|
||||
*/
|
||||
const setup = () => {
|
||||
const dsId = 'ds_iter_reactivity';
|
||||
const app = new TMagicApp({
|
||||
config: {
|
||||
type: NodeType.ROOT,
|
||||
id: 'app_iter_reactivity',
|
||||
items: [],
|
||||
dataSources: [
|
||||
{
|
||||
type: 'base',
|
||||
id: dsId,
|
||||
fields: [
|
||||
{
|
||||
name: 'list',
|
||||
type: 'array',
|
||||
fields: [{ name: 'label' }],
|
||||
defaultValue: [{ label: 'A' }, { label: 'B' }],
|
||||
},
|
||||
{ name: 'matrix', type: 'array', fields: [{ name: 'v' }], defaultValue: [{ v: 1 }] },
|
||||
],
|
||||
methods: [],
|
||||
events: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const dsm = new DataSourceManager({ app });
|
||||
const ds = dsm.get(dsId)!;
|
||||
|
||||
const rawMatrix = dsm.data[dsId].matrix;
|
||||
|
||||
const templateNodes = [
|
||||
{
|
||||
id: 'iter_reactivity_node',
|
||||
type: 'text',
|
||||
text: '${ds_iter_reactivity.list.label}',
|
||||
// 绑定整个数据源,编译结果是迭代上下文(matrix 是迭代路径之外的兄弟字段)
|
||||
cell: { isBindDataSource: true, dataSourceId: dsId },
|
||||
},
|
||||
] as any[];
|
||||
|
||||
const renderCount = { cell: 0, item: 0 };
|
||||
|
||||
const cellComp = defineComponent({
|
||||
props: { matrix: { type: Array, required: true } },
|
||||
setup(props) {
|
||||
return () => {
|
||||
renderCount.cell += 1;
|
||||
return h('i', { class: 'cell' }, String((props.matrix[0] as any).v));
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const itemComp = defineComponent({
|
||||
props: { config: { type: Object, required: true } },
|
||||
setup(props) {
|
||||
return () => {
|
||||
renderCount.item += 1;
|
||||
const node = (props.config as any).items[0];
|
||||
return h('div', { class: 'item' }, [h('b', node.text), h(cellComp, { matrix: node.cell.matrix })]);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// 与 IteratorContainer.vue 一样:数据源变化 -> 节点被替换(config 变成新对象) -> computed 重算
|
||||
const containerConfig = ref<any>({ iteratorData: ds.data.list, items: templateNodes });
|
||||
|
||||
const containerComp = defineComponent({
|
||||
setup() {
|
||||
const configs = computed(() =>
|
||||
(containerConfig.value.iteratorData as any[]).map((itemData) => ({
|
||||
items: dsm.compliedIteratorItems(itemData, containerConfig.value.items, [dsId, 'list']),
|
||||
})),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
configs.value.map((config, index) => h(itemComp, { key: index, config })),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const root = document.createElement('div');
|
||||
const vueApp = createApp(containerComp);
|
||||
vueApp.mount(root);
|
||||
|
||||
// 模拟 createDataSourceManager 的 change 处理:触发容器节点被整体替换
|
||||
const replaceContainerNode = () => {
|
||||
containerConfig.value = { iteratorData: ds.data.list, items: templateNodes };
|
||||
};
|
||||
|
||||
return { ds, dsm, root, renderCount, rawMatrix, replaceContainerNode, unmount: () => vueApp.unmount() };
|
||||
};
|
||||
|
||||
describe('迭代容器编译 + Vue 响应式', () => {
|
||||
afterAll(() => {
|
||||
DataSourceManager.clearDataSourceClass();
|
||||
});
|
||||
|
||||
test('路径之外的兄弟字段是与数据源隔离的快照,不修改数据源原始数据', async () => {
|
||||
const { dsm, root, rawMatrix, unmount } = setup();
|
||||
await nextTick();
|
||||
|
||||
expect(root.querySelectorAll('.item').length).toBe(2);
|
||||
expect(root.querySelector('.cell')?.textContent).toBe('1');
|
||||
expect(rawMatrix).toEqual([{ v: 1 }]);
|
||||
expect(dsm.data.ds_iter_reactivity.matrix).toBe(rawMatrix);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('整体替换数据源数据(setData 无 path)后,视图和子孙组件都会更新', async () => {
|
||||
const { ds, root, replaceContainerNode, unmount } = setup();
|
||||
await nextTick();
|
||||
|
||||
ds.setData([{ v: 2 }], 'matrix');
|
||||
replaceContainerNode();
|
||||
await nextTick();
|
||||
|
||||
expect(root.querySelector('.cell')?.textContent).toBe('2');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('按 path 原地修改兄弟字段对象内部值后,子孙组件也会更新', async () => {
|
||||
const { ds, root, renderCount, replaceContainerNode, unmount } = setup();
|
||||
await nextTick();
|
||||
const cellRenderBefore = renderCount.cell;
|
||||
|
||||
// SimpleObservedData 按路径更新为原地修改,matrix[0] 的引用不变
|
||||
ds.setData(9, 'matrix.0.v');
|
||||
replaceContainerNode();
|
||||
await nextTick();
|
||||
|
||||
// Item 一定会因 config 换新对象而重新渲染
|
||||
expect(renderCount.item).toBeGreaterThan(2);
|
||||
// 兄弟字段是新的快照,子孙组件的 props 会变化并拿到最新值
|
||||
expect(renderCount.cell).toBeGreaterThan(cellRenderBefore);
|
||||
expect(root.querySelector('.cell')?.textContent).toBe('9');
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@ -193,6 +193,63 @@ describe('createIteratorContentData', () => {
|
||||
});
|
||||
expect(ctxData.ds.a.c.a).toBe(1);
|
||||
});
|
||||
|
||||
test('不修改原始数据源数据', () => {
|
||||
const dsData: any = {
|
||||
ds: {
|
||||
a: [{ b: 0, c: [{ a: 1 }] }],
|
||||
obj: { x: { y: 1 }, z: 2 },
|
||||
b: 2,
|
||||
},
|
||||
};
|
||||
const snapshot = JSON.parse(JSON.stringify(dsData));
|
||||
|
||||
const ctxData: any = createIteratorContentData({ a: 1 }, 'ds', ['a', 'c'], dsData);
|
||||
expect(ctxData.ds.a.c).toEqual({ a: 1 });
|
||||
|
||||
const objCtx: any = createIteratorContentData({ n: 1 }, 'ds', ['obj', 'x'], dsData);
|
||||
expect(objCtx.ds.obj.x).toEqual({ n: 1 });
|
||||
expect(objCtx.ds.obj.z).toBe(2);
|
||||
expect(objCtx.ds.b).toBe(2);
|
||||
|
||||
expect(dsData).toEqual(snapshot);
|
||||
expect(ctxData.ds).not.toBe(dsData.ds);
|
||||
expect(objCtx.ds.obj).not.toBe(dsData.ds.obj);
|
||||
});
|
||||
|
||||
test('路径之外的对象型字段惰性深拷贝:与原数据隔离、多次读取引用稳定、可被覆盖', () => {
|
||||
const dsData: any = { ds: { a: [{ b: 1 }], matrix: [{ v: 1 }], n: 1 } };
|
||||
|
||||
const ctxData: any = createIteratorContentData({ b: 1 }, 'ds', ['a'], dsData);
|
||||
|
||||
expect(ctxData.ds.n).toBe(1);
|
||||
const { matrix } = ctxData.ds;
|
||||
expect(matrix).toEqual([{ v: 1 }]);
|
||||
expect(matrix).not.toBe(dsData.ds.matrix);
|
||||
expect(matrix[0]).not.toBe(dsData.ds.matrix[0]);
|
||||
expect(ctxData.ds.matrix).toBe(matrix);
|
||||
|
||||
// 修改拷贝不影响原数据,原地修改原数据也不影响已读取过的快照
|
||||
matrix[0].v = 2;
|
||||
expect(dsData.ds.matrix[0].v).toBe(1);
|
||||
dsData.ds.matrix[0].v = 3;
|
||||
expect(ctxData.ds.matrix[0].v).toBe(2);
|
||||
|
||||
// 每次创建的上下文拿到的都是新的引用
|
||||
expect(createIteratorContentData({ b: 1 }, 'ds', ['a'], dsData).ds).not.toBe(ctxData.ds);
|
||||
expect((createIteratorContentData({ b: 1 }, 'ds', ['a'], dsData) as any).ds.matrix).not.toBe(dsData.ds.matrix);
|
||||
|
||||
// 支持覆盖,对象展开可枚举
|
||||
ctxData.ds.matrix = 'x';
|
||||
expect(ctxData.ds.matrix).toBe('x');
|
||||
expect(Object.keys(ctxData.ds)).toEqual(expect.arrayContaining(['a', 'matrix', 'n']));
|
||||
});
|
||||
|
||||
test('数据源数据或路径中间层缺失时不抛错', () => {
|
||||
expect((createIteratorContentData({ a: 1 }, 'ds', ['a', 'c'], {}) as any).ds.a.c).toEqual({ a: 1 });
|
||||
expect((createIteratorContentData({ a: 1 }, 'ds', ['a', 'c'], { ds: {} }) as any).ds.a.c).toEqual({ a: 1 });
|
||||
expect((createIteratorContentData({ a: 1 }, 'ds', [], { ds: { b: 1 } }) as any).ds).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('compliedConditions', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user