pin_drop当前位置:知识文库 ❯ 图文

JS集成:HTML DOM创建与删除节点 - 详细教程与实战指南

教程简介

DOM节点的创建与删除是动态修改页面结构的核心操作。通过JavaScript,开发者可以在运行时动态创建新的元素和文本节点、将它们插入到DOM树中的指定位置、替换已有节点、克隆节点以及删除不再需要的节点。这些操作使得Web应用能够根据用户交互和数据变化实时更新页面内容。本教程将全面讲解DOM节点创建、插入、删除、替换和克隆的各种方法,以及DocumentFragment的使用和节点操作的性能优化策略。


核心概念

DOM节点操作方法一览

方法 功能 返回值
createElement 创建元素节点 新元素
createTextNode 创建文本节点 新文本节点
createComment 创建注释节点 新注释节点
createDocumentFragment 创建文档片段 新文档片段
appendChild 在末尾添加子节点 添加的节点
insertBefore 在指定节点前插入 插入的节点
removeChild 移除子节点 移除的节点
replaceChild 替换子节点 被替换的节点
cloneNode 克隆节点 克隆的节点
append 在末尾插入多个节点 undefined
prepend 在开头插入多个节点 undefined
before 在元素前插入节点 undefined
after 在元素后插入节点 undefined
remove 移除元素自身 undefined
replaceWith 替换元素自身 undefined

重排与重绘的影响

DOM节点的增删改会导致浏览器重新计算页面布局(重排)和重新绘制页面(重绘),这些操作是昂贵的。理解这一点对于编写高性能的DOM操作代码至关重要。


语法与用法

1. createElement

createElement 方法创建一个新的元素节点。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>createElement示例</title>
</head>
<body>
  <div id="container"></div>

  <script>
    // 创建元素
    var div = document.createElement('div');
    var p = document.createElement('p');
    var span = document.createElement('span');
    var ul = document.createElement('ul');
    var li = document.createElement('li');
    var input = document.createElement('input');
    var button = document.createElement('button');

    // 设置元素属性和内容
    div.id = 'myDiv';
    div.className = 'container';
    p.textContent = '这是一个段落';
    span.textContent = '行内文本';
    li.textContent = '列表项';
    input.type = 'text';
    input.placeholder = '请输入';
    button.textContent = '点击我';

    // 创建的元素不在DOM中,需要手动插入
    console.log(div.parentNode);  // null

    // 插入到DOM中
    var container = document.getElementById('container');
    container.appendChild(div);

    // 注意:createElement创建的元素在文档作用域内创建
    // 如果使用iframe,需要在正确的document上创建
    // var iframeDoc = iframe.contentDocument;
    // var iframeElement = iframeDoc.createElement('div');
  </script>
</body>
</html>

2. createTextNode

createTextNode 方法创建一个新的文本节点。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>createTextNode示例</title>
</head>
<body>
  <div id="container"></div>

  <script>
    var container = document.getElementById('container');

    // 创建文本节点
    var text1 = document.createTextNode('Hello, ');
    var text2 = document.createTextNode('World!');

    // 创建元素并添加文本节点
    var p = document.createElement('p');
    p.appendChild(text1);
    p.appendChild(text2);
    container.appendChild(p);

    // createTextNode vs textContent
    // textContent更简洁,推荐使用
    var p2 = document.createElement('p');
    p2.textContent = 'Hello, World!';  // 等效且更简洁

    // createTextNode的优势:可以精确控制文本节点的组合
    var p3 = document.createElement('p');
    var normalText = document.createTextNode('普通文本 ');
    var bold = document.createElement('strong');
    bold.textContent = '加粗文本';
    var moreText = document.createTextNode(' 更多文本');
    p3.appendChild(normalText);
    p3.appendChild(bold);
    p3.appendChild(moreText);
    container.appendChild(p3);

    // 注意:createTextNode会自动转义HTML特殊字符
    var safeText = document.createTextNode('<script>alert("XSS")<\/script>');
    // 页面显示纯文本,不会执行脚本
  </script>
</body>
</html>

3. appendChild

appendChild 方法将一个节点添加到指定父节点的子节点列表末尾。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>appendChild示例</title>
</head>
<body>
  <ul id="list">
    <li>已有项目1</li>
    <li>已有项目2</li>
  </ul>

  <script>
    var list = document.getElementById('list');

    // 在末尾添加新节点
    var newItem = document.createElement('li');
    newItem.textContent = '新增项目';
    list.appendChild(newItem);

    // appendChild的返回值是添加的节点
    var anotherItem = document.createElement('li');
    anotherItem.textContent = '另一个项目';
    var returnedNode = list.appendChild(anotherItem);
    console.log(returnedNode === anotherItem);  // true

    // 如果添加的节点已在DOM中,会从原位置移动到新位置
    var firstItem = list.children[0];
    list.appendChild(firstItem);  // 第一个项目移到末尾

    // 最终顺序:已有项目2 -> 新增项目 -> 另一个项目 -> 已有项目1
  </script>
