前端树形数据结构实战:从遍历算法到虚拟滚动与性能优化
发布时间:2026/8/23 2:46:43 作者:尧图编辑部 阅读量:1,286

1. 项目概述为什么前端开发者绕不开树形结构干了这么多年前端从 jQuery 时代一路走到现在三大框架鼎立我处理过最频繁、也最容易让人头疼的数据结构之一就是“树”。无论是后台管理系统里层层嵌套的菜单还是文件管理器中的目录列表甚至是组织架构图、商品分类它们的底层数据模型几乎都是树形结构。你可能会觉得这不就是个带children的数组吗但真到了要渲染、要搜索、要拖拽排序、要性能优化的时候各种“坑”就接踵而至了。这个项目标题“前端树形Tree数据结构使用-各种姿势总结”精准地戳中了前端开发者的日常痛点。它不是一个简单的 API 调用教程而是对“树”这一数据结构在前端场景下从数据建模、遍历算法、渲染方案到性能优化的全方位实战经验汇总。我见过不少同事面对一个深度不确定的树状数据第一反应就是写个递归结果页面卡顿、内存泄漏问题频发。也见过产品经理提需求“这个树支持一下无限级拖拽排序”听起来简单实现起来却涉及数据结构的深度操作和视图的精准更新。所以这篇文章我想抛开那些教科书式的定义直接从我踩过的坑、总结的最佳实践出发和你聊聊在前端玩转树形数据的“各种姿势”。无论你是正在为面试中“手写树的前序遍历”而发愁的新手还是被复杂业务树比如万级节点的大型组织树的性能问题困扰的资深开发者相信都能在这里找到可落地的解决方案和思路启发。我们的目标很明确拿到任意树形数据都能清晰、高效、稳健地把它呈现在用户面前并处理好与之相关的所有交互。2. 核心概念与数据结构设计从源头打好基础在动手写代码之前我们必须统一对“树”这个数据模型的认识。前端处理树形数据最常见的交互形式是用户看到的那个可以展开/折叠的“树形控件”。但控件的背后是数据层和视图层的分离与协作。理解这一点是设计出健壮方案的前提。2.1 前端常见树形数据格式剖析后端返回的树形数据格式五花八门但归根结底可以归纳为三种主流格式。选择哪种格式直接决定了前端处理的复杂度和性能。1. 嵌套儿童格式这是最直观、也最常见的一种格式。每个节点都是一个对象如果它有子节点则包含一个children字段该字段本身又是一个节点对象的数组。const nestedTree [ { id: 1, label: 节点1, children: [ { id: 2, label: 节点1-1, children: [] }, { id: 3, label: 节点1-2, children: [ { id: 4, label: 节点1-2-1, children: [] } ] } ] } ];优点结构清晰与视图的层级关系完全对应递归遍历非常方便。缺点对节点的增删改查特别是查找某个特定节点比较麻烦需要递归遍历整个树。当树非常大时频繁操作可能导致性能问题。2. 扁平列表格式所有节点都放在一个数组里每个节点通过一个parentId或类似的字段来指向其父节点从而建立层级关系。const flatList [ { id: 1, label: 节点1, parentId: null }, { id: 2, label: 节点1-1, parentId: 1 }, { id: 3, label: 节点1-2, parentId: 1 }, { id: 4, label: 节点1-2-1, parentId: 3 }, ];优点查找、更新、删除某个特定节点效率极高O(1)或O(n)因为可以直接操作数组。也非常适合存储到类似Map的数据结构中。缺点渲染前需要将其转换为嵌套格式或者需要能直接消费扁平数据的特殊组件。构建完整的树路径信息如获取某个节点的所有祖先节点稍显繁琐。3. 路径枚举格式每个节点存储从根节点到自身的完整路径通常是一个ID数组或字符串。const pathEnum [ { id: 1, label: 节点1, path: /1 }, { id: 2, label: 节点1-1, path: /1/2 }, { id: 3, label: 节点1-2, path: /1/3 }, { id: 4, label: 节点1-2-1, path: /1/3/4 }, ];优点判断节点间关系父子、祖先-后代非常快只需比较路径字符串。快速定位节点位置。缺点当树的结构发生变更如节点移动时更新所有受影响节点的路径开销巨大。数据冗余较多。我的经验之谈在项目初期如果后端有主导权且数据结构稳定优先推荐使用嵌套格式因为它最符合前端渲染的思维模型。但如果你的应用有大量基于ID的节点查询、频繁的节点状态如勾选状态同步需求在内存中维护一份id - node的扁平化 Map 是至关重要的性能优化手段即“嵌套用于渲染扁平用于查询”。2.2 为节点设计一个健壮的数据模型一个良好的节点对象设计是后续所有操作的基础。除了基本的id和label以下字段在实践中非常有用class TreeNode { constructor(data) { this.id data.id; // 唯一标识必须 this.label data.label; // 显示文本 this.parentId data.parentId; // 父节点ID便于扁平化操作 this.children data.children || []; // 子节点数组 this.level 0; // 节点层级根节点为0便于样式和权限控制 this.isLeaf false; // 是否为叶子节点用于优化渲染 this.expanded false; // 是否展开视图状态 this.checked false; // 是否勾选视图状态 this.indeterminate false; // 部分勾选状态视图状态 this.visible true; // 是否可见用于过滤搜索 this.rawData data; // 保留原始数据避免属性丢失 } }关键点解释level在遍历树时动态计算并赋值对于根据层级缩进、设置不同样式或进行基于层级的权限判断非常方便。isLeaf这个字段最好由后端提供或者在前端初始化时通过判断children是否为空数组来设置。它能让组件在渲染时提前知道哪些节点不可能有子节点从而避免渲染无用的展开图标或发送无谓的异步加载请求。视图状态分离expanded,checked,visible,indeterminate这些属于“视图状态”。我强烈建议不要把它们和核心数据模型一起持久化到后端而是放在前端组件的状态管理如 Vue 的 data、React 的 state、Pinia/Vuex store中。这样数据模型保持纯净只关心业务数据。2.3 两种数据格式的相互转换在实际项目中我们经常需要在嵌套格式和扁平格式之间转换。这里提供两个高效的转换函数。扁平列表转嵌套树这是最常用的函数常用于接收后端扁平数据后渲染前进行转换。/** * 将扁平列表转换为嵌套树 * param {Array} flatArray 扁平节点数组每个节点包含 id 和 parentId * param {*} rootParentId 根节点的 parentId 值通常是 null 或 0 * returns {Array} 嵌套树结构 */ function buildTreeFromFlatList(flatArray, rootParentId null) { const nodeMap new Map(); // id - node (with children) const result []; // 第一遍初始化所有节点并建立 id 映射 flatArray.forEach(item { nodeMap.set(item.id, { ...item, children: [] }); }); // 第二遍构建父子关系 flatArray.forEach(item { const node nodeMap.get(item.id); const parentNode nodeMap.get(item.parentId); if (parentNode) { parentNode.children.push(node); } else if (item.parentId rootParentId) { // 如果没有父节点且 parentId 等于根标识则认为是根节点 result.push(node); } // 注意如果 parentId 在 map 中找不到且不等于 rootParentId // 则该节点可能数据有误暂时被丢弃。可根据业务需要调整。 }); // 可选按需对 children 数组进行排序 result.forEach(root sortTree(root)); return result; } // 辅助函数递归排序树节点例如按 label 字母顺序 function sortTree(node) { if (node.children.length 0) { node.children.sort((a, b) a.label.localeCompare(b.label)); node.children.forEach(child sortTree(child)); } }嵌套树转扁平列表当你需要将修改后的树提交给后端或者需要快速查找节点时这个转换很有用。/** * 将嵌套树展平为列表 * param {Array} tree 嵌套树 * param {*} parentId 当前层级的父节点ID * returns {Array} 扁平列表每个节点包含 id, parentId */ function flattenTree(tree, parentId null) { const result []; tree.forEach(node { const { children, ...rest } node; // 分离 children 属性 const flatNode { ...rest, parentId }; result.push(flatNode); if (children children.length 0) { // 递归处理子节点当前节点成为它们的父节点 const childList flattenTree(children, node.id); result.push(...childList); } }); return result; }3. 树的遍历与核心算法深度优先 vs 广度优先对树进行遍历是几乎所有树操作的基础渲染、搜索、查找节点、收集勾选节点等。前端最需要掌握的是深度优先遍历和广度优先遍历。3.1 深度优先遍历的递归与迭代实现深度优先遍历会沿着树的深度遍历节点尽可能深地搜索树的分支。它又分为前序、中序、后序。对于前端常见的组件树前序遍历父节点-子节点最为常用。递归实现最直观// 前序遍历执行操作 - 遍历子节点 function dfsPreorderRecursive(node, callback) { if (!node) return; callback(node); // 对当前节点进行操作如渲染 if (node.children) { node.children.forEach(child dfsPreorderRecursive(child, callback)); } } // 使用示例收集所有节点ID const idList []; dfsPreorderRecursive(rootNode, node idList.push(node.id));递归写法简洁明了但对于深度非常大的树理论上可能存在调用栈溢出的风险。不过在前端业务中树的深度通常不会夸张到那种程度递归仍然是首选。迭代实现使用栈如果你确实担心栈溢出或者想显式控制遍历过程可以使用迭代法。function dfsPreorderIterative(root, callback) { if (!root) return; const stack [root]; // 使用栈模拟递归 while (stack.length 0) { const node stack.pop(); // 弹出栈顶 callback(node); // 注意为了保持前序父-子的顺序需要将子节点逆序压栈 // 这样先入栈的后出保证最左边的子节点先被处理 if (node.children) { for (let i node.children.length - 1; i 0; i--) { stack.push(node.children[i]); } } } }3.2 广度优先遍历的应用场景广度优先遍历会先访问离根节点最近的节点一层一层向下访问。这在需要按层级处理节点时非常有用例如计算树的深度、按层级渲染、寻找最短路径在树中就是最少步数找到某个节点。迭代实现使用队列function bfs(root, callback) { if (!root) return; const queue [root]; // 使用队列先进先出 while (queue.length 0) { const node queue.shift(); // 从队头取出 callback(node); // 将当前节点的所有子节点依次加入队尾 if (node.children) { node.children.forEach(child queue.push(child)); } } } // 使用示例查找树中第一个 label 包含“关键”的节点 function findNodeByBFS(root, predicate) { const queue [root]; while (queue.length 0) { const node queue.shift(); if (predicate(node)) { return node; } if (node.children) { node.children.forEach(child queue.push(child)); } } return null; } const targetNode findNodeByBFS(rootTree, node node.label.includes(关键));3.3 查找节点的“终极优化方案”在大型树中根据ID或条件查找一个节点如果每次都从头遍历性能堪忧。我的终极建议是在初始化树时就构建一个Mapid, node的索引。class TreeManager { constructor(nestedTree) { this.tree nestedTree; this.nodeMap new Map(); this._buildIndex(this.tree); } _buildIndex(nodes, parent null) { nodes.forEach(node { node.parent parent; // 可选直接在节点上挂载父节点引用方便回溯 this.nodeMap.set(node.id, node); if (node.children node.children.length 0) { this._buildIndex(node.children, node); } }); } getNodeById(id) { return this.nodeMap.get(id); // O(1) 时间复杂度 } // 获取从根节点到目标节点的路径 getNodePath(id) { const node this.getNodeById(id); if (!node) return []; const path []; let current node; while (current) { path.unshift(current); // 向前插入保证顺序是根-...-父-当前 current current.parent; // 这里用到了我们挂载的 parent 引用 } return path; } }这个TreeManager类封装了树数据和索引任何基于ID的查找、路径获取、兄弟节点查找等操作都变得极其高效。这是处理复杂树交互的基石。4. 树形组件的渲染策略与性能优化数据准备好了算法也掌握了接下来就是如何高效地将树渲染到页面上。不同的场景需要不同的渲染策略。4.1 递归组件渲染简单场景的首选在 Vue 或 React 中递归组件是渲染嵌套树最自然的方式。Vue 3 示例 (单文件组件)!-- TreeNode.vue -- template li div clicktoggleExpand :style{ paddingLeft: level * 20 px } span v-ifhasChildren{{ isExpanded ? - : }}/span span{{ node.label }}/span /div ul v-ifhasChildren isExpanded TreeNode v-forchild in node.children :keychild.id :nodechild :levellevel 1 / /ul /li /template script setup import { computed, ref } from vue; const props defineProps({ node: Object, level: { type: Number, default: 0 } }); const isExpanded ref(false); const hasChildren computed(() props.node.children props.node.children.length 0); const toggleExpand () { if (hasChildren.value) { isExpanded.value !isExpanded.value; } }; /script优点代码直观符合数据逻辑。缺点当树节点数量巨大比如超过1000个时即使很多节点是折叠的Vue/React 仍然需要为所有节点创建组件实例可能导致初始渲染缓慢和内存占用过高。4.2 虚拟滚动渲染应对海量节点的利器对于大型树如文件目录、全公司组织架构必须使用虚拟滚动。其核心思想是只渲染可视区域内的节点。 实现要点扁平化可视节点列表根据树的展开状态将需要显示的节点计算成一个扁平列表。这需要一次深度优先遍历但只收集expanded为 true 的祖先节点下的、且自身visible为 true 的节点。计算滚动位置每个节点需要知道自己的准确高度固定高度或动态测量和在扁平列表中的索引。渲染窗口根据滚动容器的scrollTop和clientHeight计算出当前应该渲染的节点范围的起始和结束索引。这个过程较为复杂通常建议使用成熟的库如React:react-window或react-virtualized。Vue:vue-virtual-scroller或基于tanstack/vue-virtual原 react-virtual。针对 Element Plus 的 Tree可以尝试社区方案或自行封装将el-tree的数据源替换为虚拟滚动计算后的切片数据。踩坑记录虚拟滚动中节点高度如果是动态的如内容行数不定会非常棘手。一个务实的方案是使用“预估高度动态调整”策略。先给一个预估高度渲染待节点真实渲染后用ResizeObserver监测其实际高度并更新总高度和滚动位置。虽然会有细微跳动但比完全卡死要好。4.3 异步加载与懒渲染优化体验的关键对于节点数量多但用户通常只查看其中一小部分的树如全国省市区划异步加载懒加载是必备功能。实现模式节点初始化时即使它有children字段也先设为空数组[]并标记isLeaf: false如果确定无子节点则标记为true。当用户点击展开图标时触发一个异步函数如loadChildren(node)。显示加载状态如旋转图标。向后台请求该节点的子节点数据。数据返回后将数据赋值给node.children并触发视图更新。async function loadChildren(node) { if (node.childrenLoaded || node.isLeaf) return; // 已加载过或是叶子节点则返回 node.loading true; try { const childData await api.fetchChildren(node.id); // 调用API node.children childData; // 更新子节点 node.childrenLoaded true; // 重要更新索引 Map childData.forEach(child { child.parent node; treeManager.nodeMap.set(child.id, child); }); } catch (error) { console.error(加载子节点失败:, error); } finally { node.loading false; } }结合虚拟滚动异步加载和虚拟滚动是绝配。用户滚动时动态加载即将进入可视区域的未加载节点。5. 复杂交互的实现勾选、拖拽与搜索过滤树形组件光能看还不够复杂的交互才是体现功力的地方。5.1 勾选状态联动从叶子到根的计算实现一个带复选框的树要求勾选子节点父节点自动变为“半选”或“全选”勾选父节点其下所有子节点自动勾选/取消。 这是一个经典的“状态向上传播”问题。核心算法步骤向下传播当某个节点被勾选/取消时递归地设置其所有子孙节点的checked状态与之一致。向上传播更新完当前节点及子孙节点后需要递归地更新其所有祖先节点的状态。一个父节点的勾选状态取决于其所有子节点如果所有子节点都勾选了则父节点为全选。如果所有子节点都未勾选则父节点为未选。其他情况部分勾选父节点为半选indeterminate: true。function updateCheckState(node, checked) { // 1. 向下设置子孙节点 setChildrenChecked(node, checked); // 2. 向上更新祖先节点 updateParentCheckState(node); } function setChildrenChecked(node, checked) { node.checked checked; node.indeterminate false; // 当明确设置时清除半选状态 if (node.children) { node.children.forEach(child setChildrenChecked(child, checked)); } } function updateParentCheckState(node) { let parent node.parent; while (parent) { const children parent.children; const checkedCount children.filter(c c.checked).length; const indeterminateCount children.filter(c c.indeterminate).length; if (checkedCount children.length) { // 全部子节点选中 parent.checked true; parent.indeterminate false; } else if (checkedCount 0 indeterminateCount 0) { // 全部子节点未选中 parent.checked false; parent.indeterminate false; } else { // 部分选中或存在半选 parent.checked false; parent.indeterminate true; } parent parent.parent; // 继续向上 } }性能注意对于节点很多的树频繁递归可能影响性能。可以考虑“惰性计算”即只在需要获取最终勾选结果如提交时或用户操作了某个层级时才去计算相关路径上的节点状态而不是每次勾选都遍历整棵大树。5.2 拖拽排序的实现思路树节点的拖拽排序如调整菜单顺序涉及两方面UI 交互和数据结构更新。UI交互使用 HTML5 原生 Drag and Drop API 或第三方库如sortablejs、vuedraggable、react-dnd。重点在于处理好拖拽的视觉反馈放置指示线和有效放置区域的判断。数据更新这是更关键的一步。当拖拽结束时你需要确定拖拽的源节点draggedNode和目标位置targetNode、放置类型‘before’、‘after’、‘inside’。在数据层将源节点从原父节点的children数组中移除。根据放置类型将源节点插入到目标节点的父级children数组的相应位置之前、之后或者作为目标节点的子节点插入其children数组开头或末尾。关键更新所有受影响节点的parent引用和level层级。触发视图重新渲染。function handleDrop(draggedNodeId, targetNodeId, dropType) { const draggedNode treeManager.getNodeById(draggedNodeId); const targetNode treeManager.getNodeById(targetNodeId); if (!draggedNode || !targetNode) return; if (isAncestor(draggedNode, targetNode)) { // 禁止将父节点拖拽到其子节点内部 return; } // 1. 从原位置移除 const oldParent draggedNode.parent; const oldChildren oldParent ? oldParent.children : rootTree; const draggedIndex oldChildren.findIndex(n n.id draggedNodeId); oldChildren.splice(draggedIndex, 1); // 2. 插入到新位置 let newParent, insertIndex; switch (dropType) { case before: newParent targetNode.parent; insertIndex newParent.children.findIndex(n n.id targetNodeId); newParent.children.splice(insertIndex, 0, draggedNode); draggedNode.parent newParent; break; case after: newParent targetNode.parent; insertIndex newParent.children.findIndex(n n.id targetNodeId) 1; newParent.children.splice(insertIndex, 0, draggedNode); draggedNode.parent newParent; break; case inside: newParent targetNode; if (!newParent.children) newParent.children []; newParent.children.unshift(draggedNode); // 或 push取决于业务 draggedNode.parent newParent; break; } // 3. 更新层级 (需要递归更新 draggedNode 所有子孙的 level) updateNodeLevel(draggedNode, (newParent ? newParent.level 1 : 0)); }5.3 搜索过滤与高亮显示树形搜索要求输入关键词快速过滤出包含关键词的节点并自动展开其所有祖先节点以便查看同时高亮匹配的文本。实现方案遍历与匹配对整棵树进行一次深度优先遍历对每个节点的label或其他字段进行关键词匹配。标记可见性如果节点自身匹配则visible true。如果节点的任意一个子孙节点匹配则该节点也应visible true因为需要它作为路径展开。否则visible false。控制展开状态将所有匹配节点的所有祖先节点的expanded设为true确保路径可见。高亮显示在渲染节点文本时使用一个函数将匹配到的关键词部分用span class“highlight”包裹起来。注意使用v-htmlVue或dangerouslySetInnerHTMLReact时的 XSS 安全风险务必对关键词进行转义。function filterTree(keyword, tree) { if (!keyword.trim()) { // 清空搜索重置所有节点为可见折叠状态可根据业务重置 resetTreeVisibility(tree); return; } const lowerKeyword keyword.toLowerCase(); // 递归遍历返回该节点或其子孙是否匹配 function traverse(node) { let isSelfMatch node.label.toLowerCase().includes(lowerKeyword); let hasChildMatch false; if (node.children) { for (let child of node.children) { if (traverse(child)) { hasChildMatch true; } } } // 决定当前节点是否可见 node.visible isSelfMatch || hasChildMatch; // 如果当前节点或其子孙匹配则需要展开其父节点在上一层处理 // 更简单的做法在遍历后对所有 visibletrue 的节点展开其所有祖先 return isSelfMatch || hasChildMatch; } tree.forEach(root traverse(root)); // 二次遍历展开所有可见节点的祖先 function expandAncestors(node) { let parent node.parent; while (parent) { parent.expanded true; parent parent.parent; } } // 这里需要遍历所有节点找到 visibletrue 的节点展开其祖先 // 可以利用我们之前建的 nodeMap for (let [id, node] of treeManager.nodeMap) { if (node.visible) { expandAncestors(node); } } }性能优化对于大型树每次输入都全量遍历可能造成卡顿。可以使用防抖debounce技术并在遍历中使用一些短路优化如某个子树确定无匹配可提前退出。对于更极致的性能可以考虑将节点标签预先构建成字符串索引如Fuse.js那样的模糊搜索库但复杂度会更高。6. 状态管理与架构思考当树形组件变得复杂且与应用其他部分状态关联紧密时比如一个树形选择器勾选的节点需要同步到另一个表单组件就需要好的状态管理。6.1 组件状态 vs 全局状态组件内部状态expanded展开状态、checked勾选状态如果是非受控组件、当前高亮节点等这些状态通常只与组件自身的UI交互相关可以放在组件内部如 Vue 的ref React 的useState。全局/应用状态树的完整数据源、最终用户勾选的结果集、当前选中的节点用于在详情面板显示等这些状态需要在多个组件间共享应该提升到全局状态管理库中如 Pinia (Vue)、Redux/Zustand (React)。一个常见的模式是树组件接收一个modelValue或checkedKeys作为 prop受控模式当内部勾选状态变化时通过emit(‘update:modelValue’, newKeys)将变化同步到父组件或全局状态。这样数据流清晰便于调试和与外部集成。6.2 不可变数据与性能在 React 或 Vue 的响应式系统中直接修改嵌套很深的对象属性有时可能无法触发视图更新或者更新不够高效。采用不可变数据模式可以避免这些问题。// 不好的做法直接修改 node.children.push(newChild); // 在 Vue 中可能触发更新但不够规范在 React 中可能不触发重渲染 // 好的做法创建新引用 function addChildToNode(tree, nodeId, newChild) { return produce(tree, draftTree { // 使用 Immer 库简化不可变操作 const node findNodeInDraft(draftTree, nodeId); // 在 draft 中查找 if (node) { node.children [...(node.children || []), newChild]; } }); } // 使用 const newTree addChildToNode(oldTree, parentId, childNode); setTree(newTree); // 触发响应式更新或重新渲染使用Immer这样的库你可以在“草稿”上直接进行可变操作它会自动为你生成不可变的新状态代码非常简洁。7. 实战问题排查与经验锦囊最后分享几个我踩过坑才总结出的经验。问题1节点展开/折叠时子组件状态丢失这在递归组件中很常见。比如子节点内部有一个输入框折叠再展开后输入框的内容没了。原因Vue/React 在重新渲染时如果key设置不当可能会销毁并重建组件实例。解决确保每个树节点组件都有一个稳定且唯一的key。最佳实践是使用节点的唯一标识id而不是循环的index。TreeNode v-for“child in node.children” :key“child.id” :node“child” /问题2超大型树万级节点初始化卡死页面解决分片加载不要一次性渲染所有节点。先加载第一层或前几层结合异步加载。虚拟滚动如前所述这是终极解决方案。Web Worker将构建树索引Map、复杂过滤计算等 CPU 密集型任务放到 Web Worker 中避免阻塞主线程。惰性计算属性避免在组件中直接计算依赖于整棵树的属性如“选中的所有叶子节点”改为在需要时如点击提交按钮时再计算。问题3从后端接收的树数据格式经常变解决在数据入口处创建一个“适配层”Adapter。这个层的职责就是将后端各种奇葩格式的数据转换成你前端树形组件内部约定的统一格式。// treeAdapter.js export function normalizeBackendTree(backendData) { // 假设后端有时返回 { items: [] }有时返回 { list: [] } const rawList backendData.items || backendData.list || []; // 统一转换成我们内部需要的嵌套格式 return convertToNestedFormat(rawList); }这样无论后端怎么变你只需要修改这一个适配函数业务组件代码保持稳定。问题4需要实现“全部展开/折叠”功能不要递归地去设置每个节点的expanded属性。对于大型树这会导致大量响应式更新非常慢。优化方案在树管理器TreeManager中维护一个“全局展开层级”的状态。例如设置expandLevel 2表示自动展开到第2层。在组件渲染时判断node.level expandLevel来决定是否渲染为展开状态。切换全部展开/折叠时只需修改这一个expandLevel值全部展开设为一个很大的数全部折叠设为0然后触发整棵树的重渲染即可性能要好得多。处理前端树形结构就像在整理一个错综复杂的线团耐心和策略缺一不可。从设计好数据模型开始选择适合的遍历和渲染策略再到处理好各种交互细节每一步都需要仔细考量。最重要的是一定要根据你的实际业务场景和数据规模来选择技术方案没有一种姿势是放之四海而皆准的。希望这些从实战中总结出的“姿势”能让你下次面对树形需求时心中更有底气手下更有章法。