JavaScript DOM操作实例:元素增删改查与事件委托实现

作者:页面旅途时间:2026-10-30 18:38:47浏览:197

深入讲解JavaScript DOM操作的核心API与实际案例,涵盖元素的查询、创建、修改、删除全流程,并详细剖析事件冒泡机制与基于closest的高效事件委托实现。

在前端交互开发中,DOM(文档对象模型)是 JavaScript 操纵网页结构与展示内容的标准编程接口。无论是响应用户点击、动态渲染后端数据列表,还是开发富交互表单,核心底层都依赖于 DOM 元素的“增、删、改、查”以及事件监听。本文将通过结构化解析与具体代码示例,逐一讲解现代 DOM API 的标准用法,并深入拆解性能关键技术——事件委托(Event Delegation)的原理与工程落地方式。

一、DOM 树模型与节点精准查询(查)

浏览器在解析 HTML 文档时,会将其转换为由节点构成的树状层级结构,即 DOM 树。操作任何元素的第一步都是定位目标节点。现代浏览器提供了成熟且高效的选择器 API,能够满足从单一节点定位到复杂批量筛选的各类需求。

1. 常用查询 API 选型

  • document.getElementById(id):通过唯一 ID 查找单一元素,执行速度极快,适用于页面结构固定的全局关键容器。
  • document.querySelector(selectors):接收合法的 CSS 选择器字符串,返回文档中匹配该选择器的第一个元素;未找到时返回 null。灵活性高,语义直观。
  • document.querySelectorAll(selectors):返回匹配指定 CSS 选择器的所有元素的非活动(静态)NodeList 集合。
  • element.closest(selectors):从当前元素自身开始,沿 DOM 树向上逐层匹配祖先元素,返回最近匹配的节点,在事件委托场景中至关重要。
静态 NodeList 与动态 HTMLCollection 的区别:querySelectorAll 返回的是静态 NodeList,即使后续向 DOM 树插入新元素,该集合长度也不会自动变化;而旧式的 getElementsByTagName 或 getElementsByClassName 返回的是动态 HTMLCollection,其内容会随 DOM 的增删实时同步更新。日常开发推荐使用静态选择器,逻辑更为确定,避免因隐式状态变更导致的遍历死循环。
JavaScript DOM操作实例:元素增删改查与事件委托实现
图 1:DOM 树层级架构与核心查询路径映射

2. 查询示例代码

// 1. 获取全局唯一的列表容器
const todoList = document.querySelector('#todo-list');

// 2. 查询列表内当前所有的任务项(静态集合)
const allItems = todoList.querySelectorAll('.item');
console.log(`当前共有 ${allItems.length} 个任务`);

// 3. 遍历并对特定项进行筛选
allItems.forEach((item, index) => {
  if (item.classList.contains('completed')) {
    console.log(`第 ${index + 1} 项已完成`);
  }
});

二、DOM 元素的创建、挂载与批量优化(增)

向页面动态添加内容是单页应用和交互界面的基本需求。在 DOM 中新增元素不能直接拼接字符串插入,否则会破坏已绑定的事件并带来安全隐患。

1. 标准创建与挂载方法

  • document.createElement(tagName):在内存中创建指定的 HTML 元素节点。此时元素尚未加入 DOM 树,不会触发任何页面渲染。
  • parentElement.appendChild(childNode):将一个节点追加到父元素的子节点列表末尾。若子节点已存在于文档中,则会将其移动到新位置。
  • parentElement.insertBefore(newNode, referenceNode):在指定的参考子节点之前插入新节点。如果参考节点为 null,行为等价于 appendChild。
  • parentElement.append(...nodesOrDOMStrings):现代 API,支持同时插入多个节点或纯文本,比 appendChild 更简洁。

2. 性能考量:使用 DocumentFragment 进行批量挂载

每次向处于活动渲染树(Live DOM)中的父容器追加子节点,浏览器都可能执行重排(Reflow)与重绘(Repaint)。若需要循环插入 100 个列表项,直接循环调用 appendChild 会引发多次不必要的布局计算。此时应借助 DocumentFragment(文档片段)。

文档片段存在于内存中,不属于主 DOM 树。将多个节点逐一加入片段后,只需一次性将片段挂载到主容器中,即可将 100 次页面变动精简为 1 次。