</body>
</html>

4. insertBefore

insertBefore 方法在指定的子节点前插入一个新节点。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>insertBefore示例</title>
</head>
<body>
  <ul id="list">
    <li>项目1</li>
    <li>项目2</li>
    <li>项目3</li>
  </ul>

  <script>
    var list = document.getElementById('list');

    // 在第一个子节点前插入
    var firstItem = list.children[0];
    var beforeFirst = document.createElement('li');
    beforeFirst.textContent = '在项目1前插入';
    list.insertBefore(beforeFirst, firstItem);

    // 在第二个子节点前插入
    var secondItem = list.children[1];
    var beforeSecond = document.createElement('li');
    beforeSecond.textContent = '在项目2前插入';
    list.insertBefore(beforeSecond, secondItem);

    // 在末尾插入(参考节点为null)
    var atEnd = document.createElement('li');
    atEnd.textContent = '在末尾插入';
    list.insertBefore(atEnd, null);  // 等同于appendChild

    // 在指定元素后插入(没有insertAfter,需要组合实现)
    function insertAfter(newNode, referenceNode) {
      var parent = referenceNode.parentNode;
      var nextSibling = referenceNode.nextSibling;
      if (nextSibling) {
        parent.insertBefore(newNode, nextSibling);
      } else {
        parent.appendChild(newNode);
      }
    }

    var afterItem2 = document.createElement('li');
    afterItem2.textContent = '在项目2后插入';
    insertAfter(afterItem2, list.children[2]);  // 项目2

    // 最终顺序:
    // 在项目1前插入 -> 项目1 -> 在项目2前插入 -> 项目2 -> 在项目2后插入 -> 项目3 -> 在末尾插入
  </script>
</body>
</html>

5. removeChild

removeChild 方法从DOM中移除一个子节点。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>removeChild示例</title>
</head>
<body>
  <ul id="list">
    <li>项目1</li>
    <li>项目2</li>
    <li>项目3</li>
  </ul>

  <script>
    var list = document.getElementById('list');

    // 移除指定子节点
    var secondItem = list.children[1];
    var removedNode = list.removeChild(secondItem);
    console.log('移除的节点:', removedNode.textContent);  // "项目2"

    // removeChild返回被移除的节点,可以继续使用
    // removedNode仍然存在于内存中,只是不在DOM中
    console.log(removedNode.parentNode);  // null

    // 移除所有子节点
    while (list.firstChild) {
      list.removeChild(list.firstChild);
    }

    // 更高效的清空方式
    list.innerHTML = '';
    // 或者
    list.textContent = '';

    // 现代方法:元素自己移除自己
    // element.remove()  // 不需要父节点引用
  </script>
</body>
</html>

6. replaceChild

replaceChild 方法用一个新节点替换一个子节点。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>replaceChild示例</title>
</head>
<body>
  <div id="container">
    <p id="old-paragraph">旧段落</p>
  </div>

  <script>
    var container = document.getElementById('container');
    var oldParagraph = document.getElementById('old-paragraph');

    // 创建新节点
    var newParagraph = document.createElement('p');
    newParagraph.textContent = '新段落';
    newParagraph.className = 'new-content';

    // 替换节点
    var replacedNode = container.replaceChild(newParagraph, oldParagraph);
    console.log('被替换的节点:', replacedNode.textContent);  // "旧段落"

    // replacedNode仍然存在于内存中
    console.log(replacedNode.parentNode);  // null

    // 可以将替换下来的节点重新插入其他位置
    // document.body.appendChild(replacedNode);
  </script>
</body>
</html>

7. cloneNode

