如何通过JavaScript实现树形结构菜单?

15次阅读

答案:通过递归算法将层级数据渲染为嵌套HTML,结合CSS控制样式与JavaScript管理展开折叠状态,并利用虚拟化、懒加载和DocumentFragment优化性能。

如何通过JavaScript实现树形结构菜单?

通过JavaScript实现树形结构菜单,核心在于利用递归算法处理层级数据,并将其动态渲染为嵌套的HTML元素。这通常涉及将一个具有

children

属性的数组结构转换为多层的

<ul><li>

<div>

结构,同时辅以CSS进行样式控制,以及JavaScript来管理展开/折叠等交互状态。


实现一个树形菜单,我们首先需要一个能够清晰表达层级关系的数据结构。最常见且直观的方式是使用一个数组,数组中的每个对象代表一个菜单项,如果该菜单项有子菜单,则会有一个

children

属性,其值也是一个包含子菜单项的数组。

// 示例数据结构 const menuData = [   {     id: '1',     name: '仪表盘',     link: '#dashboard'   },   {     id: '2',     name: '用户管理',     link: '#users',     children: [       {         id: '2-1',         name: '用户列表',         link: '#users/list'       },       {         id: '2-2',         name: '角色权限',         link: '#users/roles'       }     ]   },   {     id: '3',     name: '产品管理',     link: '#products',     children: [       {         id: '3-1',         name: '商品列表',         link: '#products/list'       },       {         id: '3-2',         name: '分类管理',         link: '#products/categories',         children: [           {             id: '3-2-1',             name: '主分类',             link: '#products/categories/main'           },           {             id: '3-2-2',             name: '子分类',             link: '#products/categories/sub'           }         ]       }     ]   } ];  // 核心渲染函数 function createTreeMenu(data, parentElement) {   const ul = document.createElement('ul');   ul.classList.add('menu-level'); // 添加层级标识    data.forEach(item => {     const li = document.createElement('li');     li.classList.add('menu-item');     li.dataset.id = item.id; // 方便后续操作      const link = document.createElement('a');     link.href = item.link || '#';     link.textContent = item.name;     li.appendChild(link);      if (item.children && item.children.length > 0) {       li.classList.add('has-children'); // 标记有子菜单的项       // 这里可以添加一个展开/折叠的图标或按钮       const toggleBtn = document.createElement('span');       toggleBtn.textContent = ' ▶'; // 简单示例       toggleBtn.classList.add('menu-toggle');       li.insertBefore(toggleBtn, link.nextSibling); // 放在链接后面        // 递归调用,处理子菜单       const subMenu = createTreeMenu(item.children, li);       subMenu.classList.add('sub-menu'); // 标记子菜单       li.appendChild(subMenu);     }     ul.appendChild(li);   });    parentElement.appendChild(ul); // 将生成的菜单结构添加到父元素   return ul; // 返回当前层级的UL元素 }  // 假设我们有一个ID为 'app' 的容器 const appContainer = document.getElementById('app'); if (appContainer) {   createTreeMenu(menuData, appContainer);    // 添加展开/折叠的事件监听器   appContainer.addEventListener('click', (event) => {     const target = event.target;     if (target.classList.contains('menu-toggle')) {       const parentLi = target.closest('.has-children');       if (parentLi) {         parentLi.classList.toggle('expanded'); // 切换展开状态         // 也可以直接操作子菜单的显示/隐藏         const subMenu = parentLi.querySelector('.sub-menu');         if (subMenu) {           subMenu.style.display = subMenu.style.display === 'none' ? '' : 'none';           target.textContent = parentLi.classList.contains('expanded') ? ' ▼' : ' ▶';         }       }     }   }); }  // 简单的CSS来隐藏子菜单,并设置展开状态 /* #app .sub-menu {   display: none;   padding-left: 20px; // 缩进 }  #app .has-children.expanded > .sub-menu {   display: block; }  #app .menu-item {   list-style: none;   margin: 5px 0; }  #app .menu-item a {   text-decoration: none;   color: #333; }  #app .menu-toggle {   cursor: pointer;   margin-left: 5px; } */

为什么选择递归而非迭代来实现树形菜单?

在处理树形结构时,递归无疑是一种更自然、更符合人类思维习惯的编程范式。树的定义本身就是递归的——一个树节点可以包含零个或多个子树。当你需要遍历或构建这样一个层级结构时,递归函数能够优雅地“下潜”到每一个子层级,处理完当前层级后,再“回溯”到上一层。

用迭代方式实现树形菜单当然也是可能的,通常会借助于栈(Stack)或队列(Queue)来模拟递归调用的过程,手动管理当前处理的节点和其深度。然而,这种方式往往会增加代码的复杂度和维护成本。你需要显式地维护一个数据结构来存储待处理的节点,以及它们的父节点信息,以便在渲染时正确地构建DOM层级。对于初学者来说,这无疑增加了理解难度。

立即学习Java免费学习笔记(深入)”;

递归的优势在于它将复杂问题分解为相同但规模更小的子问题。

createTreeMenu

函数就是这样,它知道如何处理当前层级的菜单项,如果某个菜单项有子菜单,它就“委托”自己去处理那个子菜单,直到没有子菜单为止。这种模式与我们对树结构的直观理解高度契合,使得代码逻辑更加清晰、简洁,也更不容易出错。当然,递归也有其局限性,例如过深的递归可能导致栈溢出(Stack Overflow),但在现代浏览器中,对于常规的菜单深度,这通常不是一个实际问题。

