你可能忽略的 DOM 扩展技巧:scrollIntoView、data-*、innerText 到性能优化

1. Selectors API(简要介绍)

Selectors API 是把 CSS 选择器直接用于 DOM 查找的统一接口,核心三个方法:

  • querySelector(selector)
    返回匹配 selector 的第一个元素(document 或任意 Element 上可用)。如果没有返回 null。适合查找单个元素或快速选择。
  • querySelectorAll(selector)
    返回所有匹配元素的 静态 NodeList(快照)。注意:返回的是静态节点集合,不会随着 DOM 修改自动更新。遍历或反复查询时注意性能。
  • matches(selector)
    检查当前元素是否匹配给定选择器,常用于事件委托中确认事件目标是否匹配。现代浏览器统一为 matches,历史上有前缀 webkitMatchesSelector / msMatchesSelector 等,可做兼容写法。

示例:

dart 复制代码
const firstBtn = document.querySelector('.btn.primary');
const allItems = document.querySelectorAll('.list > li');
if (someElement.matches(':scope > .active')) { /* ... */ }

性能建议:对于简单单一查找(如按 id)优先 getElementById;对大量选择或频繁操作,缓存查询结果或限制作用域(在容器 Element 上调用)可减少重绘/查询成本。


2. 元素遍历(通过这些 API 遍历树形)

核心属性/方法(只列元素节点相关,避免 text/comment):

  • element.children --- 返回子元素集合(HTMLCollection,是 live 的,DOM 变化会反映)。
  • element.firstElementChild / element.lastElementChild --- 第一个/最后一个子元素(或 null)。
  • element.nextElementSibling / element.previousElementSibling --- 同级的下/上一个元素节点(或 null)。

常见遍历模式(深度优先,元素节点):

scss 复制代码
// 递归 DFS 遍历所有元素节点
function walk(el, cb) {
  cb(el);
  for (let child of el.children) walk(child, cb);
}
walk(document.body, el => console.log(el.tagName));

// 使用 nextElementSibling 遍历同级
let node = container.firstElementChild;
while (node) {
  // 处理 node
  node = node.nextElementSibling;
}

补充工具(更强):TreeWalker / NodeIterator(用于复杂遍历过滤)------当你要在大量节点上做带过滤的遍历,TreeWalker 更高效并且语义更清晰。

注意 childNodeschildren 的区别:childNodes 包含文本和注释节点;children 只包含元素节点(更常用)。


3. HTML5(与 DOM 扩展相关的要点)

HTML5 对 DOM 的扩展很多,这里重点和实用 API 相关的:

  • 新语义元素(<section>, <article>, <nav> 等)------对语义化、可访问性有利。
  • dataset(data-*)与 classListquerySelector 全面支持,使 DOM 操作更灵活。
  • contentEditablehistory API(pushState/replaceState)及 canvasaudiovideo 等增强了页面交互能力。
  • document.headdocument.body 等属性更规范、便于访问。

(HTML5 范围广,上面是与 DOM 直接相关的重要点。)


4. CSS 类扩展

4.1 getElementsByClassName

  • 返回的是 live HTMLCollection(会随 DOM 变动自动更新)。
  • 速度通常比 querySelectorAll 快(具体受实现影响),适合需要实时反映的场景,但在修改 DOM 的循环中请小心 live 集合带来的副作用(建议先做 Array.from() 快照)。

4.2 classList 属性(DOMTokenList)

  • 现代推荐操作类名的 API,语义清晰:

    • el.classList.add('a','b')
    • el.classList.remove('c')
    • el.classList.toggle('d')el.classList.toggle('e', condition)
    • el.classList.contains('f')
    • el.classList.replace('old','new')
  • classList 性能优于直接字符串拼接(避免错误空格、重复),并支持 forEach

示例:

csharp 复制代码
el.classList.add('is-visible');
if (!el.classList.contains('active')) el.classList.add('active');
el.classList.toggle('expanded', shouldExpand);

5. 焦点管理(focus)

