最近看到一篇关于前端优化方面的文章,里面提到了几点对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++) {
// 遍历操作
}
对于上述代码,有以下几点建议:
- 缓存遍历
DOM
的长度,避免重复获取,例如let eleLen = eleList.length
- 缓存可能多次使用到的
DOM
元素,避免重复获取,DOM
操作是比较耗费性能的,多申请一个变量缓存下可能会多次使用的DOM
,可明显提升性能
- 如果只是查找某个符合要求的元素,则在查找到目标后使用
break
跳出循环,避免无效遍历
- 尽量手动缩小遍历的范围,例如,如果你已经确定所需要选取的元素是在
ul.box1
中,那么就不要遍历其他无关数据,let eleList = document.querySelector('ul.box1')
- 如果所遍历的
DOM
数组量级较大,可以考虑使用倒序循环,例如for (let i = eleLen; i--;) {//...}
此观点忘记在哪里看到的了,专门对此验证过,确实倒序更快,不过并没有快出多少
我在chrome
控制台以及node.js v8
的REPL
环境下单纯地使用for
遍历,不加其他任何操作,遍历一百亿次,倒序所需时间比正序只少了大约1s
,而一百亿这个数量级不论对于什么编程语言来说都挺大的,绝大部分情况下根本达不到,所以除非有特定需求,为了代码写起来更符合人的惯性思维,最好还是遵循大部分人的习惯,例如使用正序,根本没必要为了那几乎可以忽略不计的性能差别放弃良好的阅读性
前面的常用遍历操作完全能够胜任几乎所有的 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
范围内,文本值为 li2
的 li
元素的 下一个兄弟节点的类名:
常用方法可能会这么写:
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
范围内,文本值为 li2
的 li
元素的 下一个兄弟节点的类名 这么简单,而是更加复杂的 DOM
操作和场景,大概也就能体现出此API
的好处了,至于什么样的场景嘛,这个,我懒得去想,遇到了自然就知道了。
TreeWalker
也是 DOM 2
规范中的东西,而且算是 NodeIterator
的高级版本,后者有的东西,它基本上都有,而且除此之外,还提供用于在不同方向上遍历 DOM
结构的方法
可使用以下代码判断浏览器是否支持此特性:
const supportsTreeWalker = typeof document.createTreeWalker === 'function'
其同样创建了一个节点迭代器:
const iterator = document.createTreeWalker(root, whatToShow, filter, entityReferenceExpansion)
四个参数的含义和 NodeIterator
是一样的
TreeWalker
和 NodeIterator
的用法差不多,除了前者比后者多出一个能够在 DOM
结构的任何方向上进行移动迭代,在某些情况下,是可以简化操作的。
例如,针对 获取 ul.box
范围内,文本值为 li2
的 li
元素的 下一个兄弟节点的类名,你已经明确地知道,所要查找的元素在 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 即为XML路径语言
- 是一门在 XML 文档中查找信息的语言
- 用来确定XML(标准通用标记语言的子集)文档中某部分的位置
- 含有超过
100
个内建的函数。这些函数用于字符串值、数值、日期和时间比较、节点和QName
处理、序列处理、逻辑值等等- XPath 于 1999 年 11 月 16 日 成为 W3C 标准
当它得到原生支持的时候,能提供更快的方法。对
XPath
查询引擎的优化可以比直接解释 JavaScript 快得多。在某些情况下,甚至高达两个数量级的速度提升。
虽然 XPath
被定义为 XML
的相关语言,但由于 XML
与 HTML
之间存在的某些共同联系,使得 XPath
实际上也能在 HTML
中使用,XPath
的主要作用就是在文档中查找元素的位置,由于此特性,不难理解为何其在某些场景下,查询文档的速度远比单纯的JavaScript
要快了。
XPath
语法可见 菜鸟教程 XPath语法
使用 document.evaluate
来创建一个 XPathResult
对象
var xpathResult = document.evaluate(
xpathExpression,
contextNode,
namespaceResolver,
resultType,
result
)
其中,参数如下,参(zhao)考(chao) MDN
xpathExpression
:表示要计算的Xpath
字符串,例如//h2|//h3|
,其意思就是选取h2
或h3
元素contextNode
:表示本次查询的上下文节点,通常为document
,或者你也可以自己缩小查询范围namespaceResolver
:是一个函数,传入名空间前缀,返回跟此前缀相关的名空间URI
(字符串)。通常用来解析Xpath内的前缀,以便对文档进行匹配。HTML
文档或者不使用名空间前缀的文档,通常传入null
resultType
: 是整数。指定所返回的XPathResult
的类型,常常使用 named constant properties,如XPathResult.ANY_TYPE
,范围 0 到 9result
:为XPathResult
型,用以存储查询结果。通常传入null
,此时将创建新的XPathResult
对象。
例如,如果想要在文档中查找所有的 h2
和 h3
元素,按照一般方法,可能会这么写:
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
对象,其实也是一个 节点迭代器。
同样,如果你已经确定了所需查找的 h2
和 h3
元素其实就在某个 div
元素下,那么可以通过修改第二个参数为此 div
,可以减少无效遍历,加快访问速度。
至于
document.evaluate
的兼容性可能并不需要担心,从目前 caniuse给出的数据看,除了IE
全系浏览器之外,其余浏览器全部几乎在诞生之初就全面支持此API
了,所以如果你不打算兼容IE
,那么直接使用即可。
大多数情况下,DOM
遍历最好采用第一种 for
循环遍历,符合大多数人的习惯,较为直观,如果有特殊需求,例如大量复杂的 DOM
操作,DOM
需要被频繁地获取和计算,那么可以视情况选择 NodeIterator
或者 TreeWalker
的方式,如果你不想兼容 IE
,那么推荐尝试一下 XPath