mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-10 09:22:00 +00:00
perf(editor): 优化节点信息查找性能
This commit is contained in:
parent
48519b0155
commit
c4ec2c5c72
@ -217,7 +217,30 @@ class Editor extends BaseService {
|
||||
root = toRaw(root);
|
||||
}
|
||||
|
||||
return getNodeInfo(id, root);
|
||||
if (!root) {
|
||||
return { node: null, parent: null, page: null };
|
||||
}
|
||||
|
||||
if (id === root.id) {
|
||||
return { node: root, parent: null, page: null };
|
||||
}
|
||||
|
||||
// 大多数查找的目标都在当前页面内,优先在当前页面子树中查找以避免对整棵树做全量遍历。
|
||||
// 注意:不能直接使用 state.page,它可能与当前 root 不同步(指向已脱离的旧页面对象),
|
||||
// 因此仅借用其 id,再从当前 root 中取回真正的页面对象(页面均为 root 的直接子节点,数量很少)。
|
||||
const pageIdStr = `${this.get('page')?.id || ''}`;
|
||||
const currentPageNode = root.items?.find((item) => `${item.id}` === pageIdStr);
|
||||
if (currentPageNode && `${id}` !== pageIdStr) {
|
||||
// util 仅读取 root.id 与 root.items,按容器结构传入当前页面是安全的
|
||||
const info = getNodeInfo(id, currentPageNode);
|
||||
if (info.node) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:在完整 root 上查找;当前页面已搜索过,用 skip 跳过其子树避免重复遍历,
|
||||
// 同时保留真实的 parent / page 引用(id 命中当前页面节点本身时会在跳过子树前先匹配到)
|
||||
return getNodeInfo(id, root, currentPageNode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -190,6 +190,93 @@ describe('getParentById', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNodeInfo 当前页面优先 / 跨页面回退', () => {
|
||||
// 两个页面,page2 内含一个容器及其子节点,用于覆盖「优先当前页面、回退跳过当前页面」逻辑
|
||||
const PAGE2_ID = 20;
|
||||
const NODE_IN_PAGE2 = 21;
|
||||
const CONTAINER_IN_PAGE2 = 22;
|
||||
const CHILD_IN_PAGE2 = 23;
|
||||
const multiPageRoot: MApp = {
|
||||
id: NodeId.ROOT_ID,
|
||||
type: NodeType.ROOT,
|
||||
items: [
|
||||
cloneDeep(root.items[0]),
|
||||
{
|
||||
id: PAGE2_ID,
|
||||
type: NodeType.PAGE,
|
||||
layout: 'absolute',
|
||||
style: { width: 375 },
|
||||
items: [
|
||||
{ id: NODE_IN_PAGE2, type: 'text', style: {} },
|
||||
{
|
||||
id: CONTAINER_IN_PAGE2,
|
||||
type: 'container',
|
||||
style: {},
|
||||
items: [{ id: CHILD_IN_PAGE2, type: 'text', style: {} }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
editorService.set('root', cloneDeep(multiPageRoot));
|
||||
// 当前停留在 page1
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
});
|
||||
|
||||
test('id 为 root.id 时返回 root 自身,parent / page 为 null', () => {
|
||||
const info = editorService.getNodeInfo(NodeId.ROOT_ID);
|
||||
expect(info.node?.id).toBe(NodeId.ROOT_ID);
|
||||
expect(info.parent).toBeNull();
|
||||
expect(info.page).toBeNull();
|
||||
});
|
||||
|
||||
test('当前页面节点本身:node 为页面、parent 为 root、page 为页面自身', () => {
|
||||
const info = editorService.getNodeInfo(NodeId.PAGE_ID);
|
||||
expect(info.node?.id).toBe(NodeId.PAGE_ID);
|
||||
expect(info.parent?.id).toBe(NodeId.ROOT_ID);
|
||||
expect(info.page?.id).toBe(NodeId.PAGE_ID);
|
||||
});
|
||||
|
||||
test('命中当前页面内的节点(快速路径)', () => {
|
||||
const info = editorService.getNodeInfo(NodeId.NODE_ID);
|
||||
expect(info.node?.id).toBe(NodeId.NODE_ID);
|
||||
expect(info.parent?.id).toBe(NodeId.PAGE_ID);
|
||||
expect(info.page?.id).toBe(NodeId.PAGE_ID);
|
||||
});
|
||||
|
||||
test('命中非当前页面内的深层节点(回退跳过当前页面),parent / page 正确', () => {
|
||||
const info = editorService.getNodeInfo(CHILD_IN_PAGE2);
|
||||
expect(info.node?.id).toBe(CHILD_IN_PAGE2);
|
||||
expect(info.parent?.id).toBe(CONTAINER_IN_PAGE2);
|
||||
expect(info.page?.id).toBe(PAGE2_ID);
|
||||
});
|
||||
|
||||
test('非当前页面的页面节点:parent 为真实 root(同一引用,可安全 mutate)', () => {
|
||||
const info = editorService.getNodeInfo(PAGE2_ID, false);
|
||||
expect(info.node?.id).toBe(PAGE2_ID);
|
||||
expect(info.page?.id).toBe(PAGE2_ID);
|
||||
// parent 必须是真实 root 引用,而非临时副本,否则对页面增删/排序会改不到真实树
|
||||
expect(info.parent).toBe(editorService.get('root'));
|
||||
});
|
||||
|
||||
test('不存在的节点返回空 info', () => {
|
||||
const info = editorService.getNodeInfo(NodeId.ERROR_NODE_ID);
|
||||
expect(info.node).toBeNull();
|
||||
expect(info.page).toBeNull();
|
||||
});
|
||||
|
||||
test('未选中任何页面时仍能跨页面查找到节点', () => {
|
||||
editorService.set('root', cloneDeep(multiPageRoot));
|
||||
editorService.set('page', null);
|
||||
const info = editorService.getNodeInfo(CHILD_IN_PAGE2);
|
||||
expect(info.node?.id).toBe(CHILD_IN_PAGE2);
|
||||
expect(info.parent?.id).toBe(CONTAINER_IN_PAGE2);
|
||||
expect(info.page?.id).toBe(PAGE2_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOnDifferentPage', () => {
|
||||
test('当前未选中任何页面时返回 false', () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
|
||||
@ -23,7 +23,6 @@ import type {
|
||||
DataSchema,
|
||||
DataSourceDeps,
|
||||
Id,
|
||||
MApp,
|
||||
MComponent,
|
||||
MContainer,
|
||||
MNode,
|
||||
@ -80,10 +79,12 @@ export const emptyFn = (): any => undefined;
|
||||
* @param {Array} data 要查找的根容器节点
|
||||
* @return {Array} 组件在data中的子孙路径
|
||||
*/
|
||||
export const getNodePath = (id: Id, data: MNode[] = []): MNode[] => {
|
||||
export const getNodePath = (id: Id, data: MNode[] = [], skip?: MNode | null): MNode[] => {
|
||||
const path: MNode[] = [];
|
||||
// 目标 id 字符串只计算一次,避免在递归遍历中对每个节点重复拼接
|
||||
const targetId = `${id}`;
|
||||
|
||||
const get = function (id: number | string, data: MNode[]): MNode | null {
|
||||
const get = function (data: MNode[]): MNode | null {
|
||||
if (!Array.isArray(data)) {
|
||||
return null;
|
||||
}
|
||||
@ -92,12 +93,13 @@ export const getNodePath = (id: Id, data: MNode[] = []): MNode[] => {
|
||||
const item = data[i];
|
||||
|
||||
path.push(item);
|
||||
if (`${item.id}` === `${id}`) {
|
||||
if (`${item.id}` === targetId) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
const node = get(id, item.items);
|
||||
// skip 用于跳过已经搜索过的子树(如已优先搜索过的当前页面),避免重复遍历
|
||||
if (item.items && item !== skip) {
|
||||
const node = get(item.items);
|
||||
if (node) {
|
||||
return node;
|
||||
}
|
||||
@ -109,12 +111,12 @@ export const getNodePath = (id: Id, data: MNode[] = []): MNode[] => {
|
||||
return null;
|
||||
};
|
||||
|
||||
get(id, data);
|
||||
get(data);
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getNodeInfo = (id: Id, root: Pick<MApp, 'id' | 'items'> | null) => {
|
||||
export const getNodeInfo = (id: Id, root: { id: Id; items?: MNode[] } | null, skip?: MNode | null) => {
|
||||
const info: EditorNodeInfo = {
|
||||
node: null,
|
||||
parent: null,
|
||||
@ -128,7 +130,7 @@ export const getNodeInfo = (id: Id, root: Pick<MApp, 'id' | 'items'> | null) =>
|
||||
return info;
|
||||
}
|
||||
|
||||
const path = getNodePath(id, root.items);
|
||||
const path = getNodePath(id, root.items, skip);
|
||||
|
||||
if (!path.length) return info;
|
||||
|
||||
@ -137,12 +139,12 @@ export const getNodeInfo = (id: Id, root: Pick<MApp, 'id' | 'items'> | null) =>
|
||||
info.node = path[path.length - 1] as MComponent;
|
||||
info.parent = path[path.length - 2] as MContainer;
|
||||
|
||||
path.forEach((item) => {
|
||||
for (const item of path) {
|
||||
if (isPage(item) || isPageFragment(item)) {
|
||||
info.page = item as MPage | MPageFragment;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
@ -248,6 +248,27 @@ describe('getNodeInfo', () => {
|
||||
const info = getNodeInfo('foo', null);
|
||||
expect(info.node).toBeNull();
|
||||
});
|
||||
|
||||
test('skip 跳过指定页面子树后查找不到其内部节点', () => {
|
||||
const multiRoot = {
|
||||
id: 'app',
|
||||
items: [
|
||||
{ id: 'page_1', type: NodeType.PAGE, items: [{ id: 'btn_1', type: 'button' }] },
|
||||
{ id: 'page_2', type: NodeType.PAGE, items: [{ id: 'btn_2', type: 'button' }] },
|
||||
],
|
||||
} as any;
|
||||
|
||||
// 跳过 page_1 后,page_1 内的节点查不到
|
||||
const skipped = getNodeInfo('btn_1', multiRoot, multiRoot.items[0]);
|
||||
expect(skipped.node).toBeNull();
|
||||
|
||||
// 其它页面节点不受影响,parent / page 仍为真实引用
|
||||
const info = getNodeInfo('btn_2', multiRoot, multiRoot.items[0]);
|
||||
expect(info.node?.id).toBe('btn_2');
|
||||
expect(info.parent?.id).toBe('page_2');
|
||||
expect(info.page?.id).toBe('page_2');
|
||||
expect(info.parent).toBe(multiRoot.items[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValueByKeyPath / getKeys', () => {
|
||||
|
||||
@ -201,6 +201,22 @@ describe('getNodePath', () => {
|
||||
expect(path).toHaveLength(0);
|
||||
expect(path2).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('skip 跳过指定子树后查找不到其内部节点', () => {
|
||||
// 跳过 id 为 2 的容器,其子树(22 / 222)不再被遍历
|
||||
const skipped = util.getNodePath(222, root, root[1]);
|
||||
expect(skipped).toHaveLength(0);
|
||||
// 其它子树不受影响
|
||||
const path = util.getNodePath(111, root, root[1]);
|
||||
expect(path).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('skip 不影响对被跳过节点自身的匹配', () => {
|
||||
// skip 仅阻止递归进入其 items,节点自身仍可被匹配到
|
||||
const path = util.getNodePath(2, root, root[1]);
|
||||
expect(path).toHaveLength(1);
|
||||
expect(path[0].id).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterXSS', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user