一、为什么遍历方式会拖慢页面

很多前端同学在写页面交互时,都遇到过这种尴尬:明明逻辑很简单,就是找几个节点、改几个样式,可是页面一卡一卡的,甚至浏览器直接无响应。起初你可能觉得是电脑配置不行,或者 jQuery 这个库太老了。但其实,很多时候是遍历节点的方式太重了。你可以把页面想象成一个装满了杂物的仓库,而每一个标签都是仓库里的盒子。如果你要找某个盒子,却每次从仓库门口开始,把所有的盒子都打开看一遍,找完再放回去,那当然累。jQuery 的全局选择器,本质上就是在仓库里满世界找。如果我们能知道大概的位置,比如“先走到最里面那个铁柜子,再打开第二层抽屉”,那效率就完全不同了。

这一篇不会让你背晦涩的算法,而是用大白话,把工作中最常用到的父子节点遍历技巧讲透。看完之后,你会发现优化其实就藏在每一个细小的选择里。

二、常见的遍历方式与性能隐患

2.1 从父到子的遍历陷阱

父节点找子节点,大家最熟悉的可能是 find 方法。find 确实很强大,不管嵌套多少层,它都能把符合条件的节点全揪出来。但强大不等于适合所有场合。如果你只想要某个容器的直接子节点,find 却会把所有后代节点都扫一遍。假设一个容器里有几十个列表,每个列表里又有图片、链接、图标,find 就会一口气把这些全部检查一遍。虽然单个检查很快,可如果你在滚动事件里频繁触发这样的查询,积少成多,浏览器就扛不住了。

我们看一个具体的对比。假设页面里有一个外层容器,里面有两层结构。如果我们用 children 方法,只拿“亲儿子”;用 find 方法,则所有后代都会被抓回来。

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>jQuery 父子遍历示例</title>
  <style>
    .level-1 { border: 1px solid #ccc; padding: 10px; }
    .level-2 { margin: 5px 0; padding: 5px; background: #f5f5f5; }
  </style>
</head>
<body>
  <!-- 外层容器,里面有多层嵌套结构 -->
  <div class="level-1" id="outerBox">
    <p class="level-2">第一层儿子</p>
    <div class="level-2">
      这里还有一个孙子节点
      <span class="level-3">孙子</span>
    </div>
  </div>

  <!-- 引入 jQuery 库 -->
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      // 通过 children 获取外层的直接子元素
      var directKids = $('#outerBox').children();
      console.log('直接子元素数量:', directKids.length);
      // 通过 find 获取所有后代元素
      var allDescendants = $('#outerBox').find('*');
      console.log('所有后代元素数量:', allDescendants.length);
    });
  </script>
</body>
</html>

打开浏览器控制台,你会看到直接子元素是 2 个,所有后代元素是 5 个。这只是一个很小的例子,当页面结构变得庞大,多出来的这些扫描工作就成了性能负担。

2.2 从子到父的遍历思路

从下往上找,也是家常便饭。比如点击一个按钮,想知道它属于哪一行、哪一块区域。很多人的第一反应是写一连串的 parent 方法,一层一层往上跳。但这样写有两大问题。第一,代码结构一变化,比如中间多套了一个 span,你的 parent 链条就断了。第二,每一次 parent 调用,jQuery 都会构造一个新对象,你写五层 parent,就构造了五个临时对象,白白浪费内存。

更优雅的方案是使用 closest 方法。它从当前节点出发,一级一级往上检查,一旦遇到符合选择器的节点,立刻返回。这就像坐电梯,你按了“最近的出口”按钮,它会停在第一层能出去的地方,而不是跑到顶楼再下来。

