Skip to content

创建 DOM 元素:动态构建页面内容

为什么需要动态创建元素

静态 HTML 只能展示固定的内容。而现代网页往往需要根据用户操作、服务器数据或其他条件动态更新界面——添加新的列表项、插入提示消息、渲染从 API 获取的数据等等。这些都需要在 JavaScript 中创建新的 DOM 元素。

动态创建元素有几种主要方法,各有其适用场景。选择合适的方法不仅影响代码的可读性,还关系到页面的性能和安全性。

创建元素的基本方法

createElement

document.createElement() 是创建新元素的核心方法:

javascript
// 创建一个新的 div 元素
const div = document.createElement("div");

// 创建一个按钮
const button = document.createElement("button");

// 创建一个链接
const link = document.createElement("a");

新创建的元素只存在于内存中,还没有添加到页面上。在添加之前,可以设置它的各种属性:

javascript
const card = document.createElement("div");
card.className = "card";
card.id = "user-card";

const title = document.createElement("h2");
title.textContent = "John Doe";
title.classList.add("card-title");

const description = document.createElement("p");
description.textContent = "Software Engineer at TechCorp";

createTextNode

document.createTextNode() 用于创建纯文本节点:

javascript
const text = document.createTextNode("Hello, World!");

// 通常与元素结合使用
const paragraph = document.createElement("p");
paragraph.appendChild(text);

不过在大多数情况下,直接使用元素的 textContent 属性更简洁:

javascript
const paragraph = document.createElement("p");
paragraph.textContent = "Hello, World!";

textContent 的好处是它会自动创建文本节点,而且会对内容进行转义,防止 XSS 攻击。

createDocumentFragment

当需要一次性插入多个元素时,使用 DocumentFragment 可以显著提升性能:

javascript
// 创建一个文档片段
const fragment = document.createDocumentFragment();

// 向片段中添加多个元素
for (let i = 1; i <= 100; i++) {
  const item = document.createElement("li");
  item.textContent = `Item ${i}`;
  fragment.appendChild(item);
}

// 一次性插入所有元素
document.querySelector("ul").appendChild(fragment);

DocumentFragment 是一个"虚拟"的容器,它不属于实际的 DOM 树。在其中进行的操作不会触发页面重排,只有当整个片段被插入到 DOM 时,才会触发一次更新。

插入元素到 DOM

创建好的元素需要插入到文档中才能显示。有多种插入方法,适用于不同的位置需求。

appendChild

将元素添加为父元素的最后一个子节点:

javascript
const container = document.querySelector(".container");
const newElement = document.createElement("div");
newElement.textContent = "New content";

container.appendChild(newElement);

appendChild 返回被添加的节点,可以链式操作:

javascript
const list = document.querySelector("ul");
const item = document.createElement("li");
const text = document.createTextNode("New item");

list.appendChild(item).appendChild(text);

如果被添加的节点已经存在于文档中,它会被移动到新位置:

javascript
const firstItem = document.querySelector("#item-1");
const lastContainer = document.querySelector("#last-container");

// firstItem 会从原位置移除,添加到 lastContainer
lastContainer.appendChild(firstItem);

insertBefore

在指定的参考节点之前插入元素:

javascript
const parent = document.querySelector(".list");
const newItem = document.createElement("li");
newItem.textContent = "Inserted item";

const referenceNode = parent.children[2]; // 第三个子元素
parent.insertBefore(newItem, referenceNode);

如果参考节点是 null,效果等同于 appendChild

javascript
parent.insertBefore(newItem, null); // 添加到末尾

prepend 和 append

这两个现代方法更加灵活,可以同时插入多个节点或文本:

javascript
const container = document.querySelector(".container");

// append - 添加到末尾
container.append(
  "Some text", // 字符串会自动转为文本节点
  document.createElement("span"),
  document.createElement("div")
);

// prepend - 添加到开头
container.prepend(document.createElement("header"), "Header text");

appendChild 的区别:

  • append/prepend 可以接受多个参数
  • 可以直接插入字符串
  • 没有返回值

before 和 after

在元素自身的前面或后面插入:

javascript
const target = document.querySelector(".target");

// 在 target 前面插入
target.before(document.createElement("div"), "Before text");

// 在 target 后面插入
target.after("After text", document.createElement("span"));

replaceWith

用新节点替换现有节点:

javascript
const oldElement = document.querySelector(".old");
const newElement = document.createElement("div");
newElement.textContent = "I replaced the old element";

oldElement.replaceWith(newElement);

也可以同时替换为多个节点或文本:

javascript
oldElement.replaceWith(
  "Some text",
  document.createElement("span"),
  "More text"
);

插入位置对比

javascript
const target = document.querySelector(".target");

