前段时间还写了关于大量节点dom渲染卡顿的排查分析。展示大量数据节点(tree),引发的一次性能排查
最近两个月做了一个需求, 大致功能如下
- 大量数据节点(每个用户有不同的数据量),并且需要展示清晰的层级结构信息。(涉及到大量dom渲染,页面卡顿)
- 每个数据节点中挂载多种数据资源(列表数据,截图,镜像,当前节点相关资源等等),需要进行tab切换展示。(涉及到数据状态保存)
- 需要进行搜索定位。(就是搜索列表数据,并且定位到对应的节点,进行数据回溯)(涉及到数据节点和数据表格联动)
- 支持数据导出,链表导出等等。(这个主要是后端功能)
需求功能预览
下面来看看需求的展示情况
- 数据分层预览
- 数据合并预览
- 数据搜索定位
- 修改数据节点折叠逻辑后预览
节点树形层级展示
当时这个功能查找了很多组件去实现,因为我们当前项目使用的是element-plus
,于是就去查看tree
相关组件,发现对于正常的tree组件他是可以进行懒加载请求数据的(展开节点时请求数据)。对于节点加载过多后,导致页面渲染很卡。对于tree-v2
它支持虚拟列表展示,但是不支持节点懒加载。对于大量数据来说显然是不合适的。
然后又查找了网上相关tree组件,发现都和element-plus
这两种相同。基本都是只支持虚拟列表展示。但是发现antd
却可以既支持懒加载又支持虚拟列表。就想到了使用antdv
了。所以下面总结下使用时遇到的问题和实现思路。
方便交互
为了让用户更容易操作,我们应该是点击节点就可以展开节点,而不是点击展开箭头再进行展开。所以我们需要使用DirectoryTree
组件。
<DirectoryTree
ref="treeRef"
:height="menuWrapperHeight"
:load-data="loadMoreMenu"
:tree-data="manualTaskList" // 进入页面时主动请求的数据赋值
:fieldNames="fieldNames"
:checkable="store.previewData.allManualTaskCount !== 1" // 是否展示复选框(可根据css样式选择性展示)
v-model:checkedKeys="checkedKeys"
v-model:selectedKeys="currentNodeKey"
:expanded-keys="saveExpandedKeys"
:loaded-keys="loadedKeys"
blockNode
checkStrictly
autoExpandParent
@check="selectManualTaskNode"
@select="handleNodeClick"
@load="handleLoaded"
>
<template #title="{ name, nid }">
<span :class="`node-item-${nid}`" :title="name">{{ name }}</span>
</template>
<template #switcherIcon="data">
<component :is="data.defaultIcon" @click.stop="handleIcon(data)" />
</template>
</DirectoryTree>
如果想要使用虚拟列表,我们需要制定展示区域的高度,这里我是动态计算的高度。
// 设置菜单虚拟高度
const setMenuHeight = () => {
menuWrapper = document.querySelector(".tree-menu-wrapper");
menuWrapperHeight.value = menuWrapper?.clientHeight || 600;
};
加载数据
这里主要是将请求的数据赋值给对应节点响应式对象node.dataRef!.children
。
// 加载数据
const loadMoreMenu: TreeProps["loadData"] = async (node) => {
return new Promise<void>(async (resolve) => {
// 请求数据
let data = await requestManualNodeData(node.nid, node.dataRef);
if (!node.dataRef?.children?.length) {
node.dataRef!.children = data as any;
}
resolve();
});
};
选中节点
因为这里我们有两个树节点需要同步节点选中状态,所以我们需要将选中状态同步到全局store中。
// 选中节点
const selectManualTaskNode: TreeProps["onCheck"] = async (
checkedKeys: any,
{ node }
) => {
// 保存选中的节点
const currentSelectTasks = store.previewData.currentSelectTasks;
const currentSelectTaskNIds = currentSelectTasks.map((item) => item.nid);
if (node.node_type === "manualTask") {
if (!currentSelectTaskNIds.includes(node.nid)) {
await store.previewData.SET_CURRENT_SELECT_MANUAL_TASK([
...currentSelectTasks,
{
nid: node.nid,
name: node.name,
id: node.id,
},
]);
} else {
const currentTaskIndex = currentSelectTasks.findIndex(
(item) => item.nid === node.nid
);
currentSelectTasks.splice(currentTaskIndex, 1);
await store.previewData.SET_CURRENT_SELECT_MANUAL_TASK(
currentSelectTasks
);
}
}
};
// 联动分组和全部数据选中状态(切换tab同步)
watch(
() => props.isAllDataPreview,
(flag) => {
const currentSelectTasks = store.previewData.currentSelectTasks;
// 赋值选中状态
!flag && (checkedKeys.value = currentSelectTasks.map((item) => item.nid));
},
{
immediate: true,
}
);
点击节点
这里需要注意,我们点击节点会触发节点请求和点击回调。所以说如果想要和父组件通信传递数据,我们需要在请求的函数中也进行emit。在点击回调函数中判断node.loaded
当前是否加载过来阻止emit无效处理一次事件。这两个地方处理的事件逻辑都是一样的。主要是在点击回调函数中是用于后续非第一次点击时进行emit的。
// 点击节点(以后点击)
const handleNodeClick: TreeProps["onSelect"] = async (
selectedKeys: any,
{ node }
) => {
const key = node.key as number;
// TODO: 这里是为了处理当前节点收起逻辑的
// expanded属性表示的是点击之前的状态
const _keys = getExpandedKeys(node);
_keys && (saveExpandedKeys.value = _keys);
saveExpandedStatus(key);
// 重置点击图表的状态
isClickIconExpand.value = false;
node.loaded && emit() // 做一些事情
};
节点加载
这个回调主要是用于后续数据定位使用,来让每次节点加载都进行下一次节点滚动到对应的位置进行后续节点加载。因为这个是虚拟列表。
// 数据加载完毕
const handleLoaded: TreeProps["onLoad"] = (keys, { node }) => {
loadedKeys.value = keys as any;
// 数据定位
if (isSearchPosition.value) {
// 在定位节点的树节点id集合中查找当前加载节点的索引
const findIndex = saveExpandedKeys.value.findIndex(
(item) => item === node.nid
);
const scrollNId = saveExpandedKeys.value[findIndex + 1];
// 拿到索引下一个节点的唯一标识
scrollNId !== undefined && gotoLevelData(scrollNId);
}
};
// 定位数据行
const gotoLevelData = (nid: number) => {
nextTick(async () => {
treeRef.value?.scrollTo({
key: nid,
});
});
};
节点展示收起优化
- 手风琴模式
// 获取当前操作节点链路id
const getExpandedKeys = (node: any): number[] => {
let resultKeys: number[] = [];
let currentNode = node; // 不加入当前节点
// 遍历当前节点知道节点parent为null,获取节点标识
while ((currentNode = currentNode.parent)) {
resultKeys.unshift(currentNode.key as number);
}
return resultKeys;
};
// 节点展示或者收起
const handleNodeExpandOrCollapse: TreeProps["onExpand"] = (
expandedKeys: any,
{ expanded, node }
) => {
const _keys = getExpandedKeys(node);
if (expanded) {
// 展开需要加上当前节点
_keys.push(+node.key);
}
saveExpandedKeys.value = _keys;
// 保存节点展开状态,刷新时自动展开上次点击的位置
store.previewData.SAVE_EXPANDED_KEYS(saveExpandedKeys.value);
store.previewData.SET_CURRENT_NODE_BY_LEVEL(+node.key);
};
- 展示节点任何位置,收起点击图标 由于我们需要点击不同节点查看对应的节点中挂载的内容,所以当节点展开时不能进行数据点击后合并,不然对于我们产品来说体验不好。
但是对于antv
来说,他的理念是让我们可以很容易通过逻辑去处理我们想要的功能,就比较容易去实现。
- 首先需要定义图标插槽,因为组件内部没有提供事件供我们使用。
<template #switcherIcon="data">
<component :is="data.defaultIcon" @click.stop="handleIcon(data)" />
</template>
- 由于我们将图标绑定了我们自己的事件,所以我们需要来处理节点数据请求的回调,来保存我们已经加载的节点。
// 数据加载完毕
const handleLoaded: TreeProps["onLoad"] = (keys, { node }) => {
loadedKeys.value = keys as any;
};
- 定义图标点击事件
- 获取图标节点,主动加载数据,保存当前节点到加载数组和展开数组中。
- 展开时点击我们将当前节点标识在展开数组中删除,收起时点击我们直接获取节点元素进行点击即可。
// 点击图标
const handleIcon = async (data: any) => {
// 获取点击的元素触发点击即可。
const currentTreeItem = document.querySelector<HTMLElement>(
`span.node-item-${data.nid}`
);
if (!loadedKeys.value.includes(data.nid)) {
// currentTreeItem?.click();
await loadMoreMenu(data);
// 加载并展开
loadedKeys.value = [...loadedKeys.value, data.nid];
saveExpandedKeys.value = [...saveExpandedKeys.value, data.nid];
}
if (data.expanded) {// 点击前为展开状态
const findIndex = saveExpandedKeys.value.findIndex(
(item) => item === data.nid
);
findIndex !== -1 &&
(saveExpandedKeys.value = saveExpandedKeys.value.slice(0, findIndex));
} else {
isClickIconExpand.value = true;
currentTreeItem?.click();
}
};
- 我们不能在监听
expand
事件,我们需要在点击节点的回调中进行节点展开的收集。
// 获取当前操作节点链路id
const getExpandedKeys = (node: any): number[] | undefined => {
// 当点击的不是展开图标 isClickIconExpand我们在每次点击图标时赋值为true,在点击节点时重置为false
if (saveExpandedKeys.value.includes(node.key) && !isClickIconExpand.value) {
return;
}
// 实现手风琴,收集当前点击节点的链路标识
const resultKeys = [node.key];
let currentNode = node;
while ((currentNode = currentNode.parent)) {
resultKeys.push(currentNode.key);
}
return resultKeys.sort((a, b) => a - b);
};
// 点击节点(以后点击)
const handleNodeClick: TreeProps["onSelect"] = async (
selectedKeys: any,
{ node }
) => {
const key = node.key as number;
// expanded属性表示的是点击之前的状态
const _keys = getExpandedKeys(node);
_keys && (saveExpandedKeys.value = _keys);
// 重置点击图标的状态
isClickIconExpand.value = false;
};
今天太晚了(2024-8-18 1:03),后续在总结ag-grid
表格组件的使用注意点。
往期年度总结
往期文章
- 如何从0开始认识m3u8(提取,解析及下载)
- 展示大量数据节点(tree),引发的一次性能排查
- ts装饰器的那点东西
- 这是你所知道的ts类型断言和类型守卫吗?
- TypeScript官网内容解读
- 经常使用ts的你,知道这些内容?
- 你有了解过原生css的scope?
- 现在比较常用的移动端调试你知道哪些?
- 众多跨标签页通信方式,你知道哪些?(二)
- 众多跨标签页通信方式,你知道哪些?
- 反调试吗?如何监听devtools的打开与关闭
- 因为原生,选择一家公司(前端如何防笔试作弊)
- 结合开发,带你熟悉package.json与tsconfig.json配置
- 如何优雅的在项目中使用echarts
- 如何优雅的做项目国际化
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )
专栏文章
🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论, 支持一下博主~
公众号:全栈追逐者,不定期的更新内容,关注不错过哦!