你可能不知道的JavaScript 遍历DOM的几种方法
最近看到一篇关于前端优化方面的文章,里面提到了几点对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++) {// 遍历操作
}
对于上述代码,有以下几点建议:
- 缓存遍历
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
,而一百亿这个数量级不论对于什么编程语言来说都挺大的,绝大部分情况下根本达不到,所以除非有特定需求,为了代码写起来更符合人的惯性思维,最好还是遵循大部分人的习惯,例如使用正序,根本没必要为了那几乎可以忽略不计的性能差别放弃良好的阅读性
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
范围内,文本值为 li2
的 li
元素的 下一个兄弟节点的类名:
常用方法可能会这么写:
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
范围内,文本值为 li2
的 li
元素的 下一个兄弟节点的类名 这么简单,而是更加复杂的 DOM
操作和场景,大概也就能体现出此API
的好处了,至于什么样的场景嘛,这个,我懒得去想,遇到了自然就知道了。
TreeWalker
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
简单介绍下:
- 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
你可能不知道的JavaScript 遍历DOM的几种方法相关推荐
- Javascript 遍历对象的四种方法
方法一: for - in 循环遍历对象自身的和继承的可枚举属性 (循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)) 方法二: 使用Object.keys()遍历 (返回一个数组,包括对 ...
- LHS 和 RHS---你所不知道的JavaScript
目录 1.LHS(Left Hand Side)和 RHS(Right Hand Side) 2.实例详解 3.总结 变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没 ...
- keras 香草编码器_用香草javascript遍历dom
keras 香草编码器 If you're new to JavaScript, you're likely starting out with Vanilla JavaScript in order ...
- php 解析java map,java_java遍历Map的几种方法分析,本文实例分析了java遍历Map的几 - phpStudy...
java遍历Map的几种方法分析 本文实例分析了java遍历Map的几种方法.分享给大家供大家参考,具体如下: Java代码: Map map=new HashMap(); map.put(" ...
- php遍历数组哪个效率高,PHP遍历数组的三种方法及效率对比分析
PHP遍历数组的三种方法及效率对比分析 发布于 2015-03-04 21:55:27 | 129 次阅读 | 评论: 0 | 来源: 网友投递 PHP开源脚本语言PHP(外文名: Hypertext ...
- [转]C#遍历局域网的几种方法及比较
http://blog.163.com/ldy_3881685/blog/static/32380136200954112940184/ C#遍历局域网的几种方法及比较 2009-06-04 11:2 ...
- js遍历对象的几种方法
js遍历对象的几种方法 第一种: for......in const obj = {id:1,name:'zhangsan',age:18 }for(let key in obj){console.l ...
- python遍历数组的两种方法及将print的内容写入文件中
python遍历数组的两种方法 第一种,最常用的,通过for in遍历数组 colours = ["red","green","blue"] ...
- JavaScript 获得对象的N种方法 [转]
JavaScript 获得对象的N种方法 [转] 方法如下: document.getElementById(ID) //获得指定ID值的对象 document.getElementsB ...
最新文章
- java 进程消失_Java进程诡异消失问题
- 水准网测量平差matlab_【干货】史诗级测量大神分享道路测量全过程经验
- 怎么把PDF文件空白页删除
- 循环神经网络基础介绍
- Linux底层网络编程--ARP,PING等
- 【转】如何用Maven创建web项目(具体步骤)
- mac下sublime text的使用
- 打基础一定要吃透这12类 Python 内置函数
- 10g索引的作用实验1
- POJ 3667 Hotel 线段树区间合并
- ajax兼容写法,Ajax的兼容性问题
- 10天学会c语言与单片机第6讲,10天学会单片机c语言视频
- 清华姚班和100个“张小龙” | 中国AI天才养成计划
- 动量法与指数加权移动平均
- C++文件操作之写文件
- nas做服务器虚拟化共享存储,NAS虚拟化的部署及实现解析
- 手机投影到电脑上做演示
- 低功耗设计(low power design)和UPF介绍(含代码示例)
- RabbitMQ实现订单超时设计思路、以及在订单过期临界点支付成功如何处理
- Unity开发基础——使用字符串学习笔记
热门文章
- 呵护眼睛,从小事做起
- C语言的stdio.h文件
- C ++ 程序调用CUDA静态库
- Scikit-learn机器学习实战之Kmeans
- 【数据结构】递归斐波那契数列的时间复杂度、空间复杂度
- CNA, FCoE, TOE, RDMA, iWARP, iSCSI等概念及 Chelsio T5 产品介绍
- 【从RL到DRL】深度强化学习基础(四)——策略梯度中的Baseline,Reinforce与A2C的对比
- java模拟实现操作系统进程调度中的多级反馈队列算法
- 基于强化学习SAC_LSTM算法的机器人导航
- C++ for循环效率优化