JavaScript DOM操作实例:元素增删改查与事件委托实现
图 2:元素创建与 DOM 挂载全流程(步骤流水线)

3. 高效批量添加示例

function renderBatchTasks(tasks, container) {
  // 创建轻量级文档片段作为临时缓冲区
  const fragment = document.createDocumentFragment();

  tasks.forEach(task => {
    // 1. 创建外层 li
    const li = document.createElement('li');
    li.className = 'todo-item';
    li.dataset.id = task.id;

    // 2. 创建文本展示区
    const textSpan = document.createElement('span');
    textSpan.className = 'task-content';
    textSpan.textContent = task.title; // 避免 XSS

    // 3. 创建删除操作按钮
    const delBtn = document.createElement('button');
    delBtn.className = 'btn-delete';
    delBtn.textContent = '删除';

    // 组装单个项并追加至片段中
    li.append(textSpan, delBtn);
    fragment.appendChild(li);
  });

  // 一次性挂载上树,仅产生 1 次重排
  container.appendChild(fragment);
}

三、内容、属性与样式的更新(改)

修改已有 DOM 节点主要分为三个维度:文本与内容更新、HTML 属性与自定义数据更新,以及外观样式类更新。

1. 文本与 HTML 内容更新

更新内容最常用的两个属性是 textContent 与 innerHTML:

  • textContent:获取或设置节点及其后代的纯文本内容。设置时,浏览器会将其视为字面量,自动进行字符转义,完全免疫 XSS(跨站脚本攻击),执行性能高。
  • innerHTML:解析并设置 HTML 片段。虽然方便,但如果填入来自用户输入或不受信接口的字符串,极易引发安全漏洞,且会销毁原有子节点绑定的事件监听器与内部状态。
安全铁律:只要展示纯文本内容(如用户名、任务名称、评论内容),坚决使用 textContent,杜绝直接拼接 innerHTML。

2. 属性与 Dataset 操作

对于标准 HTML 属性,可以直接读取元素属性(如 input.value、img.src),也可以通过通用方法修改:

  • element.setAttribute(name, value) / element.getAttribute(name)
  • element.removeAttribute(name) / element.hasAttribute(name)
  • element.dataset:专门用于读取和设置以 data-* 命名的自定义属性。例如设置 el.dataset.taskId = "42",对应 DOM 属性为 data-task-id="42"。

3. 类名与样式的规范修改

不建议直接频繁修改 element.style.cssText 或内联样式,这会导致样式难以维护。现代开发规范推荐通过 classList 管理 CSS 预定义的类名:

// 推荐:通过 classList 操纵样式状态
const item = document.querySelector('.todo-item');

item.classList.add('is-active');      // 添加类
item.classList.remove('pending');     // 移除类
item.classList.toggle('completed');   // 切换类(存在则删,不存在则加)
const hasClass = item.classList.contains('completed'); // 判定是否存在

四、节点的安全移除与引用清理(删)

从页面移除不再需要的节点同样有多种方式,需要注意内存泄漏与事件解绑等底层细节。

1. 移除节点的方法对比

API 方法 调用主体 兼容性与特性
element.remove() 目标节点自身 现代标准 API,调用直观,直接将自身从父节点中脱离。
parent.removeChild(child) 父级节点 传统通用 API,需要先获取父节点;返回被删除节点的引用。
element.replaceWith(newEl) 旧节点自身 使用新节点直接替换旧节点,旧节点被移出文档。
防止内存泄漏:如果使用传统的单个节点事件绑定方式(item.addEventListener),在调用 item.remove() 之后,如果 JavaScript 全局变量或闭包中依然保留对 item 的强引用,该 DOM 节点及其监听器将无法被垃圾回收机制(GC)清理,导致内存常驻。这也正是推荐使用事件委托的深层原因之一。

五、事件冒泡机制与事件委托实现(核心进阶)

当页面包含大量动态子元素(例如 1000 行可交互表格、持续追加的任务列表)时,如果为每个子元素单独绑定 addEventListener,会带来两大严重缺陷:

  1. 内存消耗巨大:成百上千个事件监听器函数占用大量运行时内存,拖慢初次加载速度。
  2. 动态元素失效:后续通过 JavaScript 动态创建并插入的新元素,由于没有经历初始化绑定逻辑,无法响应点击事件,迫使开发者在新增时重复编写绑定代码。

