在《DOM的操作》一节中知道querySelectorAll()
和getElementsByTagName()
两个方法都是用来查找DOM元素的。通过上一节的学习,知道querySelectorAll()
方法将获取到NodeList
对象,getElementsByTagName()
方法获取到的是HTMLCollection
对象。虽然他们获取的都是DOM动态集合,但两者还是略有差异的。今天我们就来看这两者之间的区别。
有关于querySelectorAll()
和getElementsByTagName()
两者的区别,这里推荐几篇文章:
@Jin的《简单讨论 querySelectorAll
Vs getElementsByTagName
区别》
@Nicholas C. Zakas 的《Why is getElementsByTagName()
faster than querySelectorAll()
?》
《为什么 getElementsByTagName
比 querySelectorAll
方法快》
为了能理解这两者之间的区别,接下来的内容和整个思路是跟着上面几篇文章进行的。
稍微接触过JavaScript的同学都应该知道,querySelectorAll()
和getElementsByTagName()
两个方法都是用来从DOM树中获取元素集合。如果简单的理解就是用来选择DOM元素。虽然表面上都是用来选择DOM元素,但事实并非如此,两者之间还有很大的区别:
深度优先遍历
如果阅读过上一节的内容,对于querySelectorAll()
和getElementsByTagName()
返回值的类型与状态,都有了一定的了解,但这里所说的遍历方式:深度优先 还是初次接触这个概念。那么为了后面的内容更易于理解,很有必要了解一下。
维基百科是这样描述深度优先遍历的:
深度优先搜索算法(英语:Depth-First-Search,简称DFS)是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点
v
的所在边都己被探寻过,搜索将回溯到发现节点v
的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。
简单点来描述:深度优先遍历从一路径的起始点开始追溯,直到遍历该路径的最后一个节点,然后回溯,继续追溯下一路径,依次类推,直到遍历完成。如上图所示。
深度优先遍历实现相对简单,通过递归即可完成。不断递归,直到本路径最后一个节点,然后回溯,继续递归。
除了深度优先遍历之外,还有广度优先遍历:
广度优先搜索算法(英语:Breadth-First-Search,缩写为BFS),又译作宽度优先搜索,或横向优先搜索,是一种图形搜索算法。简单的说,BFS是从根节点开始,沿着树的宽度遍历树的节点。如果所有节点均被访问,则算法中止。广度优先搜索的实现一般采用open-closed表。
广度优先遍历,则优先遍历同一层次最邻近的节点,然后再往下遍历上一层首个节点的下层节点。如下图所示:
DOM的结构和数据结构中的“树”型结构比较类似,所以很自然的就可以使用DFS和BFS进行遍历。
比如我们有一个这样的HTML结构:
对应的DOM树结构如下图所示:
对于DOM树的深度优先遍历,执行的结果应该如下:
写个函数来实现深度优先遍历:
浏览器打印出来的结果如下:
上面通过JavaScript使用了递归的方法实现了DFS,在控制台依次打印出节点的元素名,类名和层次。
深度优先可以理解为“一条路走到黑”,只有在撞到了“南墙”才回头。具体到DOM树中来说就是,从根节点开始,继而访问它的直接子元素,并依此往复直到不存在子元素。
再来看看对于DOM树的广度优先遍历的结果。广度优先可以理解为“一层一层的剥离”,对同一层次的元素全部遍历过后,再遍历下一层。广度优先适合使用队列这种数据结构来实现,将每层的节点依次放入队列,并根据队列“先入先出”的特性取出就可以了。在JavaScript中模拟队列的的方法可以使用数组方法的push
和shift
对应入队和出队操作。同样给出JavaScript实现的DOM树遍历。
对应的代码如下:
输出的结果如下:
有关于深度优先遍历和广度优先遍历更多的资料可以阅读:
深度优先遍历与广度优先遍历
关于DOM节点的深度优先和广度优先遍历
深度优先与广度优先方法对DOM树的遍历
JS 中的广度与深度优先遍历
假设你对深度优先遍历有了一定的了解。接下来回到querySelectorAll()
和getElementsByTagName()
世界中。使用这两个方法对DOM树进行遍历的思咱就是深度优先遍历算法,只不过节点对应着DOM树中的元素。
从图中的浏览器的控制台输出可以看出,两个方法返回的顺序都是一样的。返回的结果都是:
[ div.container, section.sidebar, ul.menu, li, a, li, a, section.main, article.paragraph, p.note ]
querySelectorAll()
和getElementsByTagName()
两者的主要区别就是返回值。前者返回的是NodeList
集合,后者返回的是HTMLCollection
集合。其前者是一个动态集合,后者是一个静态集合。
其中动态集合和静态集合的最大区别在于:
动态集合指的就是元素集合会随着DOM树元素的增加而增加,减少而减少;静态集合则不会受DOM树元素变化的影响。
NodeList
对象是一个节点的集合,是由Node.childNodes
和document.querySelectorAll()
返回的。NodeList
并不是都是静态的,也就是说Node.childNodes
返回的是动态的元素集合;querySelectorAll()
返回的是一个静态集合。
HTMLCollection
返回一个时时包括所有给定标签名称的元素的HTML集合,也就是动态集合。
上图已经告诉我们结果了。虽然root.appendChild(newEle)
增加了一个新的div
。但query.length
还是10
,而elements.length
却变成了11
。
有关于这方面更详细的介绍,可以阅读上一篇《动态集合》文章。
getElementsByTagName
比 querySelectorAll
方法快?通过上一节的学习,我们知道为什么动态NodeList
要比静态NodeList
更快。即:
使用
getElementsByTagName
方法我们得到的结果就像是一个对象的索引,而通过querySelectorAll
方法我们得到的是一个对象的克隆;所以当这个对象数据量非常大的时候,显然克隆这个对象所需要花费的时间是很长的。
这也就是为什么说getElementsByTagName()
在所有浏览器上都比querySelectorAll()
要快好多倍。
其中具体的原委早在2010年@Nicholas C. Zakas就做过相关的阐述,而且还提供了一份JSPerf测试页。
虽然道理明白了,但是希望自己动手撸一下代码,这样更能加强我们的理解。比如下面这样的一个测试用例:
当我们在body
下创建1000
个div
标签时,控制台打印出来的结果如下:
上面是刷新多次后的结果,接下来,把1000
个换成1000000
个,结果会是:
结果已经告诉我们了。当div
数量增加时,使用querySelectorAll()
方法所费的时间越来越长,而使用getElementsByTagName()
方法所费的时间并没太大的差异。从而再次验证:getElementsByTagName()
比querySelectorAll()
要快好多倍。
还有一点其实是需要我们注意的,我们使用的console.time
和console.timeEnd
方法得出来的时间并不是特别准确的;更准确的做法是使用Performance
这个对象提供的now
方法来进行计时。这里有一些文章关于为什么要使用Performance
的解释: Timing JavaScript Code with High Resolution Timestamps和Discovering the High Resolution Time API,接下来我们来修改一下上面的代码:
可以清楚地看到,随着div
标签数量的增多,使用querySelectorAll
方法会越来越慢,而使用getElementsByTagName
方法的速度却变化不大,这也说明了getElementsByTagName
方法确实比querySelectorAll
方法要快。
这就是querySelectorAll
和getElementsByTagName
不同之处。所以以后在项目中使用的时候,还是需要注意。不然一不小心,就掉坑里了。
文章涉及到图片和代码,如果展示不全给您带来不好的阅读体验,欢迎点击文章底部的 阅读全文。如果您觉得小站的内容对您的工作或学习有所帮助,欢迎关注此公众号。
W3cplus.com
————————————
记述前端那些事,引领web前沿
长按二维码,关注W3cplus
▼