关键点与最佳实践:

  • API:element.focus([options])element.blur()document.activeElement

    • element.focus({preventScroll: true})(可选)可聚焦同时不触发滚动(现代浏览器支持)。
  • tabindex

    • tabindex="-1":可以程序聚焦,但不可通过 Tab 导航。
    • tabindex="0":参与顺序,位置由文档流决定(常用于自定义可聚焦组件)。
    • 正整数 tabindex(>0)会创建自定义顺序,不推荐使用(维护困难)。
  • 可访问性(A11y):确保可视焦点样式(不要随意移除 outline),模态对话框需实现焦点陷阱(trap focus)和返回焦点。

  • 焦点陷阱示例(简单):

ini 复制代码
const focusable = modal.querySelectorAll('a, button, input, [tabindex]:not([tabindex="-1"])');
let first = focusable[0], last = focusable[focusable.length-1];
modal.addEventListener('keydown', e => {
  if (e.key === 'Tab') {
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault(); last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault(); first.focus();
    }
  }
});

6. HTMLDocument 扩展

  • document.readyState:常见值 loading / interactive / complete。在 interactive 时 DOM 已可操作但外部资源(图片)可能未完全加载;DOMContentLoaded 触发时通常为 interactive
  • document.compatMode"CSS1Compat"(标准模式)或 "BackCompat"(怪异模式)。用于判断文档是否以标准渲染。
  • document.head:直接访问 <head> 节点的快捷属性(等价于 document.getElementsByTagName('head')[0])。

实践:在绑定 DOMContentLoaded 时,可先检查 document.readyState !== 'loading' 以避免重复等待。


7. 字符集属性(characterSet 详解)

  • document.characterSet(有时也见 document.charset)返回文档使用的字符编码字符串,例如 "UTF-8"
  • 这是只读的,决定于 HTTP 头或文档内 <meta charset="...">。解析阶段确定,运行时更改不会重新解析页面。
  • 用途:调试/检测文本编码问题或服务器端生成页面时确认编码;一般无需频繁使用。

