前置知识: HTML5、CSS

DOM 操作与事件

9 min中级

DOM 树操作、事件模型与事件委托。

前置知识

学习目标

  • 掌握「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 值说明示例
Element1元素节点<div>, <p>
Text3文本节点标签间的文字
Comment8注释节点<!-- comment -->
Document9文档节点document
DocumentType10文档类型<!DOCTYPE html>
DocumentFragment11文档片段轻量级容器
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返回类型动态/静态兼容性
getElementByIdElement / null—IE6+
getElementsByClassNameHTMLCollection动态IE9+
getElementsByTagNameHTMLCollection动态IE6+
querySelectorElement / null—IE8+
querySelectorAllNodeList(静态)静态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);
});

事件委托的优势:

  1. 减少内存:不需要为每个子元素绑定监听器
  2. 动态元素:新增子元素自动拥有事件处理
  3. 统一管理:代码更集中、更易维护 适用场景:
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 的工作流程:

  1. 用 JS 对象描述 UI 结构(虚拟 DOM 树)
  2. 状态变化时,创建新的虚拟 DOM 树
  3. Diff 算法比较新旧虚拟 DOM 树差异
  4. 只将差异部分更新到真实 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
  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));