我们用一个删除按钮的例子来说明:

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>jQuery 向上遍历示例</title>
  <style>
    .list-item { padding: 12px; border-bottom: 1px solid #ddd; }
    .delete-btn { color: red; margin-left: 10px; cursor: pointer; }
  </style>
</head>
<body>
  <ul class="list-group">
    <li class="list-item" data-id="101">
      项目一 <span class="delete-btn">删除</span>
    </li>
    <li class="list-item" data-id="102">
      项目二 <span class="delete-btn">删除</span>
    </li>
  </ul>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      $('.delete-btn').on('click', function () {
        // 从被点击的删除按钮开始,向上找到最近的 li.list-item
        var targetItem = $(this).closest('.list-item');
        console.log('要删除的项目ID:', targetItem.data('id'));
        // 这里可以执行真正的删除操作,比如 slideUp 之后移除
        targetItem.slideUp(200, function () {
          $(this).remove();
        });
      });
    });
  </script>
</body>
</html>

这个例子里,按钮的父级是列表项,用 closest 正好一步到位。即使以后按钮外面再包一个 div,closest 依然能找到列表项,代码不用改动。

2.3 同胞兄弟之间的遍历技巧

除了上下级,我们经常要和兄弟节点打交道。jQuery 提供了 siblings 方法,可以取到所有兄弟节点。但如果你想找的是“紧挨着的那一个”,那使用 siblings 反而浪费。你需要的是 prev 和 next,一个看前一个兄弟,一个看后一个兄弟。这就像你去食堂排队,只想知道前面那个人的绰号,却把整个队伍的人都问了一遍号码牌,明显没必要。

所以,在写遍历代码之前,先问自己一句:我真的需要“所有”吗?如果只要最近的那一个,就选更精确的方法。

三、高效定位父子节点的实用技巧

3.1 用 find 缩小范围

前面提到 find 可能造成性能问题,但这不是说 find 就不能用。find 非常适合用来在某个明确区域内查找目标节点。它的搜索路径是“从当前节点向下”,如果你能把初始范围缩到很小,那查找速度就会非常快。反过来,如果每次都用全局选择器,jQuery 就得从 html 根节点开始往下扫描整个文档,这开销就大了。我们可以把这种优化看作“先框定一个小区,再在小区里找人”。

我们来看一个对比示例:

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>jQuery find 区域查找示例</title>
</head>
<body>
  <div id="bigArea">
    <div class="inner-box">
      <span class="target">目标节点</span>
    </div>
  </div>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      // 全局查找,从根节点开始
      var startGlob = performance.now();
      var globalTarget = $('.target');
      var endGlob = performance.now();
      console.log('全局查找耗时:', endGlob - startGlob);

      // 区域查找,从 #bigArea 开始往下找
      var startArea = performance.now();
      var areaTarget = $('#bigArea').find('.target');
      var endArea = performance.now();
      console.log('区域查找耗时:', endArea - startArea);
    });
  </script>
</body>
</html>

当然,这个示例的差异在极小页面里看不出来,但累积到成千上万个节点时,差距会变得非常明显。核心思想是:能确定父容器,就不要全局搜索。

3.2 用 children 避免全量扫描

children 方法只取当前节点的直接子元素,它不会往更深的层级钻。这种特性让它在很多场景下特别高效,尤其是当我们只需要“第一层”的时候。比如菜单结构,外层 ul 的 children 就是一级菜单项,如果你用 find,则会连子菜单、孙菜单都找出来。所以,尽量让 children 发挥作用,既能提高效率,也能让代码意图更清晰:我只在乎这一层。

我们看一个菜单示例:

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>children 方法示例</title>
</head>
<body>
  <ul class="menu">
    <li class="menu-item">
      首页
      <ul class="sub-menu">
        <li>子菜单一</li>
        <li>子菜单二</li>
      </ul>
    </li>
    <li class="menu-item">关于我们</li>
    <li class="menu-item">联系方式</li>
  </ul>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      // 只获取一级菜单项,不会把子菜单也算进来
      var firstLevel = $('.menu').children('.menu-item');
      console.log('一级菜单数量:', firstLevel.length);

      // 如果使用 find,则会包含子菜单里的 li
      var allItems = $('.menu').find('li');
      console.log('所有 li 数量:', allItems.length);
    });
  </script>
</body>
</html>

看到区别了吗?children 对于层级展开收起之类的操作非常友好,因为展开时你只操作当前层的子元素,不会误伤深层级。

3.3 用 closest 从下往上精准命中

