遍历node.children与使用node.querySelectorAll移除DOM类名的性能差异原因咨询
你好呀!看了你的测试代码和结果,完全理解你的疑惑——本来以为直接操作内存里的子元素集合会比DOM查询更快,结果实际跑出来method_1(用querySelectorAll)快了几十倍,这确实有点反直觉对吧?我来帮你拆解下背后的原因:
核心原因拆解
原生方法的底层优势:浏览器的
querySelectorAll这类DOM API是用C++实现的,属于浏览器内核的底层原生代码,执行效率远高于JavaScript层面的循环逻辑。JS作为解释型语言,手写的循环在执行速度上天生就比编译后的浏览器原生代码慢一大截,更别说浏览器还会对选择器匹配做各种优化(比如利用样式表缓存、DOM树索引等)。HTMLCollection的动态特性坑:你用到的
p.children是一个动态的HTMLCollection对象,不是静态数组。每次你在循环里访问c[i]的时候,浏览器都会重新去查询父元素的子节点列表,相当于每一次循环都偷偷做了一次DOM查询,这会带来极大的额外性能开销。而querySelectorAll返回的是静态的NodeList,只在调用时查询一次DOM,之后就把匹配的元素存在内存里了,遍历过程完全不需要再触碰DOM。遍历目标的数量级差异:你的测试里只有400个带
.sel类的元素,method_1只需要遍历这400个元素并移除类名;但method_2要遍历全部100000个元素,哪怕绝大多数元素本来就没有.sel类,还是要执行一次classList.remove操作——这中间的工作量差了250倍,时间差距自然就拉开了。
补充验证小技巧
如果你把method_2里的c = p.children改成静态数组,比如:
let c = Array.from(p.children);
再跑测试的话,性能会有明显提升(不过还是比不过querySelectorAll,因为原生方法的优势还在);另外,如果在method_2里加个判断,只给有.sel类的元素执行移除操作,也能减少不少无用功:
for (i =0; i < l; i++) { if (c[i].classList.contains('sel')) { c[i].classList.remove('sel'); } }
你的测试代码与结果
测试代码
let p = document.querySelector('.parent'), f = document.createDocumentFragment(), c = p.children ; function populate(n,mod) { for (let d, i =0; i < n; i++) { d = document.createElement("DIV"); if ( i % mod === 0 ) d.className = "sel"; f.append(d); } p.append(f); console.log(`populated total of ${n} having ${p.querySelectorAll('.sel').length} selected.`); } function method_1() { let n = Date.now(); p.querySelectorAll('.sel').forEach( v => v.classList.remove('sel')); console.log(`method_1 elapsed: ${Date.now() - n}`); } function method_2() { let n = Date.now(), i, l = c.length ; for (i =0; i < l; i++) { c[i].classList.remove('sel'); } console.log(`method_2 elapsed: ${Date.now() - n}`); } populate(100000,250); method_1() console.log(`Remaining selected after method_1: ${p.querySelectorAll('.sel').length} selected.`); populate(100000,250); method_2(); console.log(`Remaining selected after method_2: ${p.querySelectorAll('.sel').length} selected.`);
<div class="parent"></div>
测试结果
populated total of 100000 having 400 selected.
method_1 elapsed: 1
Remaining selected after method_1: 0 selected.
populated total of 100000 having 400 selected.
method_2 elapsed: 84
Remaining selected after method_2: 0 selected.
内容来源于stack exchange

