我平时看别人写的页面代码,最先注意的往往不是用了什么框架,而是它对 DOM 的访问方式。原因是这个环节最容易暴露对浏览器渲染模型的理解深度。明明只是取一个元素、改一段文本、给某个节点挂个事件,处理不好照样会把页面搞卡、把数据搞错,甚至在安全上开一个口子。这个话题很基础,但它从选择器性能一路延伸到异步渲染时机、动态列表、XSS 防护,水很深。
这篇文章我会借几个真实项目里碰过的问题,把 DOM 访问从“查节点”这层表象往下拆。先讲访问策略的整体取舍,再讲性能层面的坑,然后用一个完整的 Ajax 数据渲染场景把 JSON、HTML、XML 三类常见数据的落地方式串起来,顺带解析一遍 echarts 取不到容器宽高的经典报错,最后专门聊聊 DOM 型 XSS 为什么会在访问和写入之间产生。适合正准备把原生 JavaScript 用扎实、想搞清“为什么这里会慢/会炸/会有漏洞”的前端开发者。
1. 整体取舍:还没写代码前,先想清“从哪里访问”
1.1 原生选择器不是越新越好:getElementById 与 querySelector 的定位差异
DOM 访问第一件事是选节点。很多新手习惯拿着 querySelector 一套到底,因为 CSS 选择器写起来方便,能一次选到嵌套很深的目标。我承认在普通交互里这两种写法的差距肉眼几乎看不出来,但你要知道背后的运行逻辑是不同的。
getElementById 这类接口本质上是浏览器维护的一张哈希映射,按 id 直接定位,查找成本基本是常量。querySelector 则要先把选择器字符串做解析,再按选择器的结构去遍历候选节点。当你只在某个按钮点击事件里执行一次时,两者相差几乎可以忽略;一旦这种调用发生在循环、滚动回调或高频动画里,差距就会被放大。
举个实际例子。一次我在改造一个老项目,页面有一张几千行的表格,每一行都要根据状态更新某个单元格的文本。原代码在循环里用 querySelector 重复查找:
js复制for (let i = 0; i < rows.length; i++) {
const statusCell = rows[i].querySelector('.status-cell');
statusCell.textContent = statusMap[rows[i].dataset.id];
}
这段代码在数据量小时没毛病,但几千行的表格在低端机上明显卡顿。我的处理是把行状态映射先收集好,再直接通过行内的元素结构取子节点,或者先把每行的状态单元格用 getElementsByClassName 做一次扁平化查询,避免每行都重新做一次完整选择器解析。改造后体感顺畅很多。
我的结论是:优先使用目标明确的简单选择器或 getElementById;确需后代选择时再上 querySelector。querySelector 最忌出现“#app div ul li .active”这种一长串路径,选择器的可维护性和性能都不理想。
1.2 HTMLCollection 和 NodeList,访问前先分清“活”还是“死”
DOM 访问过程中很多人会被一个隐藏特性质疑:从 children、getElementsByTagName 返回的集合,和 querySelectorAll 返回的集合,行为并不完全一致。
children 和 getElementsBy* 返回的是 HTMLCollection,特点是实时集合。页面结构一旦变化,集合会自动更新。这个特性在某些场景是好用的,比如你要持续追踪一类元素的数量。但它也会产生一个经典问题:遍历时删除元素,下标会错乱。
看这段代码:
js复制const items = document.getElementsByClassName('item');
for (let i = 0; i < items.length; i++) {
if (shouldRemove(items[i])) {
items[i].remove();
}
}
问题很明显,每次 remove 都会让集合实时变化,length 变小、后面的元素下标前移,结果会跳过一些本该处理的元素。而 querySelectorAll 返回的是静态 NodeList,它获取的是调用那一刻的快照,遍历时怎么改页面都不会影响这份集合的内容。
childNodes 又是一个特殊情况,它虽然是 NodeList,但属于活集合。所以不能仅凭集合类型判断,要看接口语义。我的经验是:凡是遍历过程中会修改 DOM 的,尽量先把集合转成普通数组,或者用 while 循环从末尾往前删,避免集合动态性带来的下标问题。
1.3 访问前确认目标节点真的“在场上”
真实的项目里,null 错误是出现频率最高的问题之一。常见报错是 Cannot read properties of null,翻译成大白话就是:你想访问一个不存在
