跟着 MDN 学 HTML day_38:(DocumentFragment 文档片段接口详解)

一、什么是 DocumentFragment

DocumentFragment 是 DOM 接口中一个非常实用但常常被忽视的特性。它表示一个没有父对象的最小文档对象,可以将其理解为一个轻量版的 Document 对象。与标准的 document 对象类似,DocumentFragment 能够存储由节点组成的文档结构。

DocumentFragment 与常规 DOM 节点最大的区别在于,它不是真实 DOM 树的一部分。这意味着对 DocumentFragment 所做的任何修改都不会触发浏览器的重排和重绘,也不会对页面的性能产生任何负面影响。这一特性使得 DocumentFragment 成为批量 DOM 操作的理想工具。

javascript 复制代码
// 创建一个空的 DocumentFragment 对象
const fragment = new DocumentFragment();
console.log(fragment.nodeType); // 11
console.log(fragment.nodeName); // #document-fragment

// 也可以使用 createDocumentFragment 方法
const fragment2 = document.createDocumentFragment();
console.log(fragment2 instanceof DocumentFragment); // true

二、构造函数与创建方式

DocumentFragment 提供了标准的构造函数,可以直接通过 new 关键字来创建实例。此外,传统上也可以使用 document.createDocumentFragment 方法。两种方式都能创建出功能完全相同的 DocumentFragment 对象。

javascript 复制代码
// 方式一:使用构造函数
const fragmentFromConstructor = new DocumentFragment();

// 方式二:使用 createDocumentFragment 方法
const fragmentFromMethod = document.createDocumentFragment();

// 验证两种方式创建的对象是否相同
console.log(fragmentFromConstructor instanceof DocumentFragment); // true
console.log(fragmentFromMethod instanceof DocumentFragment); // true

// 向片段中添加元素
const div = document.createElement('div');
div.textContent = 'Hello World';
fragmentFromConstructor.appendChild(div);

console.log(fragmentFromConstructor.childNodes.length); // 1
console.log(fragmentFromConstructor.firstChild.textContent); // Hello World

三、属性详解

DocumentFragment 继承自 Node 接口,同时也拥有一些自身特有的属性。这些属性主要用于获取片段中的元素信息,包括子元素的数量、子元素集合以及首尾元素等。

javascript 复制代码
// 创建示例 DocumentFragment
const fragment = new DocumentFragment();

