跟着 MDN 学 HTML day_29:(动态构建与更新 DOM 树)

引言

在掌握了 DOM 树的结构和节点导航之后,下一步就是学习如何动态地创建、修改和删除 DOM 节点。DOM Level 1 核心规范提供了一系列基础方法,使得开发者可以通过 JavaScript 在运行时构建完整的页面结构。本文将通过实际示例,系统地介绍 createElementcreateTextNodeappendChildremoveChild 等核心方法的使用方式,以及动态操作 DOM 时应遵循的最佳实践。

一、动态创建 HTML 表格

动态创建 DOM 结构需要遵循自顶向下创建、自底向上挂载的原则。下面的示例展示了如何通过按钮点击事件,动态生成一个完整的表格并添加到页面中。

html 复制代码
<button id="generate-btn">生成表格</button>
<div id="table-container"></div>

<script>
  function generateTable() {
    // 第一步:创建最外层的 table 元素
    const tbl = document.createElement("table");
    // 第二步:创建 tbody 元素,作为 table 的子节点
    const tblBody = document.createElement("tbody");

    // 第三步:循环创建行和单元格
    for (let i = 0; i < 3; i++) {
      // 创建 tr 元素,作为 tbody 的子节点
      const row = document.createElement("tr");

      for (let j = 0; j < 3; j++) {
        // 创建 td 元素,作为 tr 的子节点
        const cell = document.createElement("td");
        // 创建文本节点,作为 td 的子节点
        const cellText = document.createTextNode(
          `第 ${i} 行,第 ${j} 列`
        );
        // 自底向上挂载:先将文本节点挂载到 td
        cell.appendChild(cellText);
        // 再将 td 挂载到 tr
        row.appendChild(cell);
      }

      // 将 tr 挂载到 tbody
      tblBody.appendChild(row);
    }

    // 将 tbody 挂载到 table
    tbl.appendChild(tblBody);
    // 设置表格边框属性
    tbl.setAttribute("border", "1");

    // 最后将 table 挂载到页面中的容器
    document.getElementById("table-container").appendChild(tbl);
  }

  document
    .getElementById("generate-btn")
    .addEventListener("click", generateTable);
</script>

创建 DOM 结构的关键思路是:先在内存中从外到内依次创建所有元素节点和文本节点,然后从内到外依次将子节点挂载到父节点上。如果打乱这个顺序,可能会导致节点关系混乱或遗漏。

二、查询现有元素并修改样式

除了创建新元素,DOM API 也提供了多种查询现有元素的方法。getElementsByTagName 可以在指定元素或整个文档中根据标签名查找后代元素,返回一个动态的 HTMLCollection。

html 复制代码
<input type="button" value="设置段落背景色" id="style-btn" />
<p>第一段文字</p>
<p>第二段文字</p>
<p>第三段文字</p>

<script>
  function setBackground() {
    // 获取文档中所有的 p 元素
    const paragraphs = document.getElementsByTagName("p");

    // 通过索引访问特定段落
    const secondParagraph = paragraphs[1];

    // 直接修改 style 属性设置背景色
    secondParagraph.style.background = "lightblue";
    secondParagraph.style.padding = "10px";
    secondParagraph.style.borderRadius = "4px";
  }

  document
    .getElementById("style-btn")
    .addEventListener("click", setBackground);
</script>

getElementsByTagName 返回的是动态集合,这意味着如果文档中的段落元素发生变化,集合会自动更新。这一特性在需要实时反映 DOM 状态时很有用,但在遍历集合的同时修改 DOM 结构时需要格外小心,避免出现意外的跳过或死循环。

三、创建文本节点与 appendChild 的使用细节

createTextNode 方法用于创建一个纯文本节点,该节点的 nodeType 为 TEXT_NODE。创建后必须通过 appendChild 将其挂载到某个元素节点上,才能在页面中显示。

html 复制代码
<p id="greeting">你好</p>
<button id="append-btn">添加文本</button>
<button id="reset-btn">重置</button>

<script>
  const greeting = document.getElementById("greeting");
  const appendBtn = document.getElementById("append-btn");
  const resetBtn = document.getElementById("reset-btn");

  // 保存初始文本节点的引用,方便后续操作
  let worldTextNode = null;

  appendBtn.addEventListener("click", () => {
    // 创建新的文本节点
    worldTextNode = document.createTextNode(" 世界");
    // 将文本节点追加为 p 元素的最后一个子节点
    greeting.appendChild(worldTextNode);
    // 此时页面显示 "你好 世界"
    // DOM 结构中有两个文本节点:"你好" 和 " 世界"
  });

  resetBtn.addEventListener("click", () => {
    if (worldTextNode && worldTextNode.parentNode === greeting) {
      // removeChild 用于移除子节点
      greeting.removeChild(worldTextNode);
      // 页面恢复为 "你好"
    }
  });
</script>