如何处理菜单项的展开与折叠状态?

菜单项的展开与折叠是树形菜单最核心的交互功能之一。实现这一功能,通常有几种思路,而JavaScript驱动的方式最为灵活和强大。

最直接的方法是在HTML结构中,为每个带有子菜单的

<li>

元素添加一个特定的CSS类,例如

has-children

。当用户点击展开/折叠按钮时,我们通过JavaScript来切换这个

<li>

元素上的另一个CSS类,比如

expanded

// 假设父li元素上有一个 .has-children 类 // 点击事件监听器中: const parentLi = target.closest('.has-children'); if (parentLi) {   parentLi.classList.toggle('expanded'); // 切换展开状态   // 根据 expanded 状态改变图标   target.textContent = parentLi.classList.contains('expanded') ? ' ▼' : ' ▶'; }

然后,在CSS中,我们可以根据这个

expanded

类来控制子菜单的显示与隐藏:

如何通过JavaScript实现树形结构菜单?

DeepL Write

DeepL推出的AI驱动的写作助手,在几秒钟内完善你的写作

如何通过JavaScript实现树形结构菜单?97

查看详情 如何通过JavaScript实现树形结构菜单?

.sub-menu {   display: none; /* 默认隐藏子菜单 */   overflow: hidden; /* 配合 max-height 实现过渡效果 */   max-height: 0;   transition: max-height 0.3s ease-out; }  .has-children.expanded > .sub-menu {   display: block; /* 展开时显示 */   max-height: 500px; /* 足够大的值,或者计算实际高度 */ }

这种方法的好处是,所有的展示逻辑都集中在CSS中,JavaScript只负责切换类名,实现了关注点分离。为了增强用户体验,可以配合CSS的

transition

属性,让展开和折叠过程更加平滑。

除了直接操作DOM元素的类名,更高级的实现会结合数据管理。在现代前端框架(如React、Vue)中,我们会将每个菜单项的

isExpanded

状态存储在组件的内部状态或全局状态管理中。当用户点击时,更新这个状态,框架会自动根据状态变化重新渲染UI,从而达到展开/折叠的效果。这种数据驱动的模式,在处理复杂交互和状态同步时,显得更加健壮和可维护。同时,为了提高可访问性,别忘了为展开/折叠按钮添加

aria-expanded

属性,告知屏幕阅读器当前菜单的展开状态。

优化大型树形菜单的性能有哪些策略?

当树形菜单的数据量非常庞大时,直接一次性渲染所有节点可能会导致严重的性能问题,例如页面加载缓慢、UI卡顿。这时,我们需要采取一些优化策略来提升用户体验。

一个非常有效的策略是虚拟化(Virtualization)或懒加载(Lazy Loading)。这意味着我们只渲染用户当前可见的菜单项,以及少量即将进入视口的菜单项。对于那些超出屏幕范围的菜单项,我们只保留其数据,而不生成实际的DOM元素。当用户滚动或展开菜单时,再动态地创建和销毁DOM节点。这通常需要一个复杂的滚动容器和视口检测机制,但对于拥有成千上万个节点的树来说,性能提升是巨大的。例如,一些UI库提供了虚拟滚动组件,可以直接应用于树形结构。

其次,初始状态的优化也至关重要。默认情况下,可以将所有子菜单都设置为折叠状态。这样,在页面加载时,浏览器只需要渲染顶层菜单项,大大减少了首次渲染的DOM节点数量和计算量。只有当用户主动点击展开某个菜单时,其子菜单才会被渲染出来。

再者,高效的DOM操作是基础。在JavaScript中,频繁地直接操作DOM会触发浏览器的重排(Reflow)和重绘(Repaint),这是非常耗费性能的。如果需要批量添加或修改DOM元素,可以考虑使用

DocumentFragment

。它是一个轻量级的文档片段,你可以将所有要添加的元素先添加到

DocumentFragment

中,最后再将整个

DocumentFragment

一次性添加到实际的DOM树中。这样可以显著减少DOM操作的次数,从而提升性能。

// 使用 DocumentFragment 优化批量 DOM 操作 function createTreeMenuOptimized(data, parentElement) {   const fragment = document.createDocumentFragment();   const ul = document.createElement('ul');   ul.classList.add('menu-level');    data.forEach(item => {     // ... 创建 li 和 link 等元素 ...     // 将 li 添加到 ul     ul.appendChild(li);   });    fragment.appendChild(ul);   parentElement.appendChild(fragment); // 一次性将所有元素添加到 DOM   return ul; }

最后,如果你的菜单数据需要频繁更新或搜索过滤,考虑数据结构本身的优化。确保你的数据获取和处理逻辑是高效的,例如,如果数据来自后端,确保API响应速度快且数据结构合理。对于前端的搜索功能,可以对数据进行索引或使用Trie树等数据结构来加速查找。在现代前端框架中,利用

shouldComponentUpdate

React.memo

等机制,可以避免不必要的组件重新渲染,这也是一种有效的优化手段。

css vue react javascript java html 前端 go 浏览器 app 懒加载 ssl 后端 JavaScript css html 前端框架 递归 数据结构 委托 对象 dom overflow transition ul li 算法 ui 虚拟化

text=ZqhQzanResources