// 添加多个元素
for (let i = 0; i < 5; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i + 1}`;
  div.id = `item-${i + 1}`;
  fragment.appendChild(div);
}

// childElementCount - 获取子元素的数量
console.log(fragment.childElementCount); // 5

// children - 获取所有子元素的实时 HTMLCollection
const children = fragment.children;
console.log(children.length); // 5
console.log(children[0].textContent); // Item 1

// firstElementChild - 获取第一个子元素
const firstChild = fragment.firstElementChild;
console.log(firstChild.textContent); // Item 1
console.log(firstChild.id); // item-1

// lastElementChild - 获取最后一个子元素
const lastChild = fragment.lastElementChild;
console.log(lastChild.textContent); // Item 5
console.log(lastChild.id); // item-5

// 向片段中添加文本节点
const textNode = document.createTextNode('这是一段文本');
fragment.appendChild(textNode);

// childElementCount 只计算元素节点,不计算文本节点
console.log(fragment.childElementCount); // 仍然是 5
console.log(fragment.childNodes.length); // 6(5个div + 1个文本节点)

四、核心方法详解

DocumentFragment 提供了多个实用的方法,用于查询、添加和操作片段内的元素。这些方法与 Document 和 Element 接口中的方法非常相似,但作用范围仅限于片段内部。

javascript 复制代码
// 创建测试用的 DocumentFragment
const fragment = new DocumentFragment();

const items = ['苹果', '香蕉', '橙子', '葡萄', '西瓜'];
items.forEach((item, index) => {
  const div = document.createElement('div');
  div.textContent = item;
  div.className = 'fruit';
  div.setAttribute('data-id', index + 1);
  fragment.appendChild(div);
});

// querySelector - 查询第一个匹配的元素
const firstFruit = fragment.querySelector('.fruit');
console.log(firstFruit.textContent); // 苹果

// querySelectorAll - 查询所有匹配的元素
const allFruits = fragment.querySelectorAll('.fruit');
console.log(allFruits.length); // 5
allFruits.forEach((fruit, index) => {
  console.log(`${index}: ${fruit.textContent}`);
});

// getElementById - 通过 ID 获取元素
// 注意:需要先为元素设置 ID
const targetDiv = fragment.querySelector('.fruit');
targetDiv.id = 'special-fruit';
const foundElement = fragment.getElementById('special-fruit');
console.log(foundElement.textContent); // 苹果

五、append 和 prepend 方法

append 和 prepend 是 DocumentFragment 提供的两个便捷方法,用于向片段中添加内容。append 方法将内容添加到片段的末尾,而 prepend 方法则将内容添加到片段开头。这两个方法都可以接受多个参数,参数可以是 Node 节点也可以是字符串。

javascript 复制代码
// 创建空的 DocumentFragment
const fragment = new DocumentFragment();

// 使用 append 添加内容
fragment.append('第一段文本');
fragment.append(document.createTextNode('第二段文本'));
fragment.append(document.createElement('span'));

console.log(fragment.childNodes.length); // 3
console.log(fragment.childNodes[0].textContent); // 第一段文本

// 一次性添加多个内容
fragment.append('第三段文本', document.createElement('hr'), '第四段文本');
console.log(fragment.childNodes.length); // 6

// 使用 prepend 在开头添加内容
const newFragment = new DocumentFragment();
newFragment.append('原始内容');
newFragment.prepend('开头内容');
newFragment.prepend(document.createElement('strong'), '加粗内容');

console.log(newFragment.childNodes[0].nodeName); // STRONG
console.log(newFragment.childNodes[1].textContent); // 加粗内容
console.log(newFragment.childNodes[2].textContent); // 原始内容

六、实际应用场景与性能优化

DocumentFragment 最经典的用法是作为 DOM 操作的缓冲区。当需要向页面中批量添加大量元素时,如果逐个添加会触发多次重排重绘,严重影响性能。使用 DocumentFragment 可以将所有元素先添加到片段中,然后一次性插入到 DOM 中,这样只触发一次重排重绘。

javascript 复制代码
// 场景一:动态生成列表项
// 传统低效的方式
const list = document.getElementById('myList');
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `列表项 ${i}`;
  list.appendChild(li); // 触发 1000 次重排
}

// 使用 DocumentFragment 的高效方式
const list2 = document.getElementById('myList2');
const fragment = new DocumentFragment();

for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `列表项 ${i}`;
  fragment.appendChild(li); // 在内存中操作,不触发重排
}

list2.appendChild(fragment); // 只触发一次重排

// 场景二:批量创建复杂结构
const container = document.getElementById('container');
const cardsFragment = new DocumentFragment();

for (let i = 0; i < 100; i++) {
  const card = document.createElement('div');
  card.className = 'card';
  
  const title = document.createElement('h3');
  title.textContent = `卡片标题 ${i}`;
  
  const content = document.createElement('p');
  content.textContent = `这是卡片 ${i} 的内容描述信息`;
  
  card.appendChild(title);
  card.appendChild(content);
  cardsFragment.appendChild(card);
}

container.appendChild(cardsFragment);

七、与 Template 元素的配合使用

在 Web Components 开发中,template 元素与 DocumentFragment 有着密切的关系。每个 template 元素都有一个 content 属性,该属性返回一个 DocumentFragment 对象,包含了模板中的内容。这使得我们可以重复使用模板内容来创建多个 DOM 结构。

javascript 复制代码
// 创建 template 元素
const template = document.createElement('template');
template.innerHTML = `
  <div class="product-card">
    <h2 class="product-title"></h2>
    <p class="product-price"></p>
    <button class="buy-btn">购买</button>
  </div>