要优雅地彻底解决这两个痛点,唯一的正解是事件委托(Event Delegation),而事件委托的理论支柱则是 DOM 事件流模型中的事件冒泡(Event Bubbling)。

1. DOM 事件传播的三大阶段

当用户在页面上点击某个元素时,事件并非直接在该元素上凭空触发,而是经历完整的三阶段流转过程:

  1. 捕获阶段(Capture Phase):事件从顶层对象 window 沿着 DOM 树一路向下传递,途经 document、html、body,直至到达目标元素的直接父级。
  2. 目标阶段(Target Phase):事件抵达触发交互的深层源头节点(即 event.target)。
  3. 冒泡阶段(Bubble Phase):事件从目标节点开始,沿 DOM 树反向向上层祖先节点冒泡回传,直到 window。在此路径上的所有祖先元素都可以监听到该事件。
JavaScript DOM操作实例:元素增删改查与事件委托实现
图 3:DOM 事件流传播与事件委托拦截机制图

2. 事件委托的核心逻辑:区分 target 与 currentTarget

在事件处理回调中,有两个极其关键的属性:

  • event.currentTarget:当前执行监听回调的节点,即调用 addEventListener 的父容器本身(如 <ul>)。
  • event.target:用户实际点击的最深层物理节点(可能是 <button>,也可能是按钮内的 <svg> 或 <span>)。

3. 现代委托的最佳写法:Element.closest()

旧式教程通常使用 if (e.target.tagName === 'BUTTON') 进行匹配判断。这种写法存在严重漏洞:如果按钮内部包含图标或文本标签(例如 <button><i class="icon"></i> 删除</button>),用户点中图标时 e.target 为 <i> 标签,导致 tagName === 'BUTTON' 判定失败,事件无法触发。

现代标准写法是利用 e.target.closest(selector)。它会从点中的节点开始向上回溯寻找匹配的元素,并校验找到的元素是否仍在当前父容器内部,既安全又健壮:

// 现代事件委托标准结构模板
parentContainer.addEventListener('click', (event) => {
  // 1. 寻找被点击的目标按钮(即使点击了按钮内部的子元素也能正确匹配)
  const deleteBtn = event.target.closest('.btn-delete');
  
  // 2. 如果点击区域不在目标按钮内,忽略本次事件
  if (!deleteBtn || !parentContainer.contains(deleteBtn)) {
    return;
  }

  // 3. 向上获取当前操作的数据行/列表项
  const item = deleteBtn.closest('.todo-item');
  const itemId = item.dataset.id;
  
  // 4. 执行业务逻辑
  item.remove();
  console.log(`成功删除任务,ID 为:${itemId}`);
});

六、综合实战:任务清单动态增删改查全流程实现

下面结合完整的 HTML 结构与 JavaScript 代码,展示一个涵盖元素“查、增、改、删”及“统一事件委托”的完整工程实例。该实例包含输入新增、完成状态切换、内容双击就绪修改及删除功能。

1. HTML 页面骨架

<div class="todo-app">
  <!-- 顶部输入区域(增) -->
  <form id="todo-form">
    <input type="text" id="task-input" placeholder="输入待办任务内容..." required>
    <button type="submit">添加任务</button>
  </form>

  <!-- 统计信息展示(查) -->
  <div class="todo-stats">
    <span>待办总数:<strong id="count-total">0</strong></span>
    <span>已完成:<strong id="count-done">0</strong></span>
  </div>

  <!-- 核心列表容器(挂载与委托的主体) -->
  <ul id="todo-list" class="todo-list"></ul>
</div>

2. 核心 JavaScript 实现逻辑

// ===============================
// 1. 查找关键 DOM 节点(查)
// ===============================
const form = document.querySelector('#todo-form');
const input = document.querySelector('#task-input');
const todoList = document.querySelector('#todo-list');
const countTotal = document.querySelector('#count-total');
const countDone = document.querySelector('#count-done');