// 各方法的插入位置示意
/*
  parent.prepend(A)      → A 成为第一个子节点
  target.before(B)       → B 插入到 target 前面
  [target]               → 目标元素自身
  target.after(C)        → C 插入到 target 后面
  parent.append(D)       → D 成为最后一个子节点
*/

使用 insertAdjacentHTML 和相关方法

insertAdjacentHTML 提供了另一种插入内容的方式,可以直接解析 HTML 字符串:

javascript
const container = document.querySelector(".container");

container.insertAdjacentHTML("beforebegin", "<p>Before container</p>");
container.insertAdjacentHTML("afterbegin", "<p>First child</p>");
container.insertAdjacentHTML("beforeend", "<p>Last child</p>");
container.insertAdjacentHTML("afterend", "<p>After container</p>");

四个位置参数的含义:

html
<!-- beforebegin -->
<div class="container">
  <!-- afterbegin -->
  <p>Existing content</p>
  <!-- beforeend -->
</div>
<!-- afterend -->

类似的方法还有:

javascript
// insertAdjacentText - 插入纯文本(会转义 HTML)
element.insertAdjacentText("beforeend", '<script>alert("safe")</script>');
// 结果:显示文字 "<script>alert("safe")</script>"

// insertAdjacentElement - 插入元素节点
const newDiv = document.createElement("div");
element.insertAdjacentElement("afterend", newDiv);

innerHTML 的使用与风险

innerHTML 是设置或获取元素内部 HTML 的便捷属性:

javascript
const container = document.querySelector(".container");

// 设置 HTML 内容
container.innerHTML = "<h1>Title</h1><p>Paragraph</p>";

// 追加内容(会重新解析整个 innerHTML)
container.innerHTML += "<footer>Footer</footer>";

innerHTML 的问题:

  1. 性能问题:每次设置都会完全重新解析和重建 DOM
  2. 事件丢失:原有元素上的事件监听器会被清除
  3. 安全风险:如果包含用户输入,可能导致 XSS 攻击
javascript
// ❌ 危险:用户输入可能包含恶意脚本
const userInput = '<img src=x onerror="alert(document.cookie)">';
container.innerHTML = userInput; // XSS 漏洞!

// ✅ 安全:使用 textContent 会转义 HTML
container.textContent = userInput; // 显示原始文本

克隆元素

cloneNode 方法可以复制现有的元素:

javascript
const original = document.querySelector(".template");

// 浅克隆:只复制元素本身,不包括子元素
const shallowClone = original.cloneNode(false);

// 深克隆:复制元素及其所有后代
const deepClone = original.cloneNode(true);

// 克隆的元素不在文档中,需要插入
document.body.appendChild(deepClone);

克隆时需要注意:

  • 克隆会复制所有属性(包括 id,可能导致重复 ID)
  • 事件监听器不会被复制
  • 如果原元素有 id,建议修改克隆元素的 id
javascript
const template = document.querySelector("#card-template");
const clone = template.cloneNode(true);

// 修改 ID 避免重复
clone.id = "card-" + Date.now();

// 添加到页面
document.querySelector(".cards").appendChild(clone);

使用 template 元素

HTML5 的 <template> 元素专门用于定义可重用的 HTML 片段:

html
<template id="user-card-template">
  <article class="user-card">
    <img class="avatar" src="" alt="" />
    <h3 class="name"></h3>
    <p class="bio"></p>
    <button class="follow-btn">Follow</button>
  </article>
</template>
javascript
function createUserCard(user) {
  const template = document.querySelector("#user-card-template");

  // 克隆模板内容
  const clone = template.content.cloneNode(true);

  // 填充数据
  clone.querySelector(".avatar").src = user.avatar;
  clone.querySelector(".avatar").alt = user.name;
  clone.querySelector(".name").textContent = user.name;
  clone.querySelector(".bio").textContent = user.bio;

  // 添加事件
  clone.querySelector(".follow-btn").addEventListener("click", () => {
    followUser(user.id);
  });

  return clone;
}

// 使用
const container = document.querySelector(".user-list");
const users = [
  { id: 1, name: "John Doe", bio: "Developer", avatar: "/avatars/john.jpg" },
  { id: 2, name: "Jane Smith", bio: "Designer", avatar: "/avatars/jane.jpg" },
];

users.forEach((user) => {
  container.appendChild(createUserCard(user));
});

<template> 的优势:

  • 模板内容不会渲染到页面上
  • 不会执行脚本或加载资源(如图片)
  • 可以直接在 HTML 中编写,便于维护
  • 支持复杂的 HTML 结构

实际应用示例

动态列表管理

javascript
class TodoList {
  constructor(containerSelector) {
    this.container = document.querySelector(containerSelector);
    this.items = [];
  }

