拆解嵌套无序列表:JQuery递归逻辑执行结果不符问题咨询
嵌套列表拆分的jQuery递归逻辑问题分析
问题场景与疑惑
我有这样一个嵌套无序列表:
<div id="nav"> <ul id="root"> <li>item-a</li> <li>item-b <ul> <li>subitem-b1</li> <li>subitem-b2</li> </ul> </li> <li>item-c</li> </ul> </div>
想要把它拆成两个独立列表,于是写了一段递归处理的jQuery代码:
var list = $('#root'); function traverseList(list){ if(list.length > 0){ list.children('li').each(function(i){ if($(this).children('ul').length > 0){ traverseList($(this).children('ul')); }else{ $('#nav').append($(this).parent()); } }); } } traverseList(list);
我的想法是:遍历所有li,如果某个li没有子ul,就把它的父级ul追加到外层#nav中。但执行后得到的结果却和预期不符:
<div id="nav"> <ul> <li>subitem-b1</li> <li>subitem-b2</li> </ul> <ul id="root"> <li>item-a</li> <li>item-b</li> <li>item-c</li> </ul> </div>
我很困惑:item-a明明没有子ul,按逻辑应该把它的父级(带子ul的root列表)追加到div里,但实际结果里root列表还在,而且item-b的子ul被拆出来了,这到底是哪里出问题了?
一步步拆解执行流程
咱们跟着代码走一遍,就能找到问题所在:
- 首次调用
traverseList($('#root')),开始遍历root下的3个li:- 第一个
li是item-a,它没有子ul,于是执行$('#nav').append($(this).parent())——这里的parent()就是#root这个ul。但你要注意:DOM元素在页面里只能存在一个位置,如果把已经在父容器里的元素用append,它只会被移到父容器的末尾,但#root本来就在#nav里,所以这次操作等于没动它。而且jQuery的each遍历的是静态集合——也就是遍历开始时获取的那3个li,后续DOM变化不会影响这个遍历的队列。 - 接下来处理第二个
liitem-b,它有子ul,于是递归调用traverseList(item-b的子ul)。 - 在递归里,遍历这个子ul的两个
li(subitem-b1和subitem-b2),它们都没有子ul,所以执行$('#nav').append($(this).parent())——也就是把这个子ul从item-b里面移出来,追加到#nav里。这时候item-b就变成了没有子ul的空li了。 - 递归结束回到外层遍历,处理第三个
liitem-c,它也没有子ul,再次执行$('#nav').append($(this).parent())——还是移动#root,但它已经在#nav末尾了,所以还是没变化。
- 第一个
- 到这里你就明白为什么结果是这样了:你的逻辑是“只要遇到无后代ul的li,就移动它所在的整个ul”,但item-a所在的ul是root,里面还有其他li(item-b当时还有子ul),所以移动root并没有拆分它;而item-b的子ul里的两个li都是叶子节点,所以这个子ul被完整移出来了,最后root里剩下的三个li都没有子ul,但遍历已经结束,不会再处理它们了。
调整思路的启发
如果你的目标是把所有“叶子层级”(没有子ul的ul)拆成独立列表,或者把所有叶子li分组到独立列表里,可以换个思路:
- 不要直接移动父ul,而是先收集所有的叶子li,再按层级分组创建新ul来包裹;
- 或者遍历的时候,遇到子ul就先递归处理,然后把这个子ul移到外层,同时清空原li里的子ul;
- 一定要记住:jQuery的
each遍历的是静态集合,遍历开始后,即使DOM结构变了,遍历还是会按最初获取的元素列表执行,这是很容易踩的坑。
举个简单的修正示例,比如把所有子ul都移到外层,同时保留原li的结构:
var $nav = $('#nav'); function traverseList($list) { $list.children('li').each(function() { var $childUl = $(this).children('ul'); if ($childUl.length > 0) { traverseList($childUl); $nav.append($childUl); // 将子ul移到nav外层 $(this).remove('ul'); // 移除原li里的子ul } }); } traverseList($('#root'));
这样执行后,就能得到独立的子ul列表,同时原root里的li都不再包含子ul,满足拆分的需求。
内容的提问来源于stack exchange,提问作者Dillinja
相关产品推荐
相关产品推荐