// 内部状态计数器更新辅助函数
function updateStats() {
  const totalItems = todoList.querySelectorAll('.todo-item').length;
  const doneItems = todoList.querySelectorAll('.todo-item.completed').length;
  countTotal.textContent = totalItems;
  countDone.textContent = doneItems;
}

// ===============================
// 2. 新增任务逻辑(增)
// ===============================
form.addEventListener('submit', (e) => {
  e.preventDefault(); // 阻止表单提交引起的页面刷新
  
  const text = input.value.trim();
  if (!text) return;

  // 创建节点
  const li = document.createElement('li');
  li.className = 'todo-item';
  li.dataset.id = Date.now().toString(); // 生成唯一业务 ID

  // 复选框(切换状态)
  const checkbox = document.createElement('input');
  checkbox.type = 'checkbox';
  checkbox.className = 'toggle-done';

  // 任务文本内容
  const span = document.createElement('span');
  span.className = 'task-title';
  span.textContent = text; // 纯文本赋值,防范 XSS

  // 删除按钮
  const delBtn = document.createElement('button');
  delBtn.type = 'button';
  delBtn.className = 'btn-del';
  delBtn.textContent = '删除';

  // 组装并挂载节点
  li.append(checkbox, span, delBtn);
  todoList.appendChild(li);

  // 清空输入框并更新统计
  input.value = '';
  updateStats();
});

// ==============================================================
// 3. 事件委托核心:在父级 todoList 上统一监听改、删操作
// ==============================================================
todoList.addEventListener('click', (e) => {
  // -----------------------------------------------------------
  // 分支 A:点击删除按钮(删)
  // -----------------------------------------------------------
  const delBtn = e.target.closest('.btn-del');
  if (delBtn && todoList.contains(delBtn)) {
    const item = delBtn.closest('.todo-item');
    if (item) {
      item.remove(); // 直接调用 remove 安全移出 DOM
      updateStats();
    }
    return;
  }

  // -----------------------------------------------------------
  // 分支 B:点击复选框切换完成状态(改)
  // -----------------------------------------------------------
  const checkbox = e.target.closest('.toggle-done');
  if (checkbox && todoList.contains(checkbox)) {
    const item = checkbox.closest('.todo-item');
    if (item) {
      // 切换 classList 样式标记
      item.classList.toggle('completed', checkbox.checked);
      updateStats();
    }
    return;
  }
});

// ==============================================================
// 4. 双击任务内容就绪编辑(改)
// ==============================================================
todoList.addEventListener('dblclick', (e) => {
  const titleSpan = e.target.closest('.task-title');
  if (!titleSpan || !todoList.contains(titleSpan)) return;

  const currentText = titleSpan.textContent;
  const editInput = document.createElement('input');
  editInput.type = 'text';
  editInput.className = 'edit-input';
  editInput.value = currentText;

  // 替换当前文本为输入框
  titleSpan.replaceWith(editInput);
  editInput.focus();

  // 失去焦点或按回车时提交修改
  const finishEdit = () => {
    const newText = editInput.value.trim() || currentText;
    titleSpan.textContent = newText;
    editInput.replaceWith(titleSpan);
  };

  editInput.addEventListener('blur', finishEdit, { once: true });
  editInput.addEventListener('keydown', (keyEvent) => {
    if (keyEvent.key === 'Enter') {
      finishEdit();
    }
  });
});

七、DOM 操作与性能优化核心总结

为了在实际工程中保持界面的流畅度与代码的可维护性,开发者应当牢记以下架构原则:

  • 查先定界:优先在局部父容器(如 container.querySelector)内查找节点,缩小扫描范围,而非始终对全局 document 展开检索。
  • 增宜批量:涉及多个元素插入时,务必使用 DocumentFragment 在内存中合成完毕后一次性挂载,严格控制浏览器的重排重绘频次。
  • 改防注入:渲染来自用户输入的内容时,坚决使用 textContent;通过 classList 和 dataset 分别管理样式状态与业务属性。
  • 删用委托:利用 DOM 事件冒泡机制,将子项的交互逻辑全部委托给其相对稳定的父级容器,结合 closest() 精准识别目标,既能免去动态绑定烦恼,又杜绝了内存泄漏隐患。
JavaScript DOM操作实例:元素增删改查与事件委托实现
图 4:DOM 操作与事件委托架构总结
作者最新文章
相关文章
更多