8. 自定义数据属性(data-*

  • HTML:<div data-user-id="123" data-x-y-z="value"></div>
  • JS:通过 element.dataset 访问(DOMStringMap),属性名由 kebab-case 转为 camelCasedata-user-iddataset.userIddata-x-y-zdataset.xYZ(注意大小写转换)。
  • 值为字符串;如果要存对象/数组,需序列化(JSON.stringify/JSON.parse)。
  • 注意:不要把敏感数据(令牌、密码)放到 DOM 可见属性上------它对前端所有脚本和恶意扩展可见。

示例:

ini 复制代码
el.dataset.userId = '123';
console.log(el.dataset.xYz); // 读取 data-x-y-z

9. 插入标记(innerHTML / outerHTML / insertAdjacent* / 性能与安全)

9.1 innerHTML

  • 读取返回元素内部的 HTML 字符串,赋值会 替换子节点 并重新解析 HTML。
  • 优点:简单、直观。缺点:可能导致大量重排、事件监听丢失(因为旧节点被销毁)、XSS 风险。
  • 脚本插入:通过 innerHTML 插入 <script> 通常不会 直接执行(不同环境历史行为有差异),若需执行脚本应使用 createElement('script') 并 append。

9.2 旧 IE 中的 innerHTML

  • 旧 IE(历史)在对 tableselect 等元素的 innerHTML 支持上有奇怪差异(例如自动插入 tbody),现代浏览器已较统一。对兼容性要求高的项目需注意旧浏览器行为。

9.3 outerHTML

  • 代表元素自身及其内部的 HTML 字符串。赋值会替换当前元素(包括自身)。

9.4 insertAdjacentHTMLinsertAdjacentText

  • element.insertAdjacentHTML(position, htmlString):在指定位置插入解析后的节点。position 为:

    • 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
  • insertAdjacentText 是在指定位置插入纯文本(不会解析为 HTML),更安全。

  • insertAdjacentHTML 相比 innerHTML 的优点:不会替换父节点的全部内容,通常性能更好且保留现有节点和监听器(插入位置明确),常用作局部更新。

示例:

arduino 复制代码
container.insertAdjacentHTML('beforeend', '<li class="item">新项</li>');
container.insertAdjacentText('beforeend', '纯文本,不会解析成 HTML');

9.5 内存与性能问题

  • 每次修改 DOM 都可能触发重排(reflow)和重绘(repaint)。最佳实践:

    • 批量 DOM 更新时先构建 DocumentFragment 或字符串一次性插入。
    • 对频繁更新的 UI 使用虚拟 DOM、模板或最小化 DOM 变更。
    • 使用 requestAnimationFrame 来调度视觉变化,避免在短时间内多次强制布局。
    • 当需要创建大量节点时,Range.createContextualFragment()document.createDocumentFragment() 比多次 appendChild 更高效。

示例(DocumentFragment):

ini 复制代码
const frag = document.createDocumentFragment();
for (let i=0;i<1000;i++) {
  const li = document.createElement('li');
  li.textContent = `项 ${i}`;
  frag.appendChild(li);
}
ul.appendChild(frag); // 一次性插入,减少重排

9.6 跨站脚本(XSS)

  • 直接将用户输入插入 innerHTML/insertAdjacentHTML 风险极高。避免将未消毒的字符串作为 HTML 插入。
  • 方案:对输入进行白名单过滤或在服务器端清洗;在前端使用文本节点(textContent / createTextNode)插入纯文本;或使用成熟的库(如 DOMPurify)对 HTML 做消毒再插入。
  • 不要用正则简单替换 <script> 等做 XSS 过滤------很容易绕过。

10. scrollIntoView 方法

  • 简单调用:el.scrollIntoView()(旧签名)会滚动最近的可滚动祖先和视口,使元素可见。
  • 现代签名提供选项对象:el.scrollIntoView({ behavior: 'auto'|'smooth', block: 'start'|'center'|'end'|'nearest', inline: 'nearest'|'start'|'center'|'end' })
  • 常见用法:
php 复制代码
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
  • 若你只想在元素不可见时滚动,可先检查是否可见(用 getBoundingClientRect()IntersectionObserver),再调用 scrollIntoView。否则会造成不必要的跳转/滚动抖动。

11. 专有扩展(历史与兼容性)

  • 历史上不同浏览器曾引入专有 API 或前缀方法,例如:

    • element.attachEvent(旧 IE) vs addEventListener
    • webkitMatchesSelector / msMatchesSelector → 现在都可用 matches
    • outerText(IE)为非标准属性(一些浏览器实现)
    • scrollIntoViewIfNeeded(WebKit/Blink 的非标准扩展)
  • 建议:尽量使用标准 API 并在必要时提供逐级兼容处理(feature-detect 或 polyfill)。


12. children 属性(补充说明)

(已在遍历部分提到)

要点回顾:

  • children 只返回元素节点(Element),是 HTMLCollection(live)。
  • 如果要静态快照用于修改,请转换为数组 Array.from(el.children) 避免 live 集合带来的迭代问题。

13. contains / 常量(可能为你说的 "contants方法" 的两重含义)

Node.contains

  • parent.contains(node) 返回 true 当且仅当 nodeparent 的后代 或等于 parent 自身(注意:包含自身时也返回 true)。
javascript 复制代码
console.log(document.body.contains(someNode));
console.log(elem.contains(elem)); // true

节点类型常量(Node.*)

  • Node.ELEMENT_NODE(1)、Node.TEXT_NODE(3)、Node.COMMENT_NODE(8)等常量用于判断 node.nodeType。比硬编码数字更可读:
ini 复制代码
if (node.nodeType === Node.ELEMENT_NODE) { /* 是元素 */ }

文档顺序比较

  • nodeA.compareDocumentPosition(nodeB) 返回位掩码,可以检测包含关系、前后位置等(高级用法)。常用于实现稳定排序或比较节点文档顺序。

14. 插入标记(innerText / outerText

  • textContent vs innerText

    • textContent:返回/设置元素及子孙的文本内容(不考虑 CSS),性能快,不触发布局计算。
    • innerText:返回"渲染"出来的文本,受 CSS(display:none::before/::after)影响;读取 innerText 会触发重排以计算可见文本,较慢。innerText 源于旧 IE,现已被广泛实现。
  • outerText:非标准属性(在某些浏览器可用)。赋值通常会替换元素自身为文本。

推荐:若只需要取/设置纯文本,用 textContent;若需要按可视文本(考虑 CSS)用 innerText 并注意性能开销。


15. 滚动(scrollIntoViewIfNeeded 与更深的思考)

  • scrollIntoViewIfNeeded:这是一个 非标准 的 API(曾在 WebKit/Blink 中存在),用于"仅当元素不可见时"滚动到视口中心或靠近边缘。不可依赖于所有平台/浏览器。

  • 建议替代方案:

    1. 使用 IntersectionObserver 检测元素是否可见;在不可见时调用 scrollIntoView({ behavior:'smooth', block:'center' })IntersectionObserver 更高效、不会强制布局。
    2. 直接用 getBoundingClientRect() 与视口尺寸做检测(简单但要谨慎,可能触发布局读取)。
    3. 对于复杂场景(内嵌滚动容器),计算容器的滚动位置并设置 scrollTop/scrollLeft,或使用 Element.scrollTo({ top, behavior })

示例(IntersectionObserver + 自动滚动):

ini 复制代码
const obs = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (!e.isIntersecting) {
      e.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
  });
}, { threshold: 0.9 }); // 0.9 表示至少 90% 可见