appendChild 总是将新节点添加为父节点的最后一个子节点。如果需要在特定位置插入节点,应该使用 insertBefore 方法。文本节点虽然看起来和周围的文本连成一片,但在 DOM 树中它们是独立存在的节点对象。

四、使用 createElement 创建新元素

createElement 是动态构建 DOM 的核心方法之一。它接受标签名作为参数,返回一个尚未挂载到文档树中的元素节点。新创建的元素可以设置属性、添加子节点,然后通过 appendChild 插入到文档中。

html 复制代码
<div id="post-area">
  <p>这是一篇已有的内容。</p>
</div>
<button id="add-post-btn">发布新内容</button>

<script>
  const postArea = document.getElementById("post-area");
  const addPostBtn = document.getElementById("add-post-btn");

  addPostBtn.addEventListener("click", () => {
    // 创建一个新的 article 元素
    const newArticle = document.createElement("article");

    // 创建标题
    const heading = document.createElement("h3");
    const headingText = document.createTextNode(
      `新文章 - ${new Date().toLocaleTimeString()}`
    );
    heading.appendChild(headingText);

    // 创建正文段落
    const paragraph = document.createElement("p");
    const paraText = document.createTextNode("这是动态生成的文章内容。");
    paragraph.appendChild(paraText);

    // 将标题和段落追加到 article 中
    newArticle.appendChild(heading);
    newArticle.appendChild(paragraph);

    // 给 article 添加类名和样式
    newArticle.className = "dynamic-post";
    newArticle.style.border = "1px solid #ccc";
    newArticle.style.margin = "10px 0";
    newArticle.style.padding = "10px";

    // 将完整的 article 追加到页面中
    postArea.appendChild(newArticle);
  });
</script>

createElement 创建的节点在挂载之前是完全独立的,可以进行各种属性设置和子节点添加操作。这种在内存中先构建完整子树再一次性挂载的方式,比逐步向文档中添加节点具有更好的性能,因为减少了浏览器的重排次数。

五、removeChild 移除节点的正确方式

removeChild 方法用于从父节点中移除指定的子节点。被移除的节点仍然存在于内存中,可以重新挂载到文档的其他位置,实现节点的移动效果。

html 复制代码
<ul id="source-list">
  <li id="item-1">可移动的项目一</li>
  <li id="item-2">可移动的项目二</li>
  <li id="item-3">可移动的项目三</li>
</ul>
<ul id="target-list">
  <li>目标列表的固定项目</li>
</ul>
<button id="move-btn">移动第一个项目</button>

<script>
  const sourceList = document.getElementById("source-list");
  const targetList = document.getElementById("target-list");
  const moveBtn = document.getElementById("move-btn");

  moveBtn.addEventListener("click", () => {
    // 检查源列表中是否还有可移动的项目
    if (sourceList.children.length > 0) {
      const itemToMove = sourceList.firstElementChild;

      // 从源列表中移除
      sourceList.removeChild(itemToMove);

      // 追加到目标列表中
      targetList.appendChild(itemToMove);

      console.log(`已移动:${itemToMove.textContent}`);
    } else {
      console.log("源列表已无项目可移动");
    }
  });
</script>

removeChild 的调用者是父节点,参数是要移除的子节点。被移除的节点并没有被销毁,它在 JavaScript 中的引用依然有效。这种机制使得在列表之间移动元素变得非常方便,只需先从原父节点移除,再追加到新父节点即可。

六、通过节点关系深入遍历表格结构

当需要从复杂的 DOM 结构中提取特定数据时,可以结合多种遍历方法来定位目标节点。childNodes 属性会返回所有类型的子节点,包括元素节点和文本节点。

html 复制代码
<table id="data-table" border="1">
  <tbody>
    <tr>
      <td>姓名</td>
      <td>年龄</td>
      <td>城市</td>
    </tr>
    <tr>
      <td>张三</td>
      <td>28</td>
      <td>北京</td>
    </tr>
    <tr>
      <td>李四</td>
      <td>35</td>
      <td>上海</td>
    </tr>
  </tbody>
</table>
<button id="extract-btn">提取第二行第二列数据</button>
<p id="result-display"></p>

<script>
  const extractBtn = document.getElementById("extract-btn");
  const resultDisplay = document.getElementById("result-display");

  extractBtn.addEventListener("click", () => {
    // 通过关系链逐层深入到目标单元格
    const myTable = document.getElementById("data-table");
    // 获取 tbody
    const myTableBody = myTable.getElementsByTagName("tbody")[0];
    // 获取第二行(索引为 1)
    const myRow = myTableBody.getElementsByTagName("tr")[1];
    // 获取第二列(索引为 1)
    const myCell = myRow.getElementsByTagName("td")[1];

    // childNodes[0] 获取该单元格的第一个子节点(通常是文本节点)
    const myCellText = myCell.childNodes[0];

    // 使用 data 属性获取文本节点的内容
    resultDisplay.textContent = `提取结果:${myCellText.data}`;
    console.log("提取的文本内容:", myCellText.data);
  });