`;

// 获取 template 内容的 DocumentFragment
const templateContent = template.content;
console.log(templateContent instanceof DocumentFragment); // true

// 使用模板创建多个产品卡片
const products = [
  { name: '笔记本电脑', price: '5999元' },
  { name: '智能手机', price: '3999元' },
  { name: '无线耳机', price: '999元' }
];

const container2 = document.getElementById('products-container');
const fragment2 = new DocumentFragment();

products.forEach(product => {
  // 克隆模板内容
  const clone = templateContent.cloneNode(true);
  const card = clone.querySelector('.product-card');
  const title = clone.querySelector('.product-title');
  const price = clone.querySelector('.product-price');
  
  title.textContent = product.name;
  price.textContent = product.price;
  
  fragment2.appendChild(card);
});

container2.appendChild(fragment2);

八、插入后的行为特点

当 DocumentFragment 被插入到 DOM 树中时,其行为有一个重要的特点:插入的不是片段本身,而是片段的所有子节点。插入完成后,原本的 DocumentFragment 会变为空。理解这一特点对于正确使用 DocumentFragment 非常重要。

javascript 复制代码
// 演示插入后片段变空的行为
const fragment = new DocumentFragment();

// 添加三个 div
for (let i = 0; i < 3; i++) {
  const div = document.createElement('div');
  div.textContent = `div ${i + 1}`;
  fragment.appendChild(div);
}

console.log('插入前片段中的子节点数量:', fragment.childNodes.length); // 3

// 将片段插入到 body 中
document.body.appendChild(fragment);

console.log('插入后片段中的子节点数量:', fragment.childNodes.length); // 0

// 验证 div 已经被移入 DOM
const bodyDivs = document.querySelectorAll('body > div');
console.log('body 中的 div 数量:', bodyDivs.length); // 3

// 注意:此时 fragment 已经为空,可以继续复用
fragment.appendChild(document.createElement('p'));
fragment.firstChild.textContent = '这是新添加的内容';
console.log('复用后片段中的子节点数量:', fragment.childNodes.length); // 1

// 再次插入
document.body.appendChild(fragment);
console.log('片段再次变空:', fragment.childNodes.length); // 0

九、兼容性与注意事项

DocumentFragment 在现代浏览器中得到广泛支持,使用时需要注意一些细节问题。特别是当片段中包含复杂的事件监听器时,直接插入可能会丢失这些监听器。此外,对于框架开发或者复杂的前端应用,合理使用 DocumentFragment 可以带来明显的性能提升。

javascript 复制代码
// 注意事项一:事件监听器的处理
const fragment = new DocumentFragment();
const button = document.createElement('button');
button.textContent = '点击我';

// 添加事件监听器
let clickCount = 0;
button.addEventListener('click', () => {
  clickCount++;
  console.log(`按钮被点击了 ${clickCount} 次`);
});

fragment.appendChild(button);
document.body.appendChild(fragment);

// 按钮被插入后点击仍然可以触发事件
button.click(); // 按钮被点击了 1 次

// 注意事项二:插入后原 DOM 元素的引用仍然有效
const insertedButton = document.querySelector('button');
console.log(insertedButton === button); // true

// 注意事项三:不要在循环中反复操作真实 DOM
// 错误示例
const container = document.getElementById('container');
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  container.appendChild(div); // 错误:1000 次重排
}

// 正确示例
const correctFragment = new DocumentFragment();
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  correctFragment.appendChild(div);
}
container.appendChild(correctFragment); // 正确:1 次重排

DocumentFragment 作为 DOM 操作中重要的性能优化工具,掌握其使用方法对于编写高效的前端代码具有重要意义。在日常开发中,只要涉及批量 DOM 操作,都应该优先考虑使用 DocumentFragment 来减少重排重绘次数,提升页面性能。


想要解锁更多HTML 核心标签实战、前端零基础入门干货、开发避坑全指南吗?
持续关注,后续将更新CSS 布局实战、JavaScript 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

相关推荐
@大迁世界2 小时前
41.ShadCN 是什么?它如何和 Tailwind CSS 集成,从而更容易构建可访问且可自定义的 React 组件?
前端·javascript·css·react.js·前端框架
千叶风行2 小时前
Text-to-SQL 技术设计与注意事项
前端·人工智能·后端
软件开发技术深度爱好者2 小时前
HTML5+JavaScript读取DOCX 文档完整内容
前端·html5
幽络源小助理3 小时前
苹果CMS V10 MXPro V4.5模版下载, 自适应视频主题源码, 幽络源源码
前端·开源·源码·php源码
kyriewen3 小时前
坏了,黑客学会用AI写外挂了
前端·程序员·ai编程
xiangxiongfly9154 小时前
Vue3 根据角色权限动态加载路由
前端·javascript·vue.js·动态加载路由
达达尼昂4 小时前
Claude 多 Agent 系统:从零搭建一个 4 Agent 团队
前端·架构·ai编程
深度智能Ai4 小时前
云声配音(MelodyCloud Studio):AI驱动的全链路音视频创作平台
人工智能·音视频
且听风吟_xincell5 小时前
ArkTS 声明式 UI 的本质:状态映射
ui·harmonyos