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

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

常用遍历方法

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

例如下述 HTML

<ul class="box"><li class="li_1">1</li><li class="li_2">2</li><li class="li_3">3</li><li class="li_4">4</li><li class="index_5"><ul class="box1"><li class="ll1">li1</li><li class="ll2">li2</li><li class="ll3">li3</li></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.lengthlet 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

你可能不知道的JavaScript 遍历DOM的几种方法相关推荐

  1. Javascript 遍历对象的四种方法

    方法一: for - in 循环遍历对象自身的和继承的可枚举属性 (循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)) 方法二: 使用Object.keys()遍历 (返回一个数组,包括对 ...

  2. LHS 和 RHS---你所不知道的JavaScript

    目录 1.LHS(Left Hand Side)和 RHS(Right Hand Side) 2.实例详解 3.总结 变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没 ...

  3. keras 香草编码器_用香草javascript遍历dom

    keras 香草编码器 If you're new to JavaScript, you're likely starting out with Vanilla JavaScript in order ...

  4. php 解析java map,java_java遍历Map的几种方法分析,本文实例分析了java遍历Map的几 - phpStudy...

    java遍历Map的几种方法分析 本文实例分析了java遍历Map的几种方法.分享给大家供大家参考,具体如下: Java代码: Map map=new HashMap(); map.put(" ...

  5. php遍历数组哪个效率高,PHP遍历数组的三种方法及效率对比分析

    PHP遍历数组的三种方法及效率对比分析 发布于 2015-03-04 21:55:27 | 129 次阅读 | 评论: 0 | 来源: 网友投递 PHP开源脚本语言PHP(外文名: Hypertext ...

  6. [转]C#遍历局域网的几种方法及比较

    http://blog.163.com/ldy_3881685/blog/static/32380136200954112940184/ C#遍历局域网的几种方法及比较 2009-06-04 11:2 ...

  7. js遍历对象的几种方法

    js遍历对象的几种方法 第一种: for......in const obj = {id:1,name:'zhangsan',age:18 }for(let key in obj){console.l ...

  8. python遍历数组的两种方法及将print的内容写入文件中

    python遍历数组的两种方法 第一种,最常用的,通过for in遍历数组 colours = ["red","green","blue"] ...

  9. JavaScript 获得对象的N种方法 [转]

    JavaScript 获得对象的N种方法 [转] 方法如下: document.getElementById(ID)        //获得指定ID值的对象 document.getElementsB ...

最新文章

  1. java 进程消失_Java进程诡异消失问题
  2. 水准网测量平差matlab_【干货】史诗级测量大神分享道路测量全过程经验
  3. 怎么把PDF文件空白页删除
  4. 循环神经网络基础介绍
  5. Linux底层网络编程--ARP,PING等
  6. 【转】如何用Maven创建web项目(具体步骤)
  7. mac下sublime text的使用
  8. 打基础一定要吃透这12类 Python 内置函数
  9. 10g索引的作用实验1
  10. POJ 3667 Hotel 线段树区间合并
  11. ajax兼容写法,Ajax的兼容性问题
  12. 10天学会c语言与单片机第6讲,10天学会单片机c语言视频
  13. 清华姚班和100个“张小龙” | 中国AI天才养成计划
  14. 动量法与指数加权移动平均
  15. C++文件操作之写文件
  16. nas做服务器虚拟化共享存储,NAS虚拟化的部署及实现解析
  17. 手机投影到电脑上做演示
  18. 低功耗设计(low power design)和UPF介绍(含代码示例)
  19. RabbitMQ实现订单超时设计思路、以及在订单过期临界点支付成功如何处理
  20. Unity开发基础——使用字符串学习笔记

热门文章

  1. 呵护眼睛,从小事做起
  2. C语言的stdio.h文件
  3. C ++ 程序调用CUDA静态库
  4. Scikit-learn机器学习实战之Kmeans
  5. 【数据结构】递归斐波那契数列的时间复杂度、空间复杂度
  6. CNA, FCoE, TOE, RDMA, iWARP, iSCSI等概念及 Chelsio T5 产品介绍
  7. 【从RL到DRL】深度强化学习基础(四)——策略梯度中的Baseline,Reinforce与A2C的对比
  8. java模拟实现操作系统进程调度中的多级反馈队列算法
  9. 基于强化学习SAC_LSTM算法的机器人导航
  10. C++ for循环效率优化