</script>

childNodes 与 getElementsByTagName 的一个重要区别是:前者返回所有类型的节点(包括文本节点和注释节点),而后者只返回指定标签名的元素节点。在使用 childNodes 时要注意索引位置可能包含因空白字符产生的文本节点。

七、操作属性值与动态控制样式

getAttribute 和 setAttribute 是操作元素属性的基本方法。此外,通过 style 属性可以直接修改元素的内联样式,实现对页面外观的动态控制。

html 复制代码
<table id="style-table" border="1">
  <tbody>
    <tr>
      <td>第一列</td>
      <td>第二列</td>
      <td>第三列</td>
    </tr>
    <tr>
      <td>数据 1-1</td>
      <td>数据 1-2</td>
      <td>数据 1-3</td>
    </tr>
    <tr>
      <td>数据 2-1</td>
      <td>数据 2-2</td>
      <td>数据 2-3</td>
    </tr>
  </tbody>
</table>
<button id="highlight-btn">高亮第一列并隐藏第二列</button>
<button id="check-border-btn">查看表格边框属性</button>

<script>
  const styleTable = document.getElementById("style-table");
  const highlightBtn = document.getElementById("highlight-btn");
  const checkBorderBtn = document.getElementById("check-border-btn");

  highlightBtn.addEventListener("click", () => {
    // 获取所有行
    const rows = styleTable.getElementsByTagName("tr");

    for (let i = 0; i < rows.length; i++) {
      const cells = rows[i].getElementsByTagName("td");

      for (let j = 0; j < cells.length; j++) {
        if (j === 0) {
          // 第一列:设置红色背景
          cells[j].style.background = "#ffcccc";
          cells[j].style.fontWeight = "bold";
        } else if (j === 1) {
          // 第二列:隐藏
          cells[j].style.display = "none";
        }
      }
    }
  });

  checkBorderBtn.addEventListener("click", () => {
    // 使用 getAttribute 获取属性值
    const borderValue = styleTable.getAttribute("border");
    console.log("表格 border 属性值:", borderValue);

    // 也可以直接通过属性名访问
    console.log("通过属性访问:", styleTable.border);
  });
</script>

通过 style 对象设置的样式会直接写入元素的内联 style 属性中,优先级高于外部样式表。如果需要批量控制样式,更好的做法是动态增删 CSS 类名,这样可以将样式逻辑与 JavaScript 代码分离,便于维护。

总结

动态构建和更新 DOM 树是前端开发中最基础也最频繁的操作之一。本文通过多个示例梳理了以下核心知识点:

创建节点的基本流程是自上而下创建、自下而上挂载,先构建完整的子树再一次性插入文档可以提升性能。createElement 用于创建任意标签的元素节点,createTextNode 用于创建文本内容节点。appendChild 将新节点添加为父节点的最后一个子节点,被移除的节点可以通过 removeChild 重新挂载到其他位置。getElementsByTagName 返回动态集合,自动反映 DOM 变化;childNodes 返回所有类型的子节点,包括文本节点。getAttribute 和 setAttribute 用于操作元素的 HTML 属性值,而 style 对象可以直接控制内联样式。利用 parentNode 和 childNodes 等关系属性,可以沿 DOM 树层层深入,精确定位到目标节点并提取其数据。

熟练掌握这些基础方法,是进行复杂 DOM 操作、实现动态交互效果的前提。在实际项目中,合理组合这些方法可以构建出灵活且高性能的用户界面。


想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

相关推荐
编程技术手记2 小时前
html table布局平衡
前端·html
huoyueyi2 小时前
3D数字孪生项目 LCP 优化指南
前端·3d·几何学
菜鸟小芯2 小时前
【腾讯位置服务开发者征文大赛】校园美食雷达 —— 基于 CodeBuddy + 腾讯 LBS 开发实战
前端·美食
搜狐技术产品小编20233 小时前
深度解析与业务实战:将 screenshot-to-code 改造为支持 React + Ant Design 的前端利器
前端·javascript·react.js·前端框架·ecmascript
Rik3 小时前
Cursor Rules 深度玩法:从全局配置到项目级规则,让 AI 真正理解你的项目
前端·后端
weixin_471383033 小时前
set和map结构,减少O(n)复杂度
前端·javascript
hunteritself3 小时前
GPT Image2 + Seedance 2.0:3 小时从剧本到 AI 互动影游,深度实测复盘
前端·数据库·人工智能·深度学习·transformer
独秀不如众秀3 小时前
前端页面引擎协议:由浅入深——从 30 行到 vform3 的演化之路
前端
学网安的肆伍3 小时前
【044-WEB攻防篇】PHP应用&SQL盲注&布尔回显&延时判断&报错处理&增删改查方式
前端·sql·php