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

157 lines
5.0 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 { 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();
});
});