创建 DOM 元素:动态构建页面内容
为什么需要动态创建元素
静态 HTML 只能展示固定的内容。而现代网页往往需要根据用户操作、服务器数据或其他条件动态更新界面——添加新的列表项、插入提示消息、渲染从 API 获取的数据等等。这些都需要在 JavaScript 中创建新的 DOM 元素。
动态创建元素有几种主要方法,各有其适用场景。选择合适的方法不仅影响代码的可读性,还关系到页面的性能和安全性。
创建元素的基本方法
createElement
document.createElement() 是创建新元素的核心方法:
// 创建一个新的 div 元素
const div = document.createElement("div");
// 创建一个按钮
const button = document.createElement("button");
// 创建一个链接
const link = document.createElement("a");新创建的元素只存在于内存中,还没有添加到页面上。在添加之前,可以设置它的各种属性:
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() 用于创建纯文本节点:
const text = document.createTextNode("Hello, World!");
// 通常与元素结合使用
const paragraph = document.createElement("p");
paragraph.appendChild(text);不过在大多数情况下,直接使用元素的 textContent 属性更简洁:
const paragraph = document.createElement("p");
paragraph.textContent = "Hello, World!";textContent 的好处是它会自动创建文本节点,而且会对内容进行转义,防止 XSS 攻击。
createDocumentFragment
当需要一次性插入多个元素时,使用 DocumentFragment 可以显著提升性能:
// 创建一个文档片段
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
将元素添加为父元素的最后一个子节点:
const container = document.querySelector(".container");
const newElement = document.createElement("div");
newElement.textContent = "New content";
container.appendChild(newElement);appendChild 返回被添加的节点,可以链式操作:
const list = document.querySelector("ul");
const item = document.createElement("li");
const text = document.createTextNode("New item");
list.appendChild(item).appendChild(text);如果被添加的节点已经存在于文档中,它会被移动到新位置:
const firstItem = document.querySelector("#item-1");
const lastContainer = document.querySelector("#last-container");
// firstItem 会从原位置移除,添加到 lastContainer
lastContainer.appendChild(firstItem);insertBefore
在指定的参考节点之前插入元素:
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:
parent.insertBefore(newItem, null); // 添加到末尾prepend 和 append
这两个现代方法更加灵活,可以同时插入多个节点或文本:
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
在元素自身的前面或后面插入:
const target = document.querySelector(".target");
// 在 target 前面插入
target.before(document.createElement("div"), "Before text");
// 在 target 后面插入
target.after("After text", document.createElement("span"));replaceWith
用新节点替换现有节点:
const oldElement = document.querySelector(".old");
const newElement = document.createElement("div");
newElement.textContent = "I replaced the old element";
oldElement.replaceWith(newElement);也可以同时替换为多个节点或文本:
oldElement.replaceWith(
"Some text",
document.createElement("span"),
"More text"
);插入位置对比
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 字符串:
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>");四个位置参数的含义:
<!-- beforebegin -->
<div class="container">
<!-- afterbegin -->
<p>Existing content</p>
<!-- beforeend -->
</div>
<!-- afterend -->类似的方法还有:
// 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 的便捷属性:
const container = document.querySelector(".container");
// 设置 HTML 内容
container.innerHTML = "<h1>Title</h1><p>Paragraph</p>";
// 追加内容(会重新解析整个 innerHTML)
container.innerHTML += "<footer>Footer</footer>";innerHTML 的问题:
- 性能问题:每次设置都会完全重新解析和重建 DOM
- 事件丢失:原有元素上的事件监听器会被清除
- 安全风险:如果包含用户输入,可能导致 XSS 攻击
// ❌ 危险:用户输入可能包含恶意脚本
const userInput = '<img src=x onerror="alert(document.cookie)">';
container.innerHTML = userInput; // XSS 漏洞!
// ✅ 安全:使用 textContent 会转义 HTML
container.textContent = userInput; // 显示原始文本克隆元素
cloneNode 方法可以复制现有的元素:
const original = document.querySelector(".template");
// 浅克隆:只复制元素本身,不包括子元素
const shallowClone = original.cloneNode(false);
// 深克隆:复制元素及其所有后代
const deepClone = original.cloneNode(true);
// 克隆的元素不在文档中,需要插入
document.body.appendChild(deepClone);克隆时需要注意:
- 克隆会复制所有属性(包括
id,可能导致重复 ID) - 事件监听器不会被复制
- 如果原元素有
id,建议修改克隆元素的id
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 片段:
<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>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 结构
实际应用示例
动态列表管理
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");模态框生成器
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"),
});无限滚动加载
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();
});性能优化建议
批量操作
// ❌ 不好:频繁触发重排
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);离线操作
// ✅ 先从 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);虚拟列表
对于超长列表,考虑只渲染可见区域内的元素:
// 简化示例
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 |
最佳实践:
- 尽量批量操作,减少 DOM 更新次数
- 对用户输入使用
textContent而非innerHTML - 利用事件委托减少事件监听器数量
- 对于大量数据考虑使用虚拟列表