closest 方法可以说是从子到父遍历的明星方法。它从调用它的节点开始,向上查找,返回第一个匹配选择器的祖先节点。你可以把它理解为“向上找最近的符合条件的人”。在很多事件处理中,我们并不知道用户点击的元素具体是哪一个,但我们一定知道要操作的大目标是什么。closest 正好帮我们抹平了中间层级的差异。

比如在表格中,用户点击某个操作按钮,我们需要找到它所在的整行。在之前的例子里已经展示过。这里再强调一下,closest 可以接受任意 jQuery 支持的选择器,包括类、标签等,非常灵活。相比手动 parent 链,closest 让代码更健壮,性能也更好,因为它不需要生成大量临时 jQuery 对象。

3.4 用 parent 和 parents 及时刹车

既然 closest 那么强大,是不是就不需要 parent 和 parents 了呢?也不是。当你明确知道目标节点就在上一层时,直接使用 parent 方法是最快最轻量的。比如一个 div 里面有一个 span,你想给这个 div 加个背景色,那完全可以先拿到这个 span 的上一级 div,不需要向上搜索多层。另一方面,parents 方法会返回所有匹配的祖先节点,如果你确实需要做类似“检查某个节点是否存在某个祖先”的功能,用 parents 再配合判断也可以。但注意,parents 会返回一长串节点,如果要找的只是第一个,那请用 closest。这里的原则很简单:层级越浅,目标越明确,就用 parent;需要向上跨越很多层且不确定在哪一层,就用 closest。

我们来对比一个实际例子:

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>jQuery parent 与 closest 对比示例</title>
  <style>
    table { border-collapse: collapse; }
    td { border: 1px solid #ccc; padding: 8px; }
    .operate { color: blue; cursor: pointer; }
  </style>
</head>
<body>
  <table class="data-table">
    <tr>
      <td>张三</td>
      <td><span class="operate">编辑</span></td>
    </tr>
    <tr>
      <td>李四</td>
      <td><span class="operate">编辑</span></td>
    </tr>
  </table>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      $('.operate').on('click', function () {
        // 第一种:用两次 parent 找到 tr(脆弱写法,不推荐)
        var trByParent = $(this).parent().parent();
        // 第二种:用 closest 找到 tr(推荐,清晰且健壮)
        var trByClosest = $(this).closest('tr');
        // 比较是不是同一个原生的 DOM 节点
        console.log(trByParent.get(0) === trByClosest.get(0));
      });
    });
  </script>
</body>
</html>

这里输出了 true,说明二者结果一样,但开发体验完全不同。

四、结合缓存与链式操作减少损耗

4.1 把查找结果缓存起来

遍历性能的另一个大敌是重复工作。我们经常在点击事件里面写类似“选择某一个类名的元素”这样的查询,每次点击都会执行一次完整的查找。虽然 jQuery 内部有选择器引擎优化,但一次次重复查找,浪费积累起来也很可观。尤其在一个事件函数里,同一个节点被查询了多次,这就是完全可以避免的浪费。解决办法很简单:把找到的 jQuery 对象存到局部变量里,后续直接使用这个变量。

我们看一个简单的交互:

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>jQuery 缓存对象示例</title>
</head>
<body>
  <div class="panel">
    <p class="message">欢迎光临</p>
    <button id="changeBtn">换个提示</button>
  </div>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      // 把常用的节点存下来,避免每次点击都重新查找
      var $message = $('.message');
      var $btn = $('#changeBtn');

      $btn.on('click', function () {
        $message.text('今天的效率很高!');
        // 后续还可以继续使用 $message,不会再次遍历文档
        $message.addClass('highlight');
      });
    });
  </script>
</body>
</html>

这里每次点击只是修改文本和样式,并没有重新查询节点,所以即使点击一百次,也是只对同一个缓存对象操作,性能损耗几乎为零。

4.2 用 end 方法保持链式整洁