  addItem(text) {
    const id = Date.now();
    this.items.push({ id, text, completed: false });

    const li = document.createElement("li");
    li.dataset.id = id;
    li.className = "todo-item";

    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.addEventListener("change", () => this.toggleItem(id));

    const span = document.createElement("span");
    span.textContent = text;

    const deleteBtn = document.createElement("button");
    deleteBtn.textContent = "×";
    deleteBtn.className = "delete-btn";
    deleteBtn.addEventListener("click", () => this.removeItem(id));

    li.append(checkbox, span, deleteBtn);
    this.container.appendChild(li);
  }

  removeItem(id) {
    const index = this.items.findIndex((item) => item.id === id);
    if (index > -1) {
      this.items.splice(index, 1);
      const li = this.container.querySelector(`[data-id="${id}"]`);
      if (li) {
        li.remove();
      }
    }
  }

  toggleItem(id) {
    const item = this.items.find((item) => item.id === id);
    if (item) {
      item.completed = !item.completed;
      const li = this.container.querySelector(`[data-id="${id}"]`);
      if (li) {
        li.classList.toggle("completed", item.completed);
      }
    }
  }

  render() {
    // 使用 DocumentFragment 批量渲染
    const fragment = document.createDocumentFragment();

    this.items.forEach((item) => {
      const li = document.createElement("li");
      li.dataset.id = item.id;
      li.className = "todo-item" + (item.completed ? " completed" : "");

      li.innerHTML = `
        <input type="checkbox" ${item.completed ? "checked" : ""}>
        <span>${this.escapeHtml(item.text)}</span>
        <button class="delete-btn">×</button>
      `;

      fragment.appendChild(li);
    });

    this.container.innerHTML = "";
    this.container.appendChild(fragment);
    this.attachEventListeners();
  }

  escapeHtml(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
  }

  attachEventListeners() {
    // 使用事件委托
    this.container.addEventListener("change", (e) => {
      if (e.target.type === "checkbox") {
        const li = e.target.closest("li");
        if (li) {
          this.toggleItem(Number(li.dataset.id));
        }
      }
    });

    this.container.addEventListener("click", (e) => {
      if (e.target.matches(".delete-btn")) {
        const li = e.target.closest("li");
        if (li) {
          this.removeItem(Number(li.dataset.id));
        }
      }
    });
  }
}

// 使用
const todoList = new TodoList("#todo-container");
todoList.addItem("Learn JavaScript");
todoList.addItem("Build a project");

模态框生成器

javascript
function createModal({ title, content, onConfirm, onCancel }) {
  // 创建遮罩层
  const overlay = document.createElement("div");
  overlay.className = "modal-overlay";

  // 创建模态框
  const modal = document.createElement("div");
  modal.className = "modal";

  // 标题栏
  const header = document.createElement("div");
  header.className = "modal-header";

  const titleEl = document.createElement("h2");
  titleEl.textContent = title;

  const closeBtn = document.createElement("button");
  closeBtn.className = "modal-close";
  closeBtn.textContent = "×";
  closeBtn.setAttribute("aria-label", "Close");

  header.append(titleEl, closeBtn);

  // 内容区
  const body = document.createElement("div");
  body.className = "modal-body";

  if (typeof content === "string") {
    body.textContent = content;
  } else if (content instanceof Node) {
    body.appendChild(content);
  }

  // 底部按钮
  const footer = document.createElement("div");
  footer.className = "modal-footer";

  const cancelBtn = document.createElement("button");
  cancelBtn.className = "btn btn-secondary";
  cancelBtn.textContent = "Cancel";

  const confirmBtn = document.createElement("button");
  confirmBtn.className = "btn btn-primary";
  confirmBtn.textContent = "Confirm";

  footer.append(cancelBtn, confirmBtn);

  // 组装模态框
  modal.append(header, body, footer);
  overlay.appendChild(modal);

  // 关闭函数
  function closeModal() {
    overlay.remove();
  }

  // 绑定事件
  closeBtn.addEventListener("click", closeModal);
  overlay.addEventListener("click", (e) => {
    if (e.target === overlay) closeModal();
  });
  cancelBtn.addEventListener("click", () => {
    onCancel?.();
    closeModal();
  });
  confirmBtn.addEventListener("click", () => {
    onConfirm?.();
    closeModal();
  });

  // 添加到页面
  document.body.appendChild(overlay);

  // 聚焦到确认按钮
  confirmBtn.focus();

  return { close: closeModal };
}

// 使用示例
createModal({
  title: "Confirm Action",
  content: "Are you sure you want to delete this item?",
  onConfirm: () => console.log("Confirmed!"),
  onCancel: () => console.log("Cancelled"),
});

无限滚动加载

