pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML DOM创建与删除节点 - 详细教程与实战指南
教程简介
DOM节点的创建与删除是动态修改页面结构的核心操作。通过JavaScript,开发者可以在运行时动态创建新的元素和文本节点、将它们插入到DOM树中的指定位置、替换已有节点、克隆节点以及删除不再需要的节点。这些操作使得Web应用能够根据用户交互和数据变化实时更新页面内容。本教程将全面讲解DOM节点创建、插入、删除、替换和克隆的各种方法,以及DocumentFragment的使用和节点操作的性能优化策略。
核心概念
DOM节点操作方法一览
重排与重绘的影响
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>浏览器兼容性
节点操作方法兼容性
现代方法的兼容性写法
代码示例
// 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应用的基础,掌握各种节点操作方法及其性能特点对于编写高效代码至关重要:
createElement:创建元素节点,配合textContent设置文本
createTextNode:创建文本节点,自动转义HTML字符
appendChild/insertBefore:插入节点,注意已有节点会移动而非复制
removeChild/remove:移除节点,remove更简洁但IE不支持
replaceChild/replaceWith:替换节点,被替换的节点仍在内存中
cloneNode:克隆节点,深克隆包含子节点,注意id冲突和事件监听器
DocumentFragment:批量操作的利器,减少重排次数
template标签:HTML模板的标准方式,内容不渲染不执行
性能优化:使用Fragment、分批渲染、隐藏容器操作、事件委托
现代方法:append/prepend/before/after/remove/replaceWith更简洁
遵循"批量操作、减少重排、安全创建"的原则,能够编写出高性能、可维护的DOM操作代码。
常见问题
问题1:大量节点操作导致页面卡顿?
症状: 添加数千个节点时页面明显卡顿。 解决方案:
问题2:cloneNode后事件监听器丢失?
症状: 克隆的节点点击没有反应。 解决方案:
问题3:IE不支持template标签?
解决方案:
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别