DOM 操作与事件
DOM 树操作、事件模型与事件委托。
前置知识
- 模块打包原理与 Tree Shaking:建议先完成前一篇的学习
学习目标
- 掌握「1. DOM 基础 (DOM Basics)」的核心机制、典型用法与常见陷阱
- 掌握「2. 查询与遍历 (Query & Traverse)」的核心机制、典型用法与常见陷阱
- 掌握「3. 创建与插入节点 (Create & Insert)」的核心机制、典型用法与常见陷阱
- 掌握「在大量简单元素场景下,innerHTML 可能比逐个 createElement 更快,但需注意 XSS 风险。」的核心机制、典型用法与常见陷阱
- 掌握「4. 属性操作 (Attribute Operations)」的核心机制、典型用法与常见陷阱
1. DOM 基础 (DOM Basics)
1.1 DOM 树结构
DOM(Document Object Model)把 HTML 文档表示为一棵节点树。浏览器解析 HTML 后,会构建如下结构:
flowchart TD
T0["document"]
T1["html"]
T2["head"]
T3["meta"]
T4["title"]
T5["link"]
T6["body"]
T7["header"]
T8["nav"]
T9["main"]
T10["section"]
T11["article"]
T12["footer"]
T0 --> T1
T1 --> T2
T0 --> T3
T0 --> T4
T0 --> T5
T5 --> T6
T5 --> T7
T0 --> T8
T8 --> T9
T0 --> T10
T0 --> T11
T11 --> T12
每个 HTML 标签对应一个元素节点,标签内的文本对应文本节点。
1.2 节点类型
DOM 中有 12 种节点类型,最常用的有:
| 节点类型 | nodeType 值 | 说明 | 示例 |
|---|---|---|---|
Element | 1 | 元素节点 | <div>, <p> |
Text | 3 | 文本节点 | 标签间的文字 |
Comment | 8 | 注释节点 | <!-- comment --> |
Document | 9 | 文档节点 | document |
DocumentType | 10 | 文档类型 | <!DOCTYPE html> |
DocumentFragment | 11 | 文档片段 | 轻量级容器 |
const el = document.querySelector('div');
el.nodeType;
el.nodeName;
const text = el.firstChild;
text.nodeType;
text.nodeName;
1.3 节点关系
flowchart TD
T0["parentElement"]
T1["firstChild / firstElementChild"]
T2["child1"]
T3["child2 (previousSibling ← → nextSibling)"]
T4["child3"]
T5["lastChild / lastElementChild"]
T0 --> T1
T0 --> T2
T0 --> T3
T0 --> T4
T0 --> T5
节点导航属性:
const el = document.querySelector('li');
el.parentNode;
el.parentElement;
el.childNodes;
el.children;
el.firstChild;
el.firstElementChild;
el.lastChild;
el.lastElementChild;
el.previousSibling;
el.previousElementSibling;
el.nextSibling;
el.nextElementSibling;
Node vs Element 属性区别:
childNodes/firstChild/nextSibling:包含所有节点类型(文本、注释等)children/firstElementChild/nextElementSibling:只包含元素节点
<ul id="list">
<li>A</li>
<li>B</li>
</ul>
const list = document.getElementById('list');
list.childNodes.length;
list.children.length;
2. 查询与遍历 (Query & Traverse)
2.1 常用查询 API
const app = document.getElementById('app');
const firstBtn = document.querySelector('button');
const items = document.querySelectorAll('.item');
| API | 返回类型 | 动态/静态 | 兼容性 |
|---|---|---|---|
getElementById | Element / null | — | IE6+ |
getElementsByClassName | HTMLCollection | 动态 | IE9+ |
getElementsByTagName | HTMLCollection | 动态 | IE6+ |
querySelector | Element / null | — | IE8+ |
querySelectorAll | NodeList(静态) | 静态 | IE8+ |
2.2 节点集合差异
NodeList可能是静态也可能是动态(取决于来源)HTMLCollection通常是动态集合(会随 DOM 变化)
const liveList = document.getElementsByClassName('item');
const staticList = document.querySelectorAll('.item');
document.body.append(document.createElement('div'));
liveList.length;
staticList.length;
工程实践里,若要数组方法:
const arr = Array.from(document.querySelectorAll('.item'));
const arr2 = [...document.querySelectorAll('.item')];
2.3 closest() 与 matches()
const btn = document.querySelector('button');
btn.closest('.card');
btn.closest('.card').querySelector('.title');
btn.matches('.primary');
btn.matches('button');
closest(selector):从自身开始向上查找,返回最近的匹配祖先(或自身)matches(selector):检查元素是否匹配选择器,返回boolean
2.4 遍历 DOM 树
function walkDOM(node, callback) {
callback(node);
node = node.firstChild;
while (node) {
walkDOM(node, callback);
node = node.nextSibling;
}
}
walkDOM(document.body, (node) => {
if (node.nodeType === 1) {
console.log(node.tagName);
}
});
3. 创建与插入节点 (Create & Insert)
3.1 创建元素与文本
const li = document.createElement('li');
li.className = 'item';
li.textContent = 'hello';
const text = document.createTextNode('world');
优先使用 textContent 来设置文本,避免把不可信内容当作 HTML 解析。
3.2 插入与移动
parent.append(child):追加(可追加多个参数)parent.prepend(child):头部插入node.before(x)/node.after(x):在节点前后插入parent.replaceChild(newNode, oldNode):替换node.replaceWith(newNode):替换自身 节点插入时会发生”移动”,不会复制:
const a = document.querySelector('#a');
const b = document.querySelector('#b');
const x = document.querySelector('#x');
b.append(x);
3.3 复制节点
const original = document.querySelector('.card');
const copy = original.cloneNode(true);
document.body.append(copy);
cloneNode(false)(默认):浅克隆,只复制元素本身cloneNode(true):深克隆,复制元素及其所有子节点 [警告] 注意:克隆不会复制事件监听器和data-*属性中通过 JS 设置的值。
3.4 删除节点
const el = document.querySelector('.item');
el.remove();
const parent = document.querySelector('.list');
const child = parent.querySelector('.item');
parent.removeChild(child);
el.remove():现代 API,直接删除自身parent.removeChild(child):经典 API,返回被删除的节点
3.5 批量更新:DocumentFragment
批量创建并一次性插入可减少重排重绘:
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = String(i);
frag.append(div);
}
document.body.append(frag);
Fragment 插入时,其子节点会被插入,Fragment 本身不会成为 DOM 的一部分。 现代替代方案:直接构建 HTML 字符串
const html = Array.from({ length: 1000 }, (_, i) => `<div>${i}</div>`).join('');
container.innerHTML = html;
在大量简单元素场景下,innerHTML 可能比逐个 createElement 更快,但需注意 XSS 风险。
4. 属性操作 (Attribute Operations)
4.1 HTML 属性 vs DOM 属性
<input id="name" type="text" value="hello" class="input-field" data-role="username" />
const input = document.getElementById('name');
input.getAttribute('type');
input.getAttribute('value');
input.getAttribute('class');
input.type;
input.value;
input.className;
关键区别:
| 对比项 | HTML 属性 (getAttribute) | DOM 属性 (obj.prop) |
|---|---|---|
| 来源 | HTML 标签上的属性 | DOM 对象的属性 |
| 值类型 | 始终是字符串 | 可以是任意类型 |
| 同步性 | 初始值,不随用户输入变化 | 实时值(如 input.value) |
| 自定义属性 | getAttribute('data-x') | dataset.x |
input.value = 'changed';
input.getAttribute('value');
input.value;
input.setAttribute('value', 'new default');
input.getAttribute('value');
4.2 setAttribute / getAttribute / removeAttribute
const img = document.querySelector('img');
img.setAttribute('src', 'photo.jpg');
img.setAttribute('alt', 'A photo');
img.getAttribute('src');
img.removeAttribute('alt');
img.hasAttribute('alt');
4.3 dataset(自定义数据属性)
<div id="user" data-user-id="42" data-role="admin" data-last-login="2026-01-01">User Info</div>
const el = document.getElementById('user');
el.dataset.userId;
el.dataset.role;
el.dataset.lastLogin;
el.dataset.status = 'active';
el.dataset.newField = 'value';
命名规则:
- HTML 中
data-user-id→ JS 中dataset.userId(短横线转驼峰) dataset的值始终是字符串- 设置新属性时驼峰会自动转为短横线
4.4 布尔属性
<input type="checkbox" checked disabled /> <button disabled>Click</button>
const checkbox = document.querySelector('input[type="checkbox"]');
checkbox.hasAttribute('checked');
checkbox.checked;
checkbox.getAttribute('checked');
checkbox.checked = checkbox.setAttribute('checked', '');
布尔属性(checked、disabled、selected、readonly)推荐使用 DOM 属性而非 setAttribute。
5. 样式操作 (Style Operations)
5.1 style 属性(行内样式)
const el = document.querySelector('.box');
el.style.width = '200px';
el.style.backgroundColor = 'red';
el.style.fontSize = '16px';
el.style.display = 'none';
el.style.display = '';
注意:
style只能读写行内样式,无法获取 CSS 类或<style>中的样式- CSS 属性名需转为驼峰:
background-color→backgroundColor - 设置空字符串
''可移除行内样式
5.2 classList(类名操作)
const el = document.querySelector('.box');
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('dark-mode');
el.classList.toggle('visible', window.innerWidth > 768);
el.classList.contains('active');
el.classList.replace('old-class', 'new-class');
el.className = 'box active dark-mode';
| 方法 | 说明 |
|---|---|
add(...tokens) | 添加一个或多个类名 |
remove(...tokens) | 移除一个或多个类名 |
toggle(token, force?) | 切换类名,force 为 “ 添加,false 移除 |
contains(token) | 是否包含指定类名 |
replace(old, new) | 替换类名 |
className vs classList:
el.className = 'box active';
el.className += ' dark-mode';
el.classList.add('active', 'dark-mode');
推荐使用 classList,语义更清晰且不会意外覆盖已有类名。
5.3 getComputedStyle(计算样式)
获取元素最终应用的样式(包括 CSS 继承、层叠、默认值):
const el = document.querySelector('.box');
const styles = window.getComputedStyle(el);
styles.width;
styles.height;
styles.backgroundColor;
styles.fontSize;
styles.marginTop;
styles.getPropertyValue('margin-top');
注意:
- 返回的是只读的
CSSStyleDeclaration对象 - 返回的值是计算值(如
font-size: 2em可能返回32px) - 简写属性(如
margin)可能返回空字符串,需查具体子属性(如marginTop)
5.4 获取元素尺寸与位置
const el = document.querySelector('.box');
el.offsetWidth;
el.offsetHeight;
el.clientWidth;
el.clientHeight;
el.scrollWidth;
el.scrollHeight;
el.offsetTop;
el.offsetLeft;
el.offsetParent;
el.scrollTop;
el.scrollLeft;
尺寸属性对比:
| 属性 | 包含 padding | 包含 border | 包含 scrollbar | 包含溢出内容 |
|---|---|---|---|---|
clientWidth/Height | [完成] | [错误] | [错误] | [错误] |
offsetWidth/Height | [完成] | [完成] | [完成] | [错误] |
scrollWidth/Height | [完成] | [错误] | [错误] | [完成] |
| 获取精确位置: |
const rect = el.getBoundingClientRect();
rect.top;
rect.right;
rect.bottom;
rect.left;
rect.width;
rect.height;
rect.x;
rect.y;
getBoundingClientRect() 返回相对于视口的位置,随滚动变化。
6. 事件系统 (Events)
6.1 监听与移除
function onClick(e) {
console.log('clicked', e.target);
}
const btn = document.querySelector('#btn');
btn.addEventListener('click', onClick);
btn.removeEventListener('click', onClick);
移除监听必须使用同一个函数引用,因此匿名函数不便于移除。
addEventListener 第三个参数:
btn.addEventListener('click', handler, {
capture: false,
once: true,
passive: True,
});
| 选项 | 说明 |
|---|---|
capture | 在捕获阶段触发(默认 false,冒泡阶段) |
once | 触发一次后自动移除(默认 false) |
passive | 声明不会调用 preventDefault,优化滚动性能(默认 false) |
6.2 捕获与冒泡
DOM 事件传播的三个阶段:
1. 捕获阶段(Capture):window → document → ... → 目标父元素
2. 目标阶段(Target):目标元素本身
3. 冒泡阶段(Bubble):目标父元素 → ... → document → window
<div id="outer">
<div id="inner">
<button id="btn">Click</button>
</div>
</div>
document.getElementById('outer').addEventListener(
'click',
(e) => {
console.log('outer capture', e.eventPhase);
},
true
);
document.getElementById('outer').addEventListener('click', (e) => {
console.log('outer bubble', e.eventPhase);
});
document.getElementById('inner').addEventListener(
'click',
(e) => {
console.log('inner capture', e.eventPhase);
},
true
);
document.getElementById('inner').addEventListener('click', (e) => {
console.log('inner bubble', e.eventPhase);
});
document.getElementById('btn').addEventListener('click', (e) => {
console.log('btn target', e.eventPhase);
});
点击按钮后输出顺序:
outer capture 1
inner capture 1
btn target 2
inner bubble 3
outer bubble 3
eventPhase 值:1 = 捕获,2 = 目标,3 = 冒泡
6.3 事件对象常用属性与方法
btn.addEventListener('click', (e) => {
e.target;
e.currentTarget;
e.type;
e.bubbles;
e.cancelable;
e.timeStamp;
e.isTrusted;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
| 属性/方法 | 说明 |
|---|---|
target | 触发事件的元素(最内层) |
currentTarget | 绑定事件监听的元素(等于 this) |
preventDefault() | 阻止默认行为(如表单提交、链接跳转) |
stopPropagation() | 阻止事件继续传播(捕获/冒泡) |
stopImmediatePropagation() | 阻止传播 + 阻止同元素上的后续监听器 |
6.4 事件委托 (Event Delegation)
当列表项动态增删时,把监听挂在父元素上更稳:
const list = document.querySelector('#list');
list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (!item) return;
console.log('item clicked', item.dataset.id);
});
事件委托的优势:
- 减少内存:不需要为每个子元素绑定监听器
- 动态元素:新增子元素自动拥有事件处理
- 统一管理:代码更集中、更易维护 适用场景:
document.addEventListener('click', (e) => {
if (e.target.matches('.modal-overlay')) {
closeModal();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
不适合委托的场景:
focus/blur事件(不冒泡,需用focusin/focusout替代)mousemove/touchmove等高频事件(委托反而增加判断开销)- 需要精确
currentTarget的场景
6.5 自定义事件
const event = new CustomEvent('userLogin', {
bubbles: true,
detail: { userId: 42, username: 'alice' }
}
document.dispatchEvent(event)
document.addEventListener('userLogin', (e) => {
console.log('User logged in:', e.detail.username)
}
应用场景:
class TodoList extends HTMLElement {
addTodo(text) {
const todo = { id: Date.now(), text };
this.dispatchEvent(
new CustomEvent('todo-added', {
bubbles: true,
detail: todo,
})
);
}
}
document.querySelector('todo-list').addEventListener('todo-added', (e) => {
console.log('New todo:', e.detail.text);
});
6.6 常用事件类型汇总
| 分类 | 事件 | 说明 |
|---|---|---|
| 鼠标 | click, dblclick, mousedown, mouseup, mousemove | 鼠标交互 |
| 鼠标 | mouseenter, mouseleave, mouseover, mouseout | 鼠标悬停 |
| 键盘 | keydown, keyup, keypress(已废弃) | 键盘输入 |
| 表单 | input, change, submit, focus, blur | 表单交互 |
| 滚动 | scroll, wheel | 滚动行为 |
| 触摸 | touchstart, touchmove, touchend | 移动端 |
| 拖拽 | dragstart, drag, dragend, drop | 拖放操作 |
| 资源 | load, error, DOMContentLoaded | 资源加载 |
| 视口 | resize, scroll, visibilitychange | 视口变化 |
7. 性能优化 (Performance)
7.1 避免布局抖动 (Layout Thrashing)
读布局信息(如 offsetHeight)会触发布局计算;写样式会使布局失效。交替读写会导致反复布局。
const items = document.querySelectorAll('.item');
items.forEach((item) => {
const height = item.offsetHeight;
item.style.height = height * 2 + 'px';
});
优化:批量读取,再批量写入:
const items = document.querySelectorAll('.item');
const heights = Array.from(items, (item) => item.offsetHeight);
items.forEach((item, i) => {
item.style.height = heights[i] * 2 + 'px';
});
或使用 requestAnimationFrame:
function updateLayout() {
const height = el.offsetHeight;
requestAnimationFrame(() => {
el.style.height = height * 2 + 'px';
});
}
7.2 innerHTML 的取舍
- 优点:构建复杂结构时省代码
- 风险:容易引入 XSS;会重建子树导致事件丢失
当内容来自不可信输入时,不要直接拼接
innerHTML。
7.3 DocumentFragment 与批量操作
const frag = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
frag.append(li);
}
list.append(frag);
Fragment 的优势:
- 不触发重排(不在 DOM 中)
- 插入时 Fragment 自身不进入 DOM 树
- 一次重排代替 N 次重排
7.4 事件节流与防抖
高频事件(scroll、resize、input、mousemove)需要节流或防抖:
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
window.addEventListener(
'resize',
debounce(() => {
console.log('Resized:', window.innerWidth);
}, 200)
);
window.addEventListener(
'scroll',
throttle(() => {
console.log('Scrolled:', window.scrollY);
}, 100)
);
防抖 vs 节流:
| 对比项 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 触发时机 | 停止操作后延迟触发 | 按固定间隔触发 |
| 类比 | 电梯等人 | 红绿灯 |
| 适用场景 | 搜索输入、窗口调整 | 滚动事件、拖拽 |
7.5 虚拟 DOM 概念
直接操作 DOM 的代价高(重排重绘),现代框架引入虚拟 DOM 来优化:
const vnode = {
tag: 'div',
props: { className: 'container' },
children: [
{ tag: 'h1', children: 'Hello' },
{ tag: 'p', children: 'World' },
],
};
虚拟 DOM 的工作流程:
- 用 JS 对象描述 UI 结构(虚拟 DOM 树)
- 状态变化时,创建新的虚拟 DOM 树
- Diff 算法比较新旧虚拟 DOM 树差异
- 只将差异部分更新到真实 DOM(最小化 DOM 操作) 虚拟 DOM 的优势:
- 批量更新:多次状态变更合并为一次 DOM 更新
- 最小化操作:只更新变化的部分
- 跨平台:虚拟 DOM 可以渲染到不同目标(DOM、Canvas、Native) 何时直接操作 DOM:
- 简单交互(不需要框架时)
- 性能极端敏感的场景(如动画、Canvas)
- 与框架配合的底层操作(如 D3.js 与 React 结合)
7.6 passive 事件监听器
document.addEventListener('touchstart', handler, { passive: })
document.addEventListener('wheel', handler, { passive: })
passive: 告诉浏览器该监听器不会调用 preventDefault(),浏览器可以立即开始滚动而不必等待 JS 执行完毕。
Chrome 对 touchstart/touchmove 默认使用 passive 监听器。
8. 安全要点 (Security)
8.1 XSS 防护
- 不可信文本:用
textContent
const userInput = '<img src=x onerror=alert(1)>';
el.textContent = userInput;
el.innerHTML = userInput;
- 不可信 URL:校验协议(避免
javascript:)、限制域名
function isSafeUrl(url) {
try {
const parsed = new URL(url, location.href);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
8.2 innerHTML 安全
const name = getUserInput();
el.innerHTML = `<p>Hello, ${name}</p>`;
安全替代方案:
const p = document.createElement('p');
p.textContent = `Hello, ${name}`;
el.append(p);
或使用 DOMPurify 库:
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(untrustedHtml);
8.3 事件监听器泄漏
class Modal {
constructor() {
this.onKeydown = this.onKeydown.bind(this);
}
open() {
document.addEventListener('keydown', this.onKeydown);
}
close() {
document.removeEventListener('keydown', this.onKeydown);
}
onKeydown(e) {
if (e.key === 'Escape') this.close();
}
}
常见泄漏场景:
- SPA 路由切换时未移除全局事件监听
- 组件销毁时未清理
setInterval/setTimeout - 闭包引用了已移除的 DOM 节点
8.4 模板渲染安全
优先使用成熟框架或做统一的转义/白名单策略:
function escapeHtml(str) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return str.replace(/[&<>"']/g, (c) => map[c]);
}
el.innerHTML = `<p>${escapeHtml(userInput)}</p>`;
节点获取
基本写法:getElementById
document.getElementById(<id>)
// 通过 ID 获取单个元素
let el = document.getElementById("app");
基本写法:querySelector
document.querySelector(<选择器>)
// 通过 CSS 选择器获取首个匹配元素
let el = document.querySelector(".item");
基本写法:querySelectorAll
document.querySelectorAll(<选择器>)
// 获取所有匹配元素返回 NodeList
let els = document.querySelectorAll(".item");
节点创建
基本写法:createElement
document.createElement(<标签名>)
// 创建元素节点
let div = document.createElement("div");
基本写法:createTextNode
document.createTextNode(<文本>)
// 创建文本节点
let text = document.createTextNode("hello");
基本写法:DocumentFragment 批量插入
document.createDocumentFragment()
// 使用片段批量插入减少重排
let frag = document.createDocumentFragment();
items.forEach(item => {
let li = document.createElement("li");
li.textContent = item;
frag.appendChild(li);
});
list.appendChild(frag);
节点插入与删除
基本写法:appendChild
<父节点>.appendChild(<节点>)
// 在末尾追加子节点
document.body.appendChild(div);
基本写法:insertBefore
<父节点>.insertBefore(<新节点>, <参考节点>)
// 在参考节点前插入
parent.insertBefore(newNode, refNode);
基本写法:removeChild
<父节点>.removeChild(<节点>)
// 移除子节点
parent.removeChild(child);
基本写法:replaceChild
<父节点>.replaceChild(<新节点>, <旧节点>)
// 替换子节点
parent.replaceChild(newNode, oldNode);
现代节点 API
基本写法:append
<父节点>.append(<节点或文本>)
// 追加多个节点或文本字符串
parent.append(node1, "text", node2);
基本写法:prepend
<父节点>.prepend(<节点或文本>)
// 在开头插入
parent.prepend(newNode);
基本写法:before 与 after
<节点>.before(<节点>)
// 在节点前或后插入兄弟节点
el.before(newNode);
el.after(anotherNode);
基本写法:remove
<节点>.remove()
// 节点自移除
el.remove();
基本写法:replaceWith
<节点>.replaceWith(<新节点>)
// 节点自替换
oldEl.replaceWith(newEl);
属性操作
基本写法:getAttribute setAttribute
<元素>.setAttribute(<名称>, <值>)
// 读写 HTML 属性
el.setAttribute("data-id", "1");
let id = el.getAttribute("data-id");
基本写法:dataset 自定义属性
<元素>.dataset.<名称>
// 读写 data-* 自定义属性
el.dataset.userId = "42";
let id = el.dataset.userId;
基本写法:hasAttribute removeAttribute
<元素>.removeAttribute(<名称>)
// 检查与移除属性
el.hasAttribute("disabled");
el.removeAttribute("disabled");
classList 操作
基本写法:add remove
<元素>.classList.add(<类名>)
// 添加移除类名
el.classList.add("active");
el.classList.remove("hidden");
基本写法:toggle
<元素>.classList.toggle(<类名>)
// 切换类名存在则移除否则添加
el.classList.toggle("open");
基本写法:contains
<元素>.classList.contains(<类名>)
// 判断是否包含类名
if (el.classList.contains("active")) {}
样式操作
基本写法:内联样式
<元素>.style.<属性> = <值>
// 读写内联样式需用驼峰命名
el.style.backgroundColor = "#fff";
基本写法:getComputedStyle
window.getComputedStyle(<元素>)
// 获取最终计算样式
let style = window.getComputedStyle(el);
let color = style.color;
基本写法:cssText 批量设置
<元素>.style.cssText = "<样式字符串>"
// 批量设置内联样式
el.style.cssText = "color:red;font-size:14px;";
事件绑定
基本写法:addEventListener
<元素>.addEventListener(<事件>, <回调>, [<选项>])
// 添加事件监听器
el.addEventListener("click", e => {});
基本写法:removeEventListener
<元素>.removeEventListener(<事件>, <回调>)
// 移除事件监听需同一回调引用
el.removeEventListener("click", handler);
基本写法:once 选项
<元素>.addEventListener(<事件>, <回调>, { once: true })
// once 表示只触发一次后自动移除
el.addEventListener("click", fn, { once: true });
基本写法:capture 捕获阶段
<元素>.addEventListener(<事件>, <回调>, { capture: true })
// 在捕获阶段触发
el.addEventListener("click", fn, { capture: true });
基本写法:passive 提升滚动性能
<元素>.addEventListener(<事件>, <回调>, { passive: true })
// passive 声明不调用 preventDefault 优化滚动
window.addEventListener("touchmove", fn, { passive: true });
事件对象
基本写法:preventDefault
<事件>.preventDefault()
// 阻止默认行为如表单提交链接跳转
a.addEventListener("click", e => e.preventDefault());
基本写法:stopPropagation
<事件>.stopPropagation()
// 阻止事件冒泡
el.addEventListener("click", e => e.stopPropagation());
基本写法:stopImmediatePropagation
<事件>.stopImmediatePropagation()
// 阻止冒泡并阻止同元素其他监听器
el.addEventListener("click", e => e.stopImmediatePropagation());
事件委托
基本写法:事件委托模式
<父节点>.addEventListener(<事件>, <回调>)
// 利用冒泡在父节点统一处理
list.addEventListener("click", e => {
let item = e.target.closest(".item");
if (item) handle(item);
});
基本写法:closest 匹配祖先
<元素>.closest(<选择器>)
// 从当前元素向上查找匹配选择器的最近祖先
let card = e.target.closest(".card");
自定义事件
基本写法:CustomEvent
new CustomEvent(<名称>, { detail: <数据> })
// 创建带数据的自定义事件
let evt = new CustomEvent("login", { detail: { user: "Tom" } });
el.dispatchEvent(evt);
基本写法:dispatchEvent
<元素>.dispatchEvent(<事件>)
// 同步派发事件触发监听器
el.dispatchEvent(new Event("ready"));
遍历与查找
基本写法:parentNode parentElement
<元素>.parentElement
// 获取父节点
let parent = el.parentElement;
基本写法:children childNodes
<元素>.children
// children 返回元素集合 childNodes 含文本节点
let kids = el.children;
基本写法:nextElementSibling
<元素>.nextElementSibling
// 获取下一个兄弟元素节点
let next = el.nextElementSibling;
MutationObserver
基本写法:观察 DOM 变化
new MutationObserver(<回调>)
// 监听子节点属性变化
let observer = new MutationObserver(muts => {});
observer.observe(el, { childList: true, subtree: true });
基本写法:disconnect 断开
<observer>.disconnect()
// 停止观察
observer.disconnect();
IntersectionObserver
基本写法:可见性观察
new IntersectionObserver(<回调>, [<选项>])
// 监听元素进入视口用于懒加载
let io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) loadImage(e.target);
});
});
io.observe(img);
基本写法:rootMargin
new IntersectionObserver(<回调>, { rootMargin: "<边距>" })
// 提前预加载设置根边距
let io = new IntersectionObserver(fn, { rootMargin: "100px" });
ResizeObserver
基本写法:尺寸变化观察
new ResizeObserver(<回调>)
// 监听元素尺寸变化
let ro = new ResizeObserver(entries => {
entries.forEach(e => console.log(e.contentRect.width));
});
ro.observe(el);
实用模式
基本写法:事件委托结合 dataset
<父节点>.addEventListener(<事件>, <回调>)
// 通过 dataset 传递上下文数据
list.addEventListener("click", e => {
let item = e.target.closest("[data-id]");
if (item) console.log(item.dataset.id);
});
基本写法:批量绑定事件
<元素列表>.forEach(<元素> => <元素>.addEventListener(<事件>, <回调>))
// 为多个元素绑定相同事件
document.querySelectorAll(".btn")
.forEach(btn => btn.addEventListener("click", onClick));