处理一万条数据并渲染树形结构时,关键在于高效转换数据优化渲染性能。以下是具体实现方案:

一、高效数据转换(树形结构构建)

直接递归处理一万条数据会有性能瓶颈,推荐使用哈希表映射法(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();
    }
  };
}

核心优化点:

  1. 使用 Map 存储节点映射,避免递归查找父节点(将嵌套查找改为 O (1) 的直接访问)
  2. 先复制节点并初始化 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>

核心优化点

  1. 虚拟滚动: 只渲染可视区域内的节点(通常 30-50 个),大幅减少 DOM 数量
  2. **懒加载子节点:**仅展开的节点才会加载子节点,初始只渲染根节点
  3. **固定高度计算:**通过预设节点高度快速计算滚动偏移量,避免实时测量
  4. **扁平化处理:**将树形结构转为一维数组,简化虚拟滚动的索引计算

三、使用方式

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>

四、进一步优化建议

  1. 数据分片加载:如果后端支持,可分页加载子节点(点击展开时才请求该节点的子数据)
  2. 节点过滤:添加搜索功能,通过关键词过滤节点,减少需要渲染的数量
  3. 缓存展开状态:使用 localStorage 保存用户展开过的节点 ID,刷新后恢复状态
  4. 避免频繁重绘:节点样式变化(如选中状态)使用 transform 或 opacity 等 GPU 加速属性

通过以上方法,即使处理一万条数据,也能保持树形组件的流畅渲染。

评论
默认头像
评论
来发评论吧~