javascript
class InfiniteScroll {
  constructor(containerSelector, loadMore) {
    this.container = document.querySelector(containerSelector);
    this.loadMore = loadMore;
    this.page = 1;
    this.loading = false;
    this.hasMore = true;

    this.init();
  }

  init() {
    // 初始加载
    this.load();

    // 监听滚动
    window.addEventListener("scroll", () => {
      if (this.shouldLoadMore()) {
        this.load();
      }
    });
  }

  shouldLoadMore() {
    if (this.loading || !this.hasMore) return false;

    const containerBottom = this.container.getBoundingClientRect().bottom;
    const threshold = window.innerHeight * 1.5;

    return containerBottom <= threshold;
  }

  async load() {
    this.loading = true;
    this.showLoader();

    try {
      const items = await this.loadMore(this.page);

      if (items.length === 0) {
        this.hasMore = false;
        this.showEndMessage();
      } else {
        this.renderItems(items);
        this.page++;
      }
    } catch (error) {
      this.showError(error.message);
    } finally {
      this.loading = false;
      this.hideLoader();
    }
  }

  renderItems(items) {
    const fragment = document.createDocumentFragment();

    items.forEach((item) => {
      const article = document.createElement("article");
      article.className = "card";
      article.innerHTML = `
        <h3>${this.escapeHtml(item.title)}</h3>
        <p>${this.escapeHtml(item.description)}</p>
      `;
      fragment.appendChild(article);
    });

    this.container.appendChild(fragment);
  }

  showLoader() {
    let loader = this.container.querySelector(".loader");
    if (!loader) {
      loader = document.createElement("div");
      loader.className = "loader";
      loader.textContent = "Loading...";
      this.container.appendChild(loader);
    }
    loader.style.display = "block";
  }

  hideLoader() {
    const loader = this.container.querySelector(".loader");
    if (loader) {
      loader.style.display = "none";
    }
  }

  showEndMessage() {
    const message = document.createElement("p");
    message.className = "end-message";
    message.textContent = "No more items to load";
    this.container.appendChild(message);
  }

  showError(message) {
    const error = document.createElement("div");
    error.className = "error-message";
    error.textContent = `Error: ${message}`;
    this.container.appendChild(error);
  }

  escapeHtml(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
  }
}

// 使用示例
const infiniteScroll = new InfiniteScroll("#content", async (page) => {
  const response = await fetch(`/api/items?page=${page}`);
  return response.json();
});

性能优化建议

批量操作

javascript
// ❌ 不好:频繁触发重排
for (let i = 0; i < 1000; i++) {
  const li = document.createElement("li");
  li.textContent = `Item ${i}`;
  list.appendChild(li);
}

// ✅ 好:使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement("li");
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}
list.appendChild(fragment);

离线操作

javascript
// ✅ 先从 DOM 中移除,修改后再添加回去
const container = document.querySelector(".container");
const parent = container.parentNode;

parent.removeChild(container);

// 进行大量修改
for (let i = 0; i < 1000; i++) {
  container.appendChild(document.createElement("div"));
}

parent.appendChild(container);

虚拟列表

对于超长列表,考虑只渲染可见区域内的元素:

javascript
// 简化示例
class VirtualList {
  constructor(container, items, itemHeight) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;

    this.container.style.height = items.length * itemHeight + "px";
    this.container.style.position = "relative";

    window.addEventListener("scroll", () => this.render());
    this.render();
  }

  render() {
    const scrollTop = window.scrollY;
    const containerTop = this.container.offsetTop;
    const viewportHeight = window.innerHeight;

    const startIndex = Math.floor((scrollTop - containerTop) / this.itemHeight);
    const endIndex =
      startIndex + Math.ceil(viewportHeight / this.itemHeight) + 1;

    this.container.innerHTML = "";

    for (
      let i = Math.max(0, startIndex);
      i < Math.min(this.items.length, endIndex);
      i++
    ) {
      const div = document.createElement("div");
      div.style.position = "absolute";
      div.style.top = i * this.itemHeight + "px";
      div.style.height = this.itemHeight + "px";
      div.textContent = this.items[i];
      this.container.appendChild(div);
    }
  }
}

总结

动态创建 DOM 元素是现代 Web 开发的核心技能。选择合适的方法可以让代码更简洁、性能更好:

场景推荐方法
创建单个元素createElement + appendChild
创建多个元素DocumentFragment
添加到末尾append / appendChild
添加到开头prepend
指定位置插入before / after / insertBefore
从 HTML 字符串创建insertAdjacentHTML(注意安全)
基于模板创建<template> + cloneNode

最佳实践:

  1. 尽量批量操作,减少 DOM 更新次数
  2. 对用户输入使用 textContent 而非 innerHTML
  3. 利用事件委托减少事件监听器数量
  4. 对于大量数据考虑使用虚拟列表