在vue项目中,假如接口给你一万条数据,你怎么做到渲染树形结构不卡顿呢?

处理一万条数据并渲染树形结构时,关键在于高效转换数据和优化渲染性能。以下是具体实现方案:
一、高效数据转换(树形结构构建)
直接递归处理一万条数据会有性能瓶颈,推荐使用哈希表映射法(O (n) 时间复杂度):
ts
import { ref, computed } from 'vue';
// 定义数据类型
interface TreeNode {
id: number;
parent: number;
name: string;
children?: TreeNode[];
}
export function useTreeData(flatData: TreeNode[]) {
// 缓存处理结果,避免重复计算
const treeData = ref<TreeNode[]>([]);
// 构建树形结构(核心逻辑)
const buildTree = () => {
const map = new Map<number, TreeNode>();
const roots: TreeNode[] = [];
// 第一步:建立ID到节点的映射,同时初始化children
flatData.forEach(node => {
map.set(node.id, { ...node, children: [] });
});
// 第二步:关联父子节点
flatData.forEach(node => {
const current = map.get(node.id)!;
if (node.parent === -1) {
// 根节点
roots.push(current);
} else {
// 子节点:找到父节点并添加到children
const parent = map.get(node.parent);
if (parent) {
parent.children!.push(current);
}
}
});
treeData.value = roots;
};
// 初始化时执行一次
buildTree();
return {
treeData,
// 提供重新构建的方法(如需动态更新数据)
rebuildTree: (newData: TreeNode[]) => {
flatData = newData;
buildTree();
}
};
}
核心优化点:
- 使用
Map
存储节点映射,避免递归查找父节点(将嵌套查找改为 O (1) 的直接访问) - 先复制节点并初始化
children
数组,再批量关联父子关系,减少中间状态切换
二、渲染性能优化(树形组件实现)
即使数据转换高效,直接渲染一万个节点仍会卡顿,需结合虚拟滚动和懒加载:
vue
<template>
<div class="tree-selector">
<!-- 虚拟滚动容器 -->
<div
class="tree-container"
ref="containerRef"
@scroll="handleScroll"
>
<!-- 占位元素(用于撑起滚动高度) -->
<div
class="virtual-placeholder"
:style="{ height: totalHeight + 'px' }"
/>
<!-- 可视区域节点 -->
<div
class="visible-nodes"
:style="{
transform: `translateY(${offsetTop}px)`,
height: viewportHeight + 'px'
}"
>
<!-- 递归渲染可视区域内的节点 -->
<template v-for="node in visibleNodes" :key="node.id">
<TreeItem
:node="node"
:depth="node.depth"
@toggle="handleToggle"
@select="handleSelect"
/>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue';
import TreeItem from './TreeItem.vue';
import { useTreeData, TreeNode } from './useTreeData';
// 接收扁平数据 props
const props = defineProps<{
data: TreeNode[];
itemHeight: number; // 每个节点的固定高度(用于虚拟滚动计算)
}>();
// 转换树形数据
const { treeData } = useTreeData(props.data);
// 扩展节点信息(添加深度和展开状态)
const expanded = ref<Set<number>>(new Set([0])); // 默认展开根节点
const flatNodes = ref<TreeNode & { depth: number }[]>([]);
// 扁平化树形结构(带深度信息,用于虚拟滚动)
const flattenTree = (nodes: TreeNode[], depth = 0) => {
nodes.forEach(node => {
flatNodes.value.push({ ...node, depth });
// 只展开已标记的节点的子节点
if (expanded.value.has(node.id) && node.children?.length) {
flattenTree(node.children, depth + 1);
}
});
};
// 容器和滚动状态
const containerRef = ref<HTMLDivElement>(null);
const viewportHeight = ref(500); // 可视区域高度
const scrollTop = ref(0);
const totalHeight = computed(() => flatNodes.value.length * props.itemHeight);
const offsetTop = computed(() => Math.floor(scrollTop.value / props.itemHeight) * props.itemHeight);
// 计算可视区域内的节点
const visibleNodes = computed(() => {
const start = Math.floor(scrollTop.value / props.itemHeight) - 5; // 预加载5个
const end = Math.ceil((scrollTop.value + viewportHeight.value) / props.itemHeight) + 5;
return flatNodes.value.slice(
Math.max(0, start),
Math.min(flatNodes.value.length, end)
);
});
// 处理滚动
const handleScroll = (e: Event) => {
scrollTop.value = (e.target as HTMLDivElement).scrollTop;
};
// 处理节点展开/折叠
const handleToggle = (nodeId: number) => {
if (expanded.value.has(nodeId)) {
expanded.value.delete(nodeId);
} else {
expanded.value.add(nodeId);
}
// 重新计算扁平化节点(只包含展开的子树)
flatNodes.value = [];
flattenTree(treeData.value);
};
// 处理节点选择
const handleSelect = (node: TreeNode) => {
emit('select', node);
};
// 初始化
onMounted(() => {
// 获取容器高度
if (containerRef.value) {
viewportHeight.value = containerRef.value.clientHeight;
}
// 初始扁平化
flattenTree(treeData.value);
});
const emit = defineEmits<{
(e: 'select', node: TreeNode): void;
}>();
</script>
<style scoped>
.tree-container {
position: relative;
overflow: auto;
border: 1px solid #e5e7eb;
border-radius: 4px;
}
.virtual-placeholder {
position: absolute;
top: 0;
left: 0;
width: 1px;
pointer-events: none;
}
.visible-nodes {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
</style>
vue
<template>
<div
class="tree-item"
:style="{
paddingLeft: `${depth * 20}px`, // 缩进显示层级
height: `${itemHeight}px`,
lineHeight: `${itemHeight}px`
}"
>
<!-- 展开/折叠按钮 -->
<span
class="toggle-btn"
@click.stop="handleToggle"
v-if="node.children && node.children.length"
>
<i class="fa" :class="expanded ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
</span>
<!-- 节点内容 -->
<span
class="node-content"
@click.stop="handleSelect"
>
{{ node.name }}
</span>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
const props = defineProps<{
node: TreeNode & { depth: number };
depth: number;
}>();
// 从父组件注入状态和方法
const expanded = inject<Set<number>>('expanded');
const handleToggle = () => {
emit('toggle', props.node.id);
};
const handleSelect = () => {
emit('select', props.node);
};
const emit = defineEmits<{
(e: 'toggle', id: number): void;
(e: 'select', node: TreeNode): void;
}>();
// 注入节点高度(从父组件传入)
const itemHeight = inject<number>('itemHeight', 36);
</script>
<style scoped>
.tree-item {
display: flex;
align-items: center;
cursor: pointer;
transition: background 0.2s;
}
.tree-item:hover {
background: #f5f7fa;
}
.toggle-btn {
width: 20px;
text-align: center;
color: #666;
}
.node-content {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
核心优化点
- 虚拟滚动: 只渲染可视区域内的节点(通常 30-50 个),大幅减少 DOM 数量
- **懒加载子节点:**仅展开的节点才会加载子节点,初始只渲染根节点
- **固定高度计算:**通过预设节点高度快速计算滚动偏移量,避免实时测量
- **扁平化处理:**将树形结构转为一维数组,简化虚拟滚动的索引计算
三、使用方式
vue
<template>
<TreeSelector
:data="backendData"
:item-height="36"
@select="onNodeSelect"
/>
</template>
<script setup>
import TreeSelector from './TreeSelector.vue';
import { ref } from 'vue';
// 模拟后端返回的1万条数据
const backendData = ref([
{ id: 0, parent: -1, name: 'name0' },
{ id: 1, parent: 0, name: 'name1' },
// ... 更多数据
]);
const onNodeSelect = (node) => {
console.log('选中节点:', node);
};
</script>
四、进一步优化建议
- 数据分片加载:如果后端支持,可分页加载子节点(点击展开时才请求该节点的子数据)
- 节点过滤:添加搜索功能,通过关键词过滤节点,减少需要渲染的数量
- 缓存展开状态:使用 localStorage 保存用户展开过的节点 ID,刷新后恢复状态
- 避免频繁重绘:节点样式变化(如选中状态)使用 transform 或 opacity 等 GPU 加速属性
通过以上方法,即使处理一万条数据,也能保持树形组件的流畅渲染。
- 本文链接:https://conder.top/article/16
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明文章出处!