DOM(文档对象模型)
DOM 的结构、节点类型、遍历与操作方式,以及事件委托机制。
#tech / dev / frontend
#resource / browser
#type / concept
#status / growing
[!info] related notes
- 所属 MOC: 前端基础 MOC
- 相关概念: event-bubbling-and-capturing, event-loop, web-worker
DOM(文档对象模型)
定义
DOM(Document Object Model)是浏览器将 HTML 文档解析后生成的树形数据结构。JavaScript 通过 DOM API 来读取、修改页面内容、结构和样式。
DOM 树结构
HTML 文档被解析为一棵节点树:
<html>
<head>
<title>页面标题</title>
</head>
<body>
<h1>标题</h1>
<p class="intro">一段文字</p>
</body>
</html>
对应的 DOM 树:Document → html → {head → title → "页面标题", body → {h1 → "标题", p.intro → "一段文字"}}。
节点类型
| 类型 | nodeType | 示例 |
|---|---|---|
| Element | 1 | <div>, <p> |
| Text | 3 | 文字内容 |
| Comment | 8 | <!-- 注释 --> |
| Document | 9 | document |
| DocumentFragment | 11 | 文档片段 |
| Attr | 2 | class="card" |
获取元素
// 推荐:现代 API
document.querySelector('.card') // 第一个匹配
document.querySelectorAll('.card') // 所有匹配(NodeList)
// 传统 API(更快,但灵活性低)
document.getElementById('app')
document.getElementsByClassName('card') // HTMLCollection(动态)
document.getElementsByTagName('div')
document.getElementsByName('username')
querySelectorAll 返回静态 NodeList,不会随 DOM 变化;getElementsByClassName 返回动态 HTMLCollection。
遍历 DOM
const el = document.querySelector('.card')
// 父节点
el.parentNode // 父节点(可能是 Text 节点)
el.parentElement // 父元素节点
// 子节点
el.childNodes // 所有子节点(包括 Text、Comment)
el.children // 只有元素子节点
el.firstChild // 第一个子节点
el.firstElementChild // 第一个元素子节点
el.lastChild // 最后一个子节点
// 兄弟节点
el.nextSibling
el.nextElementSibling
el.previousSibling
el.previousElementSibling
修改 DOM
const div = document.createElement('div')
div.textContent = '新内容'
div.className = 'card'
// 插入
parent.appendChild(child)
parent.insertBefore(newNode, referenceNode)
parent.append(child1, child2) // 可追加多个,可追加字符串
parent.prepend(child)
parent.after(sibling)
parent.before(sibling)
// 删除
parent.removeChild(child)
child.remove() // 更简洁
// 替换
parent.replaceChild(newChild, oldChild)
// 修改属性
el.setAttribute('id', 'app')
el.getAttribute('id')
el.removeAttribute('id')
el.dataset.userId = '123' // data-user-id="123"
// 修改样式
el.style.color = 'red'
el.classList.add('active')
el.classList.remove('active')
el.classList.toggle('active')
el.classList.contains('active')
DocumentFragment
DocumentFragment 是轻量的文档片段,不会触发重排,适合批量插入。
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li')
li.textContent = `Item ${i}`
fragment.appendChild(li)
}
// 一次性插入,只触发一次重排
ul.appendChild(fragment)
事件委托
利用事件冒泡机制,将事件监听器绑定在父元素上,通过 event.target 判断实际触发的子元素。
// 不推荐:给每个按钮都绑定事件
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', handleClick)
})
// 推荐:事件委托
document.querySelector('.btn-container').addEventListener('click', (e) => {
if (e.target.matches('.btn')) {
handleClick(e)
}
})
优势:
- 减少事件监听器数量
- 动态添加的子元素自动被处理
- 内存占用更少
详见 event-bubbling-and-capturing。
innerHTML vs textContent
el.innerHTML = '<b>粗体</b>' // 解析 HTML,有 XSS 风险
el.textContent = '<b>粗体</b>' // 纯文本,安全
el.innerText = '内容' // 受 CSS 影响(display: none 的不返回)
textContent 性能更好且更安全,优先使用。
边界
- DOM 操作是昂贵的,频繁操作会触发 reflow/repaint
- 批量操作时使用 DocumentFragment 或
innerHTML一次性写入 querySelectorAll返回的 NodeList 不是数组,需要用Array.from()或展开运算符转数组- DOM 事件监听器在元素移除后不会自动销毁(可能内存泄漏),需要手动
removeEventListener或使用 AbortController document.readyState可以检查 DOM 是否加载完成
一句话记忆法
DOM 是浏览器把 HTML 解析成的树结构;用 querySelector 获取节点,用 createElement/appendChild 操作结构,用事件委托减少监听器。