cloneNode 方法创建一个节点的副本。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>cloneNode示例</title>
</head>
<body>
  <div id="original" class="card" data-id="1">
    <h3>标题</h3>
    <p>段落内容</p>
    <ul>
      <li>列表项1</li>
      <li>列表项2</li>
    </ul>
  </div>

  <div id="clone-container"></div>

  <script>
    var original = document.getElementById('original');
    var cloneContainer = document.getElementById('clone-container');

    // 浅克隆:只克隆节点本身,不包含子节点
    var shallowClone = original.cloneNode(false);
    console.log('浅克隆子节点数:', shallowClone.childNodes.length);  // 0
    console.log('浅克隆class:', shallowClone.className);  // "card"
    console.log('浅克隆data-id:', shallowClone.dataset.id);  // "1"

    // 深克隆:克隆节点及其所有子节点
    var deepClone = original.cloneNode(true);
    console.log('深克隆子节点数:', deepClone.childNodes.length);  // 与原节点相同
    console.log('深克隆HTML:', deepClone.innerHTML);

    // 修改克隆节点(不影响原节点)
    deepClone.dataset.id = '2';
    deepClone.querySelector('h3').textContent = '克隆的标题';
    deepClone.id = 'cloned-card';

    cloneContainer.appendChild(deepClone);

    // cloneNode的注意事项:
    // 1. 克隆的节点不在DOM中,需要手动插入
    // 2. 克隆的id属性会重复,需要修改
    // 3. 事件监听器不会被克隆
    // 4. 内联事件处理器(onclick等)会被克隆
    // 5. 自定义属性(data-*)会被克隆
  </script>
</body>
</html>

8. DocumentFragment

DocumentFragment 是一个轻量级的文档对象,可以包含节点,但不是DOM树的一部分。

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>DocumentFragment示例</title>
</head>
<body>
  <ul id="list"></ul>

  <script>
    var list = document.getElementById('list');

    // 不使用DocumentFragment:多次DOM操作
    // 每次appendChild都可能触发重排
    for (var i = 0; i < 5; i++) {
      var li = document.createElement('li');
      li.textContent = '项目 ' + (i + 1);
      list.appendChild(li);  // 可能触发5次重排
    }

    // 清空
    list.innerHTML = '';

    // 使用DocumentFragment:只触发一次重排
    var fragment = document.createDocumentFragment();
    for (var i = 0; i < 5; i++) {
      var li = document.createElement('li');
      li.textContent = '项目 ' + (i + 1);
      fragment.appendChild(li);  // 不触发重排
    }
    list.appendChild(fragment);  // 只触发一次重排

    // DocumentFragment的特点:
    // 1. 插入DOM时,fragment本身不会被插入,只有其子节点被插入
    // 2. 插入后fragment变为空
    console.log(fragment.childNodes.length);  // 0

    // 使用现代语法创建Fragment
    // var fragment2 = new DocumentFragment();
  </script>
</body>
</html>

9. 现代节点操作方法

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>现代节点操作方法</title>
</head>
<body>
  <div id="container">
    <p id="target">目标元素</p>
  </div>

  <script>
    var container = document.getElementById('container');
    var target = document.getElementById('target');

    // append:在末尾插入多个节点(支持字符串和节点)
    container.append('文本', document.createElement('span'));
    // 注意:append没有返回值(undefined)

    // prepend:在开头插入多个节点
    container.prepend('开头文本', document.createElement('br'));

    // before:在元素前面插入节点
    target.before(document.createElement('hr'));

    // after:在元素后面插入节点
    target.after(document.createElement('hr'));

    // remove:移除元素自身
    // target.remove();  // 不需要父节点引用

    // replaceWith:替换元素自身
    // target.replaceWith(document.createElement('div'));

    // 与传统方法的对比:
    // append vs appendChild:
    //   - append支持多个参数和字符串
    //   - appendChild只支持单个节点
    //   - append没有返回值,appendChild返回插入的节点

    // prepend vs insertBefore:
    //   - prepend更简洁,自动插入到开头
    //   - insertBefore需要指定参考节点

    // remove vs removeChild:
    //   - remove直接在元素上调用
    //   - removeChild需要在父节点上调用

    // 字符串参数自动转为文本节点
    container.append('自动创建文本节点');
  </script>
</body>
</html>

代码示例