jQuery 的链式写法,可以让多步操作连成一条线。但链式过程中,我们常常需要临时去查找某个子节点,然后再回到之前的节点继续操作。如果不用 end 方法,你可能得先把父节点存成变量,然后再写两行代码。而 end 方法就像一条回程的车票,能把选择器集合恢复到上一次操作之前的状态。比如,一个卡片节点,你先用 find 找到里面的标题,给标题加粗,然后通过 end 回到卡片节点,再给卡片加边框。整个过程既流畅又清晰。不过也要注意,end 方法并不能从根本上提升性能,它只是帮你少写一个临时变量,让代码更紧凑。

五、实际场景里的优化演示

5.1 场景:动态列表的批量操作

在实际业务里,动态列表非常常见。比如后端返回一批待办事项,前端循环生成列表。用户点击列表中的复选框,需要给整一项添加高亮。如果我们在生成列表时给每个复选框绑定事件,那么每个事件处理函数都要占用一份内存,消耗较大。更好的做法是在容器上绑定事件,再通过 closest 在事件回调里定位当前项。这既减少了事件数量,也优化了节点查找路径。

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>动态列表批量操作示例</title>
  <style>
    .to-do-item { padding: 10px; border-bottom: 1px solid #eee; }
    .to-do-item.active { background-color: #fff3cd; }
  </style>
</head>
<body>
  <div id="todoList">
    <div class="to-do-item">
      <input type="checkbox" class="select-box"> 写周报
    </div>
    <div class="to-do-item">
      <input type="checkbox" class="select-box"> 修Bug
    </div>
  </div>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      $('#todoList').on('change', '.select-box', function () {
        // 通过 closest 找到包含该复选框的待办项
        var $item = $(this).closest('.to-do-item');
        // 判断当前复选框是否选中,然后切换样式
        if (this.checked) {
          $item.addClass('active');
        } else {
          $item.removeClass('active');
        }
      });
    });
  </script>
</body>
</html>

这里的事件委托,实际上减少了对每个复选框单独绑定的开销。配合 closest,我们只需要在事件发生时执行一次短小的向上查找,就能精准定位到目标节点。这是非常典型的优化。

5.2 场景:树形菜单的展开收起

树形菜单是另一个父子遍历的重灾区。很多树形菜单有四五层嵌套,如果你不小心用 find 去找子节点,可能会把整个树的所有子节点全部选中。比如你只想展开“前端技术”这一层,但 find 却把“jQuery”的子级也同时拿出来了,这就会导致所有层级一起展开,页面逻辑完全失控。使用 children 可以避免这种问题,因为它只关心当前节点的直接子级。

<!-- 技术栈:jQuery 3.6.0 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>jQuery 树形菜单示例</title>
  <style>
    .tree ul { margin-left: 20px; display: none; }
    .tree li { cursor: pointer; padding: 4px 0; }
    .tree li.open > ul { display: block; }
  </style>
</head>
<body>
  <ul class="tree">
    <li> 前端技术
      <ul>
        <li>HTML</li>
        <li>CSS</li>
        <li class="has-sub">jQuery
          <ul>
            <li>选择器</li>
            <li>遍历</li>
          </ul>
        </li>
      </ul>
    </li>
    <li>后端技术</li>
  </ul>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
    // 技术栈:jQuery 3.6.0
    $(function () {
      $('.tree li').on('click', function (e) {
        // 阻止事件冒泡,避免触发父级的点击
        e.stopPropagation();
        var $li = $(this);
        // 找到当前 li 下的直接子级 ul,而不是更深层级的 ul
        var $subMenu = $li.children('ul');

        // 如果有子菜单且当前还没有展开,就展开并加一个 open 类
        if ($subMenu.length > 0) {
          if ($li.hasClass('open')) {
            $li.removeClass('open');
            $subMenu.hide();
          } else {
            $li.addClass('open');
            $subMenu.show();
          }
        }
      });
    });
  </script>
</body>
</html>

在这个例子里,获取当前层级的 ul 只会拿到当前那一层下面的子节点,那些更深的嵌套不会被干扰,所以每次点击都只会操作当前层,不会把整个树都展开。运行一下,你就能体会到这种“精确控制”带来的清爽。

六、关联技术:jQuery 选择器与浏览器原生 API

