高性能递归算法实现HTML嵌套列表转换为树状JSON结构
时间:2026-06-26 06:59
先说结论:处理嵌套列表解析的核心策略是:优先使用迭代+栈模拟递归,而非纯递归。原因很简单:当浏览器中DOM深度达到20层以上时,递归极易引发栈溢出——这并非理论假设,而是实际开发中频繁遇到的真实现象。优化的目标不是追求速度,而是确保不遗漏节点、不混淆层级、不发生栈溢出。 浏览器中DOM深度一旦超过5
先说结论:处理嵌套列表解析的核心策略是:优先使用迭代+栈模拟递归,而非纯递归。原因很简单:当浏览器中DOM深度达到20层以上时,递归极易引发栈溢出——这并非理论假设,而是实际开发中频繁遇到的真实现象。优化的目标不是追求速度,而是确保不遗漏节点、不混淆层级、不发生栈溢出。

浏览器中DOM深度一旦超过50层,就很容易触发递归调用栈限制,而在实际页面中,嵌套列表的深度通常在8到12层之间。在这种情况下,纯递归基本无法胜任。
### 如何安全提取 `
` 的父子关系
需要注意的是,DOM树与JSON树的结构并不完全等同。`` 元素内部可能混杂文本、链接、图标等各种内容,且它的子 `` 不一定紧跟在后面。这意味着不能简单依赖 `children` 遍历,必须显式定位每个 `- ` 下的第一个 `
`,并将其作为子节点容器。
具体操作上:
* 首先通过 `querySelectorAll('li')` 获取所有 `- ` 元素,并按照DOM顺序逐个处理。这样能有效避免因CSS属性 `display:none` 或JavaScript动态插入导致的节点遗漏。
* 对每个 `
- `,使用 `nextElementSibling` 向下查找最近的 `
`,而不是用 `li.querySelector('ul')`。后者容易误抓兄弟节点下的子菜单,造成层级混乱。
* 如果找到了对应的 ``,则递归解析它;如果未找到,则将 `children` 设置为空数组。
### `parseListToTree()` 函数必须隔离作用域与状态
一个常见的失误点:将 `result` 数组或 `currentNode` 对象传入递归函数进行累加,导致所有层级共享同一个引用,子节点被重复推送到父节点两次。正确的做法是:让每一层递归都返回一个全新的数组,由上一层决定是否合并。
函数签名应设计为 `function parseListToTree(ulElement) { ... return childrenArray; }`,坚决不接受外部变量注入。
* 每个 `- ` 的文本内容需要清理:剔除换行、多余空格、` `,然后使用 `textContent.trim()` 而不是 `innerText`(后者受CSS影响,结果可能不准确)。
* 如果需要保留原始HTML片段(例如含有图标标签),可以使用 `innerHTML.replace(/< \/?ul[^]*?>/gi, '')` 剥离包裹标签。不过要注意XSS风险,建议添加白名单过滤。
### 深度超过20层时,改用迭代+栈模拟递归
虽然Chrome V8的默认调用栈限制大约为10000帧,但实际测试表明,DOM深度一旦超过20层,就可能触发 `RangeError: Maximum call stack size exceeded`,尤其在旧版Safari中更加敏感。这种情况下必须切换策略。
使用数组模拟栈:`const stack = [{ ul: rootUl, depth: 0, parent: null }];` 每次弹出一个节点,处理其下的 `
- `,然后将子 `
` 推入栈中。
* 每一层生成的对象都必须带有 `id` 字段(可以用 `Math.random().toString(36).substr(2, 9)` 生成轻量唯一键),否则后续无法关联父子关系。
* 迭代版本的性能会略低(多一次循环),但内存可控,没有栈溢出风险。特别适合CMS导出菜单、文档大纲等深度不可控的场景。
真正的核心难点不在于如何编写递归,而在于判断何时应该避免递归。当DOM层级不断加深时,就需要认真考虑:是否应该采用平面列表配合depth字段来替代嵌套JSON结构?许多前端树组件(如 antd Tree)在内部早已将数据扁平化处理,渲染时才按depth计算缩进。这一细节常被忽视,却对长期维护影响深远。我的结论是:与其在20层以上强行使用递归,不如从一开始就转向扁平化数据结构。