你可能不知道的JavaScript 遍历DOM的几种方法

最近看到一篇关于前端优化方面的文章,里面提到了几点对DOM遍历操作的优化,文章只是一笔带过,并没有深入的讲解。

其中提到了几个优化的手段,乍一看似乎没见过,然后再仔细想想,其实无论是犀牛书还是红宝书都有对此的描述,只是当初年轻时看书看到这里,觉得好像没什么屁用,将之当成可有可无的东西随便扫一眼就跳过了,现在既然碰上了,那就好好回顾一下吧。

常用遍历方法

对于 DOM的遍历,大部分情况下,都是直接使用选择器,选取到需要的DOM,然后对其进行遍历,并在遍历的同时进行各种筛选等操作。

例如下述 HTML

<ul class="box">
  <li class="li_1">1li>
  <li class="li_2">2li>
  <li class="li_3">3li>
  <li class="li_4">4li>
  <li class="index_5">
    <ul class="box1">
      <li class="ll1">li1li>
      <li class="ll2">li2li>
      <li class="ll3">li3li>
    ul>
  li>
ul>

对其中的 li元素进行选取遍历操作:

let eleList = document.querySelectorAll('li')
for (let i = 0; i < eleList.length; i++) {
  // 遍历操作
}

对于上述代码,有以下几点建议:

  1. 缓存遍历 DOM的长度,避免重复获取,例如 let eleLen = eleList.length

  2. 缓存可能多次使用到的 DOM元素,避免重复获取,DOM操作是比较耗费性能的,多申请一个变量缓存下可能会多次使用的 DOM,可明显提升性能

  3. 如果只是查找某个符合要求的元素,则在查找到目标后使用 break跳出循环,避免无效遍历

  4. 尽量手动缩小遍历的范围,例如,如果你已经确定所需要选取的元素是在 ul.box1中,那么就不要遍历其他无关数据,let eleList = document.querySelector('ul.box1')

  5. 如果所遍历的 DOM数组量级较大,可以考虑使用倒序循环,例如 for (let i = eleLen; i--;) {//...}

    此观点忘记在哪里看到的了,专门对此验证过,确实倒序更快,不过并没有快出多少
    我在 chrome控制台以及 node.js v8REPL环境下单纯地使用 for遍历,不加其他任何操作,遍历一百亿次,倒序所需时间比正序只少了大约 1s,而一百亿这个数量级不论对于什么编程语言来说都挺大的,绝大部分情况下根本达不到,所以除非有特定需求,为了代码写起来更符合人的惯性思维,最好还是遵循大部分人的习惯,例如使用正序,根本没必要为了那几乎可以忽略不计的性能差别放弃良好的阅读性

NodeIterator

前面的常用遍历操作完全能够胜任几乎所有的 DOM遍历,哪怕是一些很复杂的操作也不在话下,当然,避免不了一大堆的 if...else判断,从编程的角度来看也不够灵活,总觉得代码更偏向于业务而缺少灵活性

当然,这本身并没有什么问题,只要能在不留坑的前提顺利解决问题,过程不重要,只是这样一来就没办法体现出自己与其他妖艳贱货之间的区别,也没办法更技术性地敲(zhuang)码(bi)了。

DOM2 NodeIterator 就是解决此矛盾的利器之一

由于是 DOM2规范,所以可能有的浏览器不支持(IE9+),可使用下列代码检测支持性:

const supportNodeIterator = typeof document.createNodeIterator === 'function'

不同于直接使用 document.querySelectorAll返回的 DOM数据集,NodeIterator采用深度优先遍历,也就是数据结构树可能类似于一棵二叉树。

使用 createNodeIterator创建 NodeIterator结构类型,此类型实际上是一个节点迭代器,而不是普通 DOM数组

const iterator = document.createNodeIterator(root, whatToShow, filter, entityReferenceExpansion)

其中,root是你要遍历的 DOM范围,类似于 document.querySelectorAll('li')

whatToShow,可选参数,默认为 SHOW_ALL,是一个位掩码,功能类似于一个过滤器,确定要遍历的节点,可类比一个 if判断,关于此参数取值可见 MDN

filter,可选参数,是一个 NodeFilter对象,或者是一个表示应该接受还是拒绝某种特定节点的函数,在whatToShow进行再一次的过滤,确定最终所需要选取的节点

entityReferenceExpansion,可选参数,是一个 Boolean值,表示是否要扩展实体引用,此参数在 HTML页面无用,此参数已经 deprecated

针对本文最上面那段 HTML,进行如下操作:获取 ul.box范围内,文本值为 li2li元素的 下一个兄弟节点的类名:

常用方法可能会这么写:

 let liEle = document.querySelector('ul.box').querySelectorAll('li')
 let liLen = liEle.length
 let targetEle = []
 for (let i = 0; i < liLen; i++) {
   if (liEle[i].innerHTML === 'li2') {
     targetEle.push(liEle[i].nextElementSibling.getAttribute('class'))
   }
 }
 console.log(targetEle)

而使用 NodeIterator就可以这么写:

let targetEle = []
let nodeIterator = document.createNodeIterator(
  document.querySelector('.box'),
  NodeFilter.SHOW_TEXT,
  { acceptNode(node) {
      if (node.data === 'li2') {
        return NodeFilter.FILTER_ACCEPT
      }
    }
  }
);
while((node = nodeIterator.nextNode())){
  targetEle.push(node.parentNode.nextElementSibling.className)
}
console.log(targetEle)

似乎 NodeIterator的写法代码量更多了,从上述代码来看确实是这样,不过存在必有道理,既然在DOM 2中专门添加了此 API,说明有其用武之地,其返回的是一个 iterator对象,方便某些场景下的操作

另外,如果需要进行的操作不是 获取 ul.box范围内,文本值为 li2li元素的 下一个兄弟节点的类名 这么简单,而是更加复杂的 DOM操作和场景,大概也就能体现出此API的好处了,至于什么样的场景嘛,这个,我懒得去想,遇到了自然就知道了。


TreeWalker

TreeWalker 也是 DOM 2规范中的东西,而且算是 NodeIterator的高级版本,后者有的东西,它基本上都有,而且除此之外,还提供用于在不同方向上遍历 DOM结构的方法

可使用以下代码判断浏览器是否支持此特性:

const supportsTreeWalker = typeof document.createTreeWalker === 'function'

其同样创建了一个节点迭代器:

const iterator = document.createTreeWalker(root, whatToShow, filter, entityReferenceExpansion)

四个参数的含义和 NodeIterator是一样的

TreeWalkerNodeIterator 的用法差不多,除了前者比后者多出一个能够在 DOM结构的任何方向上进行移动迭代,在某些情况下,是可以简化操作的。

例如,针对 获取 ul.box范围内,文本值为 li2li元素的 下一个兄弟节点的类名,你已经明确地知道,所要查找的元素在 ul.box的最后一个 li节点中,则可以这样简化操作:

let targetEle = []
let treeWalker = document.createTreeWalker(
  document.querySelector('.box'),
  NodeFilter.SHOW_ELEMENT
)
treeWalker.lastChild()
while((node = treeWalker.nextNode())){
  if (node.innerHTML === 'li2') {
    targetEle.push(node.className)
  }
}
console.log(targetEle)

使用 lastChild()直接定位到 .box的最后一个元素子节点,然后进行遍历


XPath

简单介绍下:

  1. XPath 即为XML路径语言
  2. 是一门在 XML 文档中查找信息的语言
  3. 用来确定XML(标准通用标记语言的子集)文档中某部分的位置
  4. 含有超过 100 个内建的函数。这些函数用于字符串值、数值、日期和时间比较、节点和 QName 处理、序列处理、逻辑值等等
  5. XPath 于 1999 年 11 月 16 日 成为 W3C 标准

当它得到原生支持的时候,能提供更快的方法。对XPath 查询引擎的优化可以比直接解释 JavaScript 快得多。在某些情况下,甚至高达两个数量级的速度提升。

虽然 XPath被定义为 XML的相关语言,但由于 XMLHTML之间存在的某些共同联系,使得 XPath实际上也能在 HTML中使用,XPath的主要作用就是在文档中查找元素的位置,由于此特性,不难理解为何其在某些场景下,查询文档的速度远比单纯的JavaScript 要快了。

XPath语法可见 菜鸟教程 XPath语法

使用 document.evaluate来创建一个 XPathResult对象

var xpathResult = document.evaluate(
 xpathExpression, 
 contextNode, 
 namespaceResolver, 
 resultType, 
 result
)

其中,参数如下,参(zhao)考(chao) MDN

  1. xpathExpression:表示要计算的 Xpath字符串,例如 //h2|//h3|,其意思就是选取 h2h3元素
  2. contextNode:表示本次查询的上下文节点,通常为 document,或者你也可以自己缩小查询范围
  3. namespaceResolver:是一个函数,传入名空间前缀,返回跟此前缀相关的名空间URI(字符串)。通常用来解析Xpath内的前缀,以便对文档进行匹配。HTML文档或者不使用名空间前缀的文档,通常传入null
  4. resultType: 是整数。指定所返回的 XPathResult的类型,常常使用 named constant properties,如 XPathResult.ANY_TYPE,范围 0 到 9
  5. result:为XPathResult型,用以存储查询结果。通常传入null,此时将创建新的XPathResult对象。

例如,如果想要在文档中查找所有的 h2h3元素,按照一般方法,可能会这么写:

let allElements = document.getElementsByTagName('*')
for(let i = 0; i < allElements.length; i++) {
    if(allElements[i].tagName.match(/^h[2-4]$/i)) {
        // …
    }
}

而如果采用 XPath的写法,可以是这样:

let headings = document.evaluate('//h2|//h3', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null)
let oneheading
while(oneheading = headings.iterateNext()) {
  // …
}

可以看到,document.evaluate返回的XPathResult对象,其实也是一个 节点迭代器。

同样,如果你已经确定了所需查找的 h2h3元素其实就在某个 div元素下,那么可以通过修改第二个参数为此 div,可以减少无效遍历,加快访问速度。

至于document.evaluate的兼容性可能并不需要担心,从目前 caniuse给出的数据看,除了 IE全系浏览器之外,其余浏览器全部几乎在诞生之初就全面支持此 API了,所以如果你不打算兼容 IE,那么直接使用即可。


总结

大多数情况下,DOM遍历最好采用第一种 for循环遍历,符合大多数人的习惯,较为直观,如果有特殊需求,例如大量复杂的 DOM操作,DOM需要被频繁地获取和计算,那么可以视情况选择 NodeIterator 或者 TreeWalker的方式,如果你不想兼容 IE,那么推荐尝试一下 XPath

你可能感兴趣的:(Web前端)