聊到这里,有必要提一下 jQuery 选择器背后的引擎。jQuery 内部使用了一个名为 Sizzle 的选择器引擎,它在老浏览器上实现了和现代浏览器类似的选择器功能。这也意味着,jQuery 在选择器处理上会有额外的逻辑开销。当你的页面运行在最新的 Chrome、Edge 或 Firefox 中时,原生 API 的速度通常会更加优秀。因此,如果你可以接受原生 JavaScript 的写法,那么使用浏览器提供的查询方法,会让性能更好。

不过,jQuery 也有它独特的好处:它把不同浏览器的差异屏蔽掉了,写出来的代码风格统一,团队协作时心智负担更低。很多老项目里,jQuery 已经和业务深度融合,替换成本很高。所以在这篇文章里,我们并不强迫大家放弃 jQuery,而是希望你在使用 jQuery 时,能注意遍历的方式,避免做一些低效的选择。等到未来某天项目升级,你会发现,用这些思路写出来的代码,迁移到原生 API 也非常容易。

七、优缺点总结

到这里,我们可以把 jQuery 中的节点遍历优化做一个梳理。

优点方面:通过合理使用 children、find、closest 这些方法,代码的可读性和健壮性都会提升。你不必去关心中间具体的层级变化,closest 会自动帮你找到最近的匹配祖先,children 能清晰地表达“只关心直接子级”的意图,find 配合局部容器又能快速定位任意后代。这些方法组合在一起,既能提高开发效率,也能让后续维护者更容易理解你的逻辑。

缺点方面:jQuery 自身毕竟是第三方库,需要进行兼容性处理,选择器执行时会有函数调用开销。如果你的项目里大量使用全局模糊查找,而且页面节点数动辄上万,再优化的 jQuery 写法也比不上原生 API 来得快。另外,过分依赖链式调用和临时对象,比如写出一大长串的 parent 链,也会造成性能下降。所以,这些技巧更像是“在现有条件下尽量少走弯路”,而不是“让 jQuery 飞起来”。

八、注意事项

在实际开发中,有几个细节需要特别留意。

第一,不要在一个高频事件中使用全局查找。比如调整窗口大小、页面滚动、鼠标移动这些事件,回调函数本身触发频率极高,如果每次回调里都做全局查找,页面很快就会卡顿。正确的做法是在事件外先缓存结果,事件回调只做简单的样式切换。

第二,谨慎使用通配符选择器。那种会匹配所有元素的选择器,遍历开销极大。除非你明确知道自己在做什么,否则不要轻易使用。

第三,closest 虽然好用,但它只能向上查找,不能向下。如果你想找的节点既可能在祖先层级,也可能在后代层级,通常需要拆成两个步骤。不要指望 closest 能向下搜索。

第四,children 和 find 返回的节点集合有区别,使用时务必明确你的目标。如果你要的是所有后代,就直接用 find;如果只要直接子级,就优先用 children。写错会导致找出来的节点数量不对,进而引发逻辑错误。

第五,尽量减少 jQuery 对象的创建次数。每一次把普通节点包装成 jQuery 对象,都会生成一个新的包装对象。把多次使用的同一个节点存进变量,是成本最低、效果最明显的优化方式。

第六,在维护旧项目时,你可能还会遇到旧版 jQuery 的兼容性问题。旧版本的选择器引擎可能更慢,建议在条件允许时将 jQuery 升级到 3.x 版本,或在升级前充分测试页面性能。

九、结语

节点遍历是 jQuery 日常使用中最基础的操作之一,但基础不意味着不重要。一个小小的遍历写法差异,放到大型页面上可能就会产生肉眼可见的卡顿。今天我们聊了 find 与 children 的分工,也聊了 closest 与 parent 链的取舍,还谈到了缓存和事件委托带来的收益。希望你在下次写 jQuery 代码时,能多花几秒钟想一想:我这次查找,范围是否足够小?层级是否足够精准?这样的小思考,积累起来就是明显的性能提升。如果文章中提到的场景已经戳中了你的痛点,不妨把你自己的代码改造成更高效的方式,感受一下顺滑的操作体验。