obs.observe(targetElement);

深层思考要点:

  • 自动滚动行为会影响用户体验(打断上下文、改变阅读位置),因此:

    • 优先使用"只在必要时滚动"的策略(检测可见性)。
    • 在可访问性场景(焦点移动)保证视觉跟随焦点,但考虑 preventScroll 选项或平滑滚动以降低突兀感。
  • 对于虚拟列表/长列表(如 infinite scroll),应结合 "可见性检测 + 按需渲染 + 回收节点" 的策略,而非简单地滚动到某处插入大量 DOM。


额外实战建议(速览)

  • 避免频繁 innerHTML 造成的重排与事件丢失;使用 DocumentFragmentinsertAdjacentHTML、或构造元素然后一次性 append。
  • 为大量动态元素使用事件委托(在容器上绑定一个 listener),减少内存和绑定开销。
  • 操作 NodeList / HTMLCollection 时注意 live vs static:若中间修改 DOM,先 snapshot Array.from(...)
  • XSS:绝不要把用户输入直接通过 innerHTML 插入。使用 textContent 或可信的 sanitizer。
  • 测试各种浏览器的边界行为(尤其在面对旧浏览器或嵌入式浏览器时)。
相关推荐
二十雨辰31 分钟前
歌词滚动效果
前端·css
法医32 分钟前
和文心快码做朋友,让编程像“说话”一样简单
前端·文心快码
前端小巷子36 分钟前
JS 打造「放大镜 + 缩略图」一体组件
前端·javascript·面试
陈随易37 分钟前
适合中国宝宝的AI编程神器,文心快码
前端·后端·node.js
知识分享小能手40 分钟前
React学习教程,从入门到精通,React AJAX 语法知识点与案例详解(18)
前端·javascript·vue.js·学习·react.js·ajax·vue3
UrbanJazzerati1 小时前
掌握 DOM 的基础属性与方法:从操作元素到构建动态效果
前端·面试
hashiqimiya2 小时前
html实现右上角有个图标,鼠标移动到该位置出现手型,点击会弹出登录窗口。
前端·html
古夕2 小时前
前端文件下载的三种方式:a标签、Blob、ArrayBuffer
前端·javascript·vue.js
纯真时光2 小时前
Vue3中pinia状态管理库的使用(Composition API 风格)
前端
万少2 小时前
可可图片编辑 HarmonyOS(5)滤镜效果
前端