示例1:动态表格生成器

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>动态表格生成器</title>
  <style>
    body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    table { width: 100%; border-collapse: collapse; margin: 16px 0; }
    th, td { padding: 10px 12px; text-align: left; border: 1px solid #ddd; }
    th { background: #f5f5f5; font-weight: 600; }
    tr:hover { background: #f9f9f9; }
    .controls { margin: 16px 0; }
    .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin: 4px; }
    .btn-add { background: #28a745; color: white; }
    .btn-delete { background: #dc3545; color: white; padding: 4px 10px; font-size: 12px; }
    .btn-edit { background: #007bff; color: white; padding: 4px 10px; font-size: 12px; }
    input { padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; margin: 4px; }
    .empty-row td { text-align: center; color: #999; padding: 30px; }
  </style>
</head>
<body>
  <h1>动态表格生成器</h1>

  <div class="controls">
    <input type="text" id="name-input" placeholder="姓名">
    <input type="number" id="age-input" placeholder="年龄">
    <input type="text" id="city-input" placeholder="城市">
    <button class="btn btn-add" onclick="addRow()">添加行</button>
  </div>

  <table id="data-table">
    <thead>
      <tr>
        <th>序号</th>
        <th>姓名</th>
        <th>年龄</th>
        <th>城市</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody id="table-body">
      <tr class="empty-row" id="empty-row">
        <td colspan="5">暂无数据,请添加行</td>
      </tr>
    </tbody>
  </table>

  <script>
    var tableBody = document.getElementById('table-body');
    var emptyRow = document.getElementById('empty-row');
    var rowIndex = 0;

    function escapeHTML(str) {
      var div = document.createElement('div');
      div.textContent = str;
      return div.innerHTML;
    }

    function addRow() {
      var name = document.getElementById('name-input').value.trim();
      var age = document.getElementById('age-input').value.trim();
      var city = document.getElementById('city-input').value.trim();

      if (!name || !age || !city) {
        alert('请填写完整信息');
        return;
      }

      // 隐藏空数据提示
      if (emptyRow) {
        emptyRow.remove();
        emptyRow = null;
      }

      rowIndex++;

      // 使用DOM API创建行(安全且灵活)
      var tr = document.createElement('tr');
      tr.dataset.rowId = rowIndex;

      // 序号单元格
      var tdIndex = document.createElement('td');
      tdIndex.textContent = rowIndex;

      // 姓名单元格
      var tdName = document.createElement('td');
      tdName.textContent = name;

      // 年龄单元格
      var tdAge = document.createElement('td');
      tdAge.textContent = age;

      // 城市单元格
      var tdCity = document.createElement('td');
      tdCity.textContent = city;

      // 操作单元格
      var tdAction = document.createElement('td');
      var editBtn = document.createElement('button');
      editBtn.className = 'btn btn-edit';
      editBtn.textContent = '编辑';
      editBtn.addEventListener('click', function() { editRow(tr); });

      var deleteBtn = document.createElement('button');
      deleteBtn.className = 'btn btn-delete';
      deleteBtn.textContent = '删除';
      deleteBtn.addEventListener('click', function() { deleteRow(tr); });

      tdAction.appendChild(editBtn);
      tdAction.appendChild(deleteBtn);

      // 组装行
      tr.appendChild(tdIndex);
      tr.appendChild(tdName);
      tr.appendChild(tdAge);
      tr.appendChild(tdCity);
      tr.appendChild(tdAction);

      // 添加到表格
      tableBody.appendChild(tr);

      // 清空输入
      document.getElementById('name-input').value = '';
      document.getElementById('age-input').value = '';
      document.getElementById('city-input').value = '';
    }

    function deleteRow(tr) {
      tr.remove();

      // 检查是否还有数据行
      if (tableBody.children.length === 0) {
        emptyRow = document.createElement('tr');
        emptyRow.className = 'empty-row';
        emptyRow.id = 'empty-row';
        var td = document.createElement('td');
        td.colSpan = 5;
        td.textContent = '暂无数据,请添加行';
        emptyRow.appendChild(td);
        tableBody.appendChild(emptyRow);
      }
    }

    function editRow(tr) {
      var cells = tr.querySelectorAll('td');
      var nameCell = cells[1];
      var ageCell = cells[2];
      var cityCell = cells[3];

      // 将文本单元格转为输入框
      if (!tr.dataset.editing) {
        tr.dataset.editing = 'true';

        var nameInput = document.createElement('input');
        nameInput.type = 'text';
        nameInput.value = nameCell.textContent;
        nameInput.style.width = '80px';
        nameCell.textContent = '';
        nameCell.appendChild(nameInput);

        var ageInput = document.createElement('input');
        ageInput.type = 'number';
        ageInput.value = ageCell.textContent;
        ageInput.style.width = '60px';
        ageCell.textContent = '';
        ageCell.appendChild(ageInput);

        var cityInput = document.createElement('input');
        cityInput.type = 'text';
        cityInput.value = cityCell.textContent;
        cityInput.style.width = '80px';
        cityCell.textContent = '';
        cityCell.appendChild(cityInput);

        // 修改按钮文本
        var editBtn = tr.querySelector('.btn-edit');
        editBtn.textContent = '保存';
      } else {
        // 保存编辑
        delete tr.dataset.editing;

        var nameInput = nameCell.querySelector('input');
        var ageInput = ageCell.querySelector('input');
        var cityInput = cityCell.querySelector('input');

        nameCell.textContent = nameInput.value;
        ageCell.textContent = ageInput.value;
        cityCell.textContent = cityInput.value;

        var editBtn = tr.querySelector('.btn-edit');
        editBtn.textContent = '编辑';
      }
    }
  </script>
</body>
</html>

示例2:DocumentFragment批量操作

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DocumentFragment批量操作</title>
  <style>
    body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; margin: 16px 0; }
    .card { border: 1px solid #ddd; border-radius: 8px; padding: 12px; text-align: center; transition: transform 0.2s; }
    .card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
    .card img { width: 100%; height: 100px; object-fit: cover; border-radius: 4px; }
    .card h4 { margin: 8px 0 4px; font-size: 14px; }
    .card p { margin: 0; color: #666; font-size: 12px; }
    .btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin: 4px; color: white; }
    .perf-info { margin: 12px 0; padding: 12px; background: #f5f5f5; border-radius: 4px; font-family: monospace; }
  </style>
</head>
<body>
  <h1>DocumentFragment批量操作</h1>

  <div>
    <button class="btn" style="background:#dc3545" onclick="renderWithoutFragment()">不使用Fragment渲染</button>
    <button class="btn" style="background:#28a745" onclick="renderWithFragment()">使用Fragment渲染</button>
    <button class="btn" style="background:#6c757d" onclick="clearGrid()">清空</button>
  </div>

  <div class="perf-info" id="perf-info">点击按钮开始测试</div>
  <div class="grid" id="grid"></div>

  <script>
    var grid = document.getElementById('grid');
    var perfInfo = document.getElementById('perf-info');

    // 模拟数据
    var products = [];
    for (var i = 1; i <= 50; i++) {
      products.push({
        id: i,
        name: '商品 ' + i,
        price: (Math.random() * 500 + 50).toFixed(2),
        color: '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')
      });
    }

    function createProductCard(product) {
      var card = document.createElement('div');
      card.className = 'card';

      var img = document.createElement('div');
      img.style.cssText = 'width:100%;height:100px;border-radius:4px;background:' + product.color;

      var name = document.createElement('h4');
      name.textContent = product.name;

      var price = document.createElement('p');
      price.textContent = '¥' + product.price;

      card.appendChild(img);
      card.appendChild(name);
      card.appendChild(price);

      return card;
    }

    // 不使用Fragment:逐个添加
    function renderWithoutFragment() {
      clearGrid();
      var start = performance.now();

      products.forEach(function(product) {
        grid.appendChild(createProductCard(product));  // 每次都可能触发重排
      });

      var elapsed = (performance.now() - start).toFixed(2);
      perfInfo.textContent = '不使用Fragment:' + elapsed + 'ms(逐个appendChild)';
    }

    // 使用Fragment:批量添加
    function renderWithFragment() {
      clearGrid();
      var start = performance.now();

      var fragment = document.createDocumentFragment();
      products.forEach(function(product) {
        fragment.appendChild(createProductCard(product));  // 不触发重排
      });
      grid.appendChild(fragment);  // 只触发一次重排

      var elapsed = (performance.now() - start).toFixed(2);
      perfInfo.textContent = '使用Fragment:' + elapsed + 'ms(批量appendChild)';
    }

    function clearGrid() {
      grid.innerHTML = '';
    }
  </script>
</body>
</html>

示例3:节点克隆与模板复用

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>节点克隆与模板复用</title>
  <style>
    body { font-family: -apple-system, sans-serif; max-width: 700px; margin: 0 auto; padding: 20px; }
    .template { display: none; }  /* 隐藏模板 */
    .user-card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin: 12px 0; display: flex; align-items: center; gap: 16px; }
    .user-card .avatar { width: 50px; height: 50px; border-radius: 50%; background: #007bff; color: white; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: bold; }
    .user-card .info { flex: 1; }
    .user-card .info h3 { margin: 0 0 4px; font-size: 16px; }
    .user-card .info p { margin: 0; color: #666; font-size: 13px; }
    .user-card .actions { display: flex; gap: 8px; }
    .btn { padding: 4px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
    .btn-view { background: #17a2b8; color: white; }
    .btn-delete { background: #dc3545; color: white; }
  </style>
</head>
<body>
  <h1>节点克隆与模板复用</h1>

  <!-- 模板:使用<template>标签 -->
  <template id="user-card-template">
    <div class="user-card">
      <div class="avatar"></div>
      <div class="info">
        <h3 class="name"></h3>
        <p class="email"></p>
        <p class="role"></p>
      </div>
      <div class="actions">
        <button class="btn btn-view">查看</button>
        <button class="btn btn-delete">删除</button>
      </div>
    </div>
  </template>

  <div id="user-list"></div>

  <script>
    var template = document.getElementById('user-card-template');
    var userList = document.getElementById('user-list');

    var users = [
      { id: 1, name: '张三', email: 'zhangsan@example.com', role: '管理员' },
      { id: 2, name: '李四', email: 'lisi@example.com', role: '编辑' },
      { id: 3, name: '王五', email: 'wangwu@example.com', role: '用户' },
      { id: 4, name: '赵六', email: 'zhaoliu@example.com', role: '编辑' }
    ];

    function createUserCard(user) {
      // 克隆模板内容
      var clone = template.content.cloneNode(true);
      var card = clone.querySelector('.user-card');

      // 填充数据
      card.dataset.userId = user.id;
      card.querySelector('.avatar').textContent = user.name.charAt(0);
      card.querySelector('.name').textContent = user.name;
      card.querySelector('.email').textContent = user.email;
      card.querySelector('.role').textContent = user.role;

      // 绑定事件
      card.querySelector('.btn-view').addEventListener('click', function() {
        alert('查看用户:' + user.name);
      });

      card.querySelector('.btn-delete').addEventListener('click', function() {
        card.remove();
      });

      return clone;
    }

    // 使用DocumentFragment批量渲染
    var fragment = document.createDocumentFragment();
    users.forEach(function(user) {
      fragment.appendChild(createUserCard(user));
    });
    userList.appendChild(fragment);
  </script>
</body>
</html>

浏览器兼容性

节点操作方法兼容性

方法 Chrome Firefox Safari Edge IE
createElement 全部 全部 全部 全部 全部
createTextNode 全部 全部 全部 全部 全部
createDocumentFragment 全部 全部 全部 全部 全部
appendChild 全部 全部 全部 全部 全部
insertBefore 全部 全部 全部 全部 全部
removeChild 全部 全部 全部 全部 全部
replaceChild 全部 全部 全部 全部 全部
cloneNode 全部 全部 全部 全部 全部
append 54+ 49+ 10+ 17+ 不支持
prepend 54+ 49+ 10+ 17+ 不支持
before 54+ 49+ 10+ 17+ 不支持
after 54+ 49+ 10+ 17+ 不支持
remove 23+ 23+ 7+ 12+ 不支持
replaceWith 54+ 49+ 10+ 17+ 不支持
<template> 26+ 22+ 7.1+ 13+ 不支持

现代方法的兼容性写法

代码示例

// remove兼容
if (!Element.prototype.remove) {
  Element.prototype.remove = function() {
    if (this.parentNode) {
      this.parentNode.removeChild(this);
    }
  };
}

// append兼容
if (!Element.prototype.append) {
  Element.prototype.append = function() {
    var args = Array.prototype.slice.call(arguments);
    var docFrag = document.createDocumentFragment();
    args.forEach(function(arg) {
      var node = (typeof arg === 'string') ? document.createTextNode(arg) : arg;
      docFrag.appendChild(node);
    });
    this.appendChild(docFrag);
  };
}

// prepend兼容
if (!Element.prototype.prepend) {
  Element.prototype.prepend = function() {
    var args = Array.prototype.slice.call(arguments);
    var docFrag = document.createDocumentFragment();
    args.forEach(function(arg) {
      var node = (typeof arg === 'string') ? document.createTextNode(arg) : arg;
      docFrag.appendChild(node);
    });
    this.insertBefore(docFrag, this.firstChild);
  };
}

注意事项与最佳实践

1. 使用DocumentFragment减少重排

代码示例

// 不推荐:逐个添加节点
for (var i = 0; i < 100; i++) {
  var li = document.createElement('li');
  li.textContent = 'Item ' + i;
  list.appendChild(li);  // 可能触发100次重排
}

// 推荐:使用DocumentFragment
var fragment = document.createDocumentFragment();
for (var i = 0; i < 100; i++) {
  var li = document.createElement('li');
  li.textContent = 'Item ' + i;
  fragment.appendChild(li);  // 不触发重排
}
list.appendChild(fragment);  // 只触发1次重排

2. 克隆节点时注意id冲突

代码示例

// 克隆节点后必须修改id
var clone = original.cloneNode(true);
clone.id = original.id + '-copy';  // 避免id重复

// 或者移除id
clone.removeAttribute('id');

3. 事件监听器的处理

代码示例

// cloneNode不会克隆事件监听器
var btn = document.createElement('button');
btn.addEventListener('click', handler);
document.body.appendChild(btn);

var clone = btn.cloneNode(true);
// clone没有click事件监听器

// 解决方案1:使用事件委托
document.addEventListener('click', function(e) {
  if (e.target.matches('.my-btn')) {
    handler(e);
  }
});

// 解决方案2:克隆后重新绑定
clone.addEventListener('click', handler);

// 注意:内联事件处理器(onclick属性)会被克隆
var btn2 = document.createElement('button');
btn2.onclick = function() { alert('click'); };
var clone2 = btn2.cloneNode(true);
// clone2.onclick仍然有效(因为它是属性,不是addEventListener)

4. 使用template标签作为模板

代码示例

<!-- 推荐:使用<template>标签 -->
<template id="card-template">
  <div class="card">
    <h3 class="title"></h3>
    <p class="content"></p>
  </div>
</template>

<script>
  var template = document.getElementById('card-template');
  var clone = template.content.cloneNode(true);
  clone.querySelector('.title').textContent = '标题';
  clone.querySelector('.content').textContent = '内容';
  document.body.appendChild(clone);
</script>

<!-- 不推荐:使用隐藏div作为模板 -->
<div id="card-template" style="display:none">
  <div class="card">
    <h3 class="title"></h3>
    <p class="content"></p>
  </div>
</div>
<!-- 问题:图片会加载,脚本会执行,样式会应用 -->

5. 清空元素内容的高效方式

代码示例

// 方式1:innerHTML(简单但可能有内存泄漏)
element.innerHTML = '';

// 方式2:textContent(最快)
element.textContent = '';

// 方式3:while循环(最安全,确保事件监听器被清理)
while (element.firstChild) {
  element.removeChild(element.firstChild);
}

// 方式4:replaceChildren(现代方法,最推荐)
element.replaceChildren();  // 清空所有子节点

// 方式4兼容性写法
if (!Element.prototype.replaceChildren) {
  Element.prototype.replaceChildren = function() {
    while (this.firstChild) {
      this.removeChild(this.firstChild);
    }
  };
}

代码规范示例

节点操作规范

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>节点操作规范</title>
</head>
<body>
  <div id="app"></div>

  <script>
    (function() {
      'use strict';

      // 1. 工具函数:安全创建元素
      function createElement(tag, attrs, children) {
        var el = document.createElement(tag);

        if (attrs) {
          Object.keys(attrs).forEach(function(key) {
            if (key === 'className') {
              el.className = attrs[key];
            } else if (key === 'dataset') {
              Object.keys(attrs[key]).forEach(function(dataKey) {
                el.dataset[dataKey] = attrs[key][dataKey];
              });
            } else if (key.startsWith('on')) {
              el.addEventListener(key.slice(2).toLowerCase(), attrs[key]);
            } else {
              el.setAttribute(key, attrs[key]);
            }
          });
        }

        if (children !== undefined) {
          if (typeof children === 'string') {
            el.textContent = children;
          } else if (Array.isArray(children)) {
            var fragment = document.createDocumentFragment();
            children.forEach(function(child) {
              if (typeof child === 'string') {
                fragment.appendChild(document.createTextNode(child));
              } else if (child instanceof Node) {
                fragment.appendChild(child);
              }
            });
            el.appendChild(fragment);
          } else if (children instanceof Node) {
            el.appendChild(children);
          }
        }

        return el;
      }

      // 2. 使用DocumentFragment批量操作
      function renderList(container, items) {
        var fragment = document.createDocumentFragment();

        items.forEach(function(item) {
          var li = createElement('li', {
            className: 'list-item',
            dataset: { id: item.id }
          }, item.text);
          fragment.appendChild(li);
        });

        container.textContent = '';  // 清空
        container.appendChild(fragment);  // 一次性插入
      }

      // 3. 使用template标签
      function createFromTemplate(templateId, data) {
        var template = document.getElementById(templateId);
        if (!template) return null;

        var clone = template.content.cloneNode(true);
        // 填充数据...
        return clone;
      }

      // 4. 安全移除节点
      function safeRemove(element) {
        if (element && element.parentNode) {
          element.parentNode.removeChild(element);
        }
      }

      // 使用示例
      var app = document.getElementById('app');
      var header = createElement('h1', { className: 'title' }, '节点操作规范示例');
      var list = createElement('ul', { className: 'item-list' });

      app.appendChild(header);
      app.appendChild(list);

      renderList(list, [
        { id: '1', text: '使用DocumentFragment批量操作' },
        { id: '2', text: '使用createElement创建元素' },
        { id: '3', text: '使用textContent设置文本' },
        { id: '4', text: '注意事件监听器的清理' }
      ]);
    })();
  </script>
</body>
</html>

常见问题与解决方案

问题1:大量节点操作导致页面卡顿

症状: 添加数千个节点时页面明显卡顿。

解决方案:

代码示例

// 方案1:使用DocumentFragment
var fragment = document.createDocumentFragment();
for (var i = 0; i < 5000; i++) {
  var li = document.createElement('li');
  li.textContent = 'Item ' + i;
  fragment.appendChild(li);
}
list.appendChild(fragment);

// 方案2:分批渲染(虚拟滚动思路)
function renderBatch(items, container, batchSize) {
  var index = 0;

  function renderNextBatch() {
    var fragment = document.createDocumentFragment();
    var end = Math.min(index + batchSize, items.length);

    for (var i = index; i < end; i++) {
      var li = document.createElement('li');
      li.textContent = items[i];
      fragment.appendChild(li);
    }

    container.appendChild(fragment);
    index = end;

    if (index < items.length) {
      requestAnimationFrame(renderNextBatch);
    }
  }

  renderNextBatch();
}

// 方案3:隐藏容器后操作
container.style.display = 'none';
// ... 大量操作
container.style.display = '';

问题2:cloneNode后事件监听器丢失

症状: 克隆的节点点击没有反应。

解决方案:

代码示例

// 方案1:使用事件委托(推荐)
document.addEventListener('click', function(e) {
  if (e.target.matches('.action-btn')) {
    handleClick(e);
  }
});

// 方案2:克隆后重新绑定
function cloneWithEvents(element) {
  var clone = element.cloneNode(true);
  // 重新绑定事件
  clone.addEventListener('click', handleClick);
  return clone;
}

// 方案3:存储事件处理函数,克隆后批量绑定
var eventHandlers = {
  '.btn-save': { click: handleSave },
  '.btn-delete': { click: handleDelete }
};

function cloneAndBind(element) {
  var clone = element.cloneNode(true);
  Object.keys(eventHandlers).forEach(function(selector) {
    var el = clone.querySelector(selector);
    if (el) {
      Object.keys(eventHandlers[selector]).forEach(function(event) {
        el.addEventListener(event, eventHandlers[selector][event]);
      });
    }
  });
  return clone;
}

问题3:IE不支持template标签

解决方案:

代码示例

// 兼容性处理template
function getTemplateContent(template) {
  if (template.content) {
    return template.content.cloneNode(true);
  }

  // IE兼容:手动提取template内容
  var fragment = document.createDocumentFragment();
  var children = template.childNodes;
  for (var i = 0; i < children.length; i++) {
    fragment.appendChild(children[i].cloneNode(true));
  }
  return fragment;
}

问题4:removeChild与remove的区别

代码示例

// removeChild:在父节点上调用,需要先获取父节点
var parent = element.parentNode;
if (parent) {
  parent.removeChild(element);  // 返回被移除的节点
}

// remove:在元素自身上调用,更简洁
element.remove();  // 无返回值

// remove兼容性写法
if (!Element.prototype.remove) {
  Element.prototype.remove = function() {
    if (this.parentNode) {
      this.parentNode.removeChild(this);
    }
  };
}

总结

DOM节点的创建与删除是构建动态Web应用的基础,掌握各种节点操作方法及其性能特点对于编写高效代码至关重要:

  1. createElement:创建元素节点,配合textContent设置文本

  2. createTextNode:创建文本节点,自动转义HTML字符

  3. appendChild/insertBefore:插入节点,注意已有节点会移动而非复制

  4. removeChild/remove:移除节点,remove更简洁但IE不支持

  5. replaceChild/replaceWith:替换节点,被替换的节点仍在内存中

  6. cloneNode:克隆节点,深克隆包含子节点,注意id冲突和事件监听器

  7. DocumentFragment:批量操作的利器,减少重排次数

  8. template标签:HTML模板的标准方式,内容不渲染不执行

  9. 性能优化:使用Fragment、分批渲染、隐藏容器操作、事件委托

  10. 现代方法:append/prepend/before/after/remove/replaceWith更简洁

遵循"批量操作、减少重排、安全创建"的原则,能够编写出高性能、可维护的DOM操作代码。

常见问题

问题1:大量节点操作导致页面卡顿?

症状: 添加数千个节点时页面明显卡顿。 解决方案:

问题2:cloneNode后事件监听器丢失?

症状: 克隆的节点点击没有反应。 解决方案:

问题3:IE不支持template标签?

解决方案:

标签: Script标签 JavaScript DOM HTML 模板 节点操作

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: JS集成:HTML DOM修改样式 - 详细教程与实战指南 下一篇: JS集成:HTML DOM事件监听:ad - 详细教程与实战指南

poll相关推荐