一、DocumentFragment 接口概述
DocumentFragment 接口表示一个没有父对象的最小文档对象,它被作为一个轻量版的 Document 使用。与标准的 document 相比,最大的区别在于它不是真实 DOM 树的一部分,其变化不会触发 DOM 树的重新渲染,也不会对性能产生影响。这使得 DocumentFragment 成为批量操作 DOM 时的理想工具。
javascript
// 创建 DocumentFragment 的两种方式
// 方式一:使用构造函数
const fragment1 = new DocumentFragment();
console.log(fragment1); // #document-fragment
console.log(fragment1.nodeType); // 11 (Node.DOCUMENT_FRAGMENT_NODE)
// 方式二:使用 document.createDocumentFragment
const fragment2 = document.createDocumentFragment();
console.log(fragment2); // #document-fragment
// 两种方式创建的结果完全相同
console.log(fragment1 instanceof DocumentFragment); // true
console.log(fragment2 instanceof DocumentFragment); // true
// DocumentFragment 不是真实 DOM 的一部分
console.log(document.body.contains(fragment1)); // false
console.log(fragment1.parentNode); // null
// 可以像操作普通 DOM 一样操作 DocumentFragment
const div = document.createElement('div');
div.textContent = 'Hello Fragment';
fragment1.appendChild(div);
console.log(fragment1.childNodes.length); // 1
console.log(fragment1.firstChild.textContent); // "Hello Fragment"
二、DocumentFragment 的核心特性与性能优势
DocumentFragment 最大的特点是它在内存中操作,不会引起浏览器的重排和重绘。当需要向 DOM 中批量添加大量元素时,使用 DocumentFragment 可以显著提升性能,因为所有节点会在一次操作中完成插入,只触发一次渲染,而不是每次添加一个节点都触发一次。
javascript
// 性能对比示例:不使用 DocumentFragment
function addWithoutFragment() {
const container = document.getElementById('container1');
console.time('不使用片段');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `项目 ${i}`;
container.appendChild(li); // 每次添加都会触发重排
}
console.timeEnd('不使用片段');
}
// 使用 DocumentFragment 优化
function addWithFragment() {
const container = document.getElementById('container2');
const fragment = new DocumentFragment();
console.time('使用片段');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `项目 ${i}`;
fragment.appendChild(li); // 在内存中操作,不触发渲染
}
container.appendChild(fragment); // 一次性插入,只触发一次重排
console.timeEnd('使用片段');
}
// 实际测试代码
function runPerformanceTest() {
// 创建容器
const container1 = document.createElement('ul');
const container2 = document.createElement('ul');
container1.id = 'container1';
container2.id = 'container2';
document.body.appendChild(container1);
document.body.appendChild(container2);
addWithoutFragment();
addWithFragment();
}
// 演示插入后片段变为空
const demoFragment = new DocumentFragment();
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = '段落 1';
p2.textContent = '段落 2';
demoFragment.appendChild(p1);
demoFragment.appendChild(p2);
console.log('插入前片段子节点数:', demoFragment.childNodes.length); // 2
const target = document.createElement('div');
target.appendChild(demoFragment);
console.log('插入后片段子节点数:', demoFragment.childNodes.length); // 0
console.log('目标元素子节点数:', target.childNodes.length); // 2
三、DocumentFragment 构造函数
DocumentFragment 构造函数用于创建一个新的空 DocumentFragment 对象。创建后可以像操作普通文档一样向其添加节点,这些节点都在内存中操作,不会影响页面上现有的 DOM 结构。这个构造函数在所有现代浏览器中都得到广泛支持。
javascript
// DocumentFragment 构造函数的基本使用
const emptyFragment = new DocumentFragment();
console.log(emptyFragment.childNodes.length); // 0
// 添加各种类型的节点
const fragment = new DocumentFragment();
// 添加元素节点
const heading = document.createElement('h1');
heading.textContent = '标题';
fragment.appendChild(heading);
// 添加文本节点
const text = document.createTextNode('这是一段文本');
fragment.appendChild(text);
// 添加注释节点
const comment = document.createComment('这是注释');
fragment.appendChild(comment);
console.log(fragment.childNodes.length); // 3
console.log(fragment.childNodes[0].nodeName); // "H1"
console.log(fragment.childNodes[1].nodeName); // "#text"
console.log(fragment.childNodes[2].nodeName); // "#comment"
// 使用构造函数创建并立即使用的实用模式
function createFragmentFromHTML(htmlString) {
const fragment = new DocumentFragment();
const temp = document.createElement('div');
temp.innerHTML = htmlString;
while (temp.firstChild) {
fragment.appendChild(temp.firstChild);
}
return fragment;
}
const html = '<span>项目1</span><span>项目2</span><span>项目3</span>';
const fragmentFromHTML = createFragmentFromHTML(html);
console.log(fragmentFromHTML.childNodes.length); // 3
// 也可以先创建片段,然后返回给调用者使用
function buildComplexStructure() {
const fragment = new DocumentFragment();
const wrapper = document.createElement('div');
wrapper.className = 'wrapper';
for (let i = 0; i < 5; i++) {
const item = document.createElement('div');
item.className = 'item';
item.textContent = `项目 ${i + 1}`;
wrapper.appendChild(item);
}
fragment.appendChild(wrapper);
return fragment;
}
const complexFragment = buildComplexStructure();
document.body.appendChild(complexFragment);
四、与子元素相关的属性
DocumentFragment 提供了几个用于访问其子元素的只读属性。childElementCount 返回元素子节点的数量,children 返回一个实时的 HTMLCollection 包含所有元素子节点,firstElementChild 和 lastElementChild 分别返回第一个和最后一个元素子节点。
javascript
// childElementCount 和 children 属性的使用
const fragment = new DocumentFragment();
console.log(fragment.childElementCount); // 0
console.log(fragment.children.length); // 0
console.log(fragment.children); // HTMLCollection []
// 添加元素节点
const div1 = document.createElement('div');
const div2 = document.createElement('div');
const p = document.createElement('p');
const textNode = document.createTextNode('文本节点');
fragment.appendChild(div1);
fragment.appendChild(div2);
fragment.appendChild(p);
fragment.appendChild(textNode); // 文本节点不是元素节点
console.log(fragment.childElementCount); // 3 (只计数元素)
console.log(fragment.children.length); // 3
console.log(fragment.children[0]); // <div>
console.log(fragment.children[1]); // <div>
console.log(fragment.children[2]); // <p>
// firstElementChild 和 lastElementChild 属性
console.log(fragment.firstElementChild); // 第一个 div
console.log(fragment.lastElementChild); // p
fragment.firstElementChild.textContent = '第一个元素';
fragment.lastElementChild.textContent = '最后一个元素';
// 处理空片段的情况
const emptyFragment = new DocumentFragment();
console.log(emptyFragment.firstElementChild); // null
console.log(emptyFragment.lastElementChild); // null
// 实际应用:遍历所有子元素
function processFragmentElements(fragment, callback) {
for (let i = 0; i < fragment.children.length; i++) {
const element = fragment.children[i];
callback(element, i);
}
}
const dataFragment = new DocumentFragment();
['苹果', '香蕉', '橙子', '葡萄'].forEach(fruit => {
const li = document.createElement('li');
li.textContent = fruit;
dataFragment.appendChild(li);
});
processFragmentElements(dataFragment, (element, index) => {
console.log(`元素 ${index}: ${element.textContent}`);
element.classList.add('fruit-item');
});
console.log(dataFragment.children[0].classList.contains('fruit-item')); // true
五、querySelector 和 querySelectorAll 方法
DocumentFragment 支持使用 CSS 选择器查询其内部的元素节点。querySelector 返回第一个匹配的元素,querySelectorAll 返回所有匹配的元素组成的 NodeList。这些方法与 Document 和 Element 上的同名方法行为完全一致。
javascript
// querySelector 和 querySelectorAll 的基本使用
const fragment = new DocumentFragment();
// 构建一个复杂的结构
const header = document.createElement('header');
header.className = 'main-header';
const h1 = document.createElement('h1');
h1.textContent = '文档标题';
h1.id = 'title';
header.appendChild(h1);
const main = document.createElement('main');
main.className = 'content';
for (let i = 1; i <= 5; i++) {
const article = document.createElement('article');
article.className = 'post';
article.setAttribute('data-id', i);
article.innerHTML = `<h2>文章 ${i}</h2><p>内容描述 ${i}</p>`;
main.appendChild(article);
}
const footer = document.createElement('footer');
footer.className = 'main-footer';
footer.textContent = '页脚信息';
fragment.appendChild(header);
fragment.appendChild(main);
fragment.appendChild(footer);
// 使用 querySelector
const title = fragment.querySelector('#title');
console.log(title.textContent); // "文档标题"
const firstPost = fragment.querySelector('.post');
console.log(firstPost.querySelector('h2').textContent); // "文章 1"
// 使用 querySelectorAll
const allPosts = fragment.querySelectorAll('.post');
console.log(allPosts.length); // 5
const articlesWithDataId = fragment.querySelectorAll('[data-id]');
console.log(articlesWithDataId.length); // 5
// 复杂选择器
const specificPost = fragment.querySelector('.post[data-id="3"]');
console.log(specificPost.querySelector('p').textContent); // "内容描述 3"
// 链式查询
const headerH1 = fragment.querySelector('.main-header').querySelector('h1');
console.log(headerH1.textContent); // "文档标题"
// 实际应用:过滤和操作片段中的元素
function highlightElements(fragment, selector, className) {
const elements = fragment.querySelectorAll(selector);
elements.forEach(element => {
element.classList.add(className);
});
return elements.length;
}
// 高亮所有文章标题
const highlightedCount = highlightElements(fragment, '.post h2', 'highlight');
console.log(`高亮了 ${highlightedCount} 个元素`);
// 查找特定类型的元素
function findElementsByPattern(fragment, pattern) {
const allElements = fragment.querySelectorAll('*');
return Array.from(allElements).filter(element => {
return element.textContent.includes(pattern);
});
}
const elementsContaining = findElementsByPattern(fragment, '文章');
console.log(`包含"文章"的元素数量: ${elementsContaining.length}`);
六、getElementById 方法
DocumentFragment 的 getElementById 方法用于根据元素的 id 属性查找元素。与 Document 上的同名方法一样,它返回第一个匹配的元素,如果没有找到则返回 null。这个方法在处理具有唯一标识符的片段内容时非常方便。
javascript
// getElementById 的基本使用
const fragment = new DocumentFragment();
// 创建带有不同 id 的元素
const header = document.createElement('div');
header.id = 'page-header';
header.textContent = '页面头部';
const main = document.createElement('div');
main.id = 'main-content';
main.textContent = '主要内容区域';
const sidebar = document.createElement('aside');
sidebar.id = 'sidebar';
sidebar.textContent = '侧边栏';
const footer = document.createElement('div');
footer.id = 'page-footer';
footer.textContent = '页面底部';
fragment.appendChild(header);
fragment.appendChild(main);
fragment.appendChild(sidebar);
fragment.appendChild(footer);
// 通过 id 查找元素
const foundHeader = fragment.getElementById('page-header');
console.log(foundHeader.textContent); // "页面头部"
const foundMain = fragment.getElementById('main-content');
console.log(foundMain.textContent); // "主要内容区域"
// 查找不存在的 id
const nonExistent = fragment.getElementById('non-existent');
console.log(nonExistent); // null
// id 查找是区分大小写的
const elementWithId = document.createElement('span');
elementWithId.id = 'UpperCaseId';
fragment.appendChild(elementWithId);
console.log(fragment.getElementById('uppercaseid')); // null
console.log(fragment.getElementById('UpperCaseId')); // <span>
// 实际应用:在片段中快速定位和修改内容
function updateContentById(fragment, id, newContent) {
const element = fragment.getElementById(id);
if (element) {
element.textContent = newContent;
return true;
}
return false;
}
// 使用函数更新内容
updateContentById(fragment, 'page-header', '已更新的页面头部');
console.log(fragment.getElementById('page-header').textContent); // "已更新的页面头部"
// 构建可复用的组件片段
function createUserCardFragment(user) {
const fragment = new DocumentFragment();
const card = document.createElement('div');
card.className = 'user-card';
const nameElem = document.createElement('h3');
nameElem.id = `user-name-${user.id}`;
nameElem.textContent = user.name;
const emailElem = document.createElement('p');
emailElem.id = `user-email-${user.id}`;
emailElem.textContent = user.email;
card.appendChild(nameElem);
card.appendChild(emailElem);
fragment.appendChild(card);
// 返回片段和辅助函数
return {
fragment,
getNameElement: () => fragment.getElementById(`user-name-${user.id}`),
getEmailElement: () => fragment.getElementById(`user-email-${user.id}`)
};
}
const userData = { id: 1, name: '张三', email: 'zhangsan@example.com' };
const { fragment: userCard, getNameElement } = createUserCardFragment(userData);
console.log(getNameElement().textContent); // "张三"
七、append 和 prepend 方法
append 方法在 DocumentFragment 的最后一个子节点之后插入一组 Node 对象或字符串,而 prepend 方法在第一个子节点之前插入。字符串会自动转换为 Text 节点。这些方法提供了更灵活的方式来向片段中添加内容。
javascript
// append 方法的基本使用
const fragment = new DocumentFragment();
// 添加单个元素
const div = document.createElement('div');
div.textContent = 'DIV 元素';
fragment.append(div);
console.log(fragment.children.length); // 1
// 添加多个元素
const p1 = document.createElement('p');
p1.textContent = '段落 1';
const p2 = document.createElement('p');
p2.textContent = '段落 2';
fragment.append(p1, p2);
console.log(fragment.children.length); // 3
// 添加字符串(自动转为文本节点)
fragment.append('这是文本');
console.log(fragment.childNodes.length); // 4 (包含文本节点)
console.log(fragment.childNodes[3].nodeType); // 3 (TEXT_NODE)
// 混合添加元素和字符串
const newFragment = new DocumentFragment();
newFragment.append('开头文本 ', document.createElement('strong'), ' 结尾文本');
console.log(newFragment.childNodes.length); // 3
// prepend 方法的基本使用
const prependDemo = new DocumentFragment();
const last = document.createElement('div');
last.textContent = '最后一个';
prependDemo.append(last);
const first = document.createElement('div');
first.textContent = '第一个';
prependDemo.prepend(first);
console.log(prependDemo.firstChild.textContent); // "第一个"
console.log(prependDemo.lastChild.textContent); // "最后一个"
// append 和 prepend 的实际应用
function buildListFragment(items) {
const fragment = new DocumentFragment();
// 添加列表标题
fragment.append(document.createElement('h3'));
fragment.firstChild.textContent = '项目列表';
// 添加列表内容
const ul = document.createElement('ul');
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
ul.appendChild(li);
});
fragment.append(ul);
// 在开头添加描述
const description = document.createElement('p');
description.textContent = `共 ${items.length} 个项目`;
fragment.prepend(description);
return fragment;
}
const items = ['任务一', '任务二', '任务三', '任务四'];
const listFragment = buildListFragment(items);
console.log(listFragment.childNodes.length); // 3 (p, h3, ul)
// 链式调用示例
function createRichFragment() {
const fragment = new DocumentFragment();
fragment
.append(document.createElement('header'))
.append(document.createElement('main'))
.append(document.createElement('footer'));
return fragment;
}
// 注意:append 返回 undefined,不能链式调用
// 正确的批量添加方式
const correctFragment = new DocumentFragment();
correctFragment.append(
document.createElement('header'),
document.createElement('main'),
document.createElement('footer')
);
console.log(correctFragment.children.length); // 3
八、实际应用场景与综合示例
DocumentFragment 在实际开发中有多种应用场景,包括批量 DOM 操作、模板渲染、虚拟列表实现等。下面是一个综合性的示例,展示了 DocumentFragment 在复杂场景中的应用。
javascript
// 场景一:批量渲染大型数据列表
class LargeListRenderer {
constructor(container, data) {
this.container = container;
this.data = data;
}
render() {
const fragment = new DocumentFragment();
this.data.forEach((item, index) => {
const row = document.createElement('div');
row.className = 'list-row';
row.setAttribute('data-index', index);
row.innerHTML = `
<span class="index">${index + 1}</span>
<span class="name">${item.name}</span>
<span class="value">${item.value}</span>
`;
fragment.appendChild(row);
});
this.container.appendChild(fragment);
}
renderBatch(batchSize = 100) {
let fragment = new DocumentFragment();
let batchCount = 0;
for (let i = 0; i < this.data.length; i++) {
const row = this.createRow(this.data[i], i);
fragment.appendChild(row);
batchCount++;
if (batchCount === batchSize || i === this.data.length - 1) {
this.container.appendChild(fragment);
fragment = new DocumentFragment();
batchCount = 0;
}
}
}
createRow(item, index) {
const row = document.createElement('div');
row.className = 'list-row';
row.textContent = `${index + 1}: ${item.name} - ${item.value}`;
return row;
}
}
// 场景二:Web Components 中的 DocumentFragment
class CustomCardComponent extends HTMLElement {
constructor() {
super();
const template = document.getElementById('card-template');
const templateContent = template.content;
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(templateContent.cloneNode(true));
}
// 使用 DocumentFragment 更新内容
updateContent(data) {
const fragment = new DocumentFragment();
const title = document.createElement('h2');
title.textContent = data.title;
const content = document.createElement('p');
content.textContent = data.content;
fragment.append(title, content);
const container = this.shadowRoot.querySelector('.card-content');
container.innerHTML = '';
container.appendChild(fragment);
}
}
// 场景三:异步加载并渲染内容
async function lazyRenderAsync(container, fetchFunction, batchSize = 20) {
const data = await fetchFunction();
const fragment = new DocumentFragment();
let count = 0;
function renderBatch() {
const nextBatch = data.slice(count, count + batchSize);
const batchFragment = new DocumentFragment();
nextBatch.forEach(item => {
const element = document.createElement('div');
element.textContent = item;
batchFragment.appendChild(element);
});
fragment.appendChild(batchFragment);
count += batchSize;
if (count < data.length) {
requestAnimationFrame(renderBatch);
} else {
container.appendChild(fragment);
console.log('所有内容渲染完成');
}
}
renderBatch();
}
// 场景四:片段克隆与重用
function createButtonFragmentFactory() {
const masterFragment = new DocumentFragment();
const button = document.createElement('button');
button.className = 'btn';
button.textContent = '按钮';
masterFragment.appendChild(button);
return function createButton(text, className) {
const clone = masterFragment.cloneNode(true);
const btn = clone.firstChild;
btn.textContent = text;
if (className) {
btn.classList.add(className);
}
return clone;
};
}
const createButton = createButtonFragmentFactory();
const primaryButton = createButton('提交', 'btn-primary');
const dangerButton = createButton('删除', 'btn-danger');
console.log(primaryButton.firstChild.textContent); // "提交"
console.log(dangerButton.firstChild.classList.contains('btn-danger')); // true
通过以上内容的学习,我们全面掌握了 DocumentFragment 接口的核心概念、属性和方法。DocumentFragment 是优化 DOM 操作性能的重要工具,尤其适合批量操作和构建复杂 DOM 结构的场景。正确使用 DocumentFragment 可以显著提升页面渲染效率,为用户带来更流畅的体验。
想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!