跟着 MDN 学 HTML day_43:(DocumentFragment 接口详解)

一、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 交互基础、全站导航开发等硬核内容,带你从新手快速进阶,轻松搞定前端开发!

相关推荐
节点云科1 小时前
谷歌 Gemini Omni 深度解析:原生视频模型的技术突破与行业影响
人工智能·音视频
Bigger1 小时前
mini-cc:用最小的代码,复刻一个“真正能干活”的 AI 编程智能体(并且把架构讲清楚)
前端·ai编程·claude
电子科技圈1 小时前
XMOS将亮相台北国际电脑展并演示其在边缘AI和创新音频与互联等领域内的新方案
人工智能·游戏·计算机视觉·视觉检测·音视频·语音识别·实时音视频
问心无愧05131 小时前
ctf show web 入门46
android·前端·笔记
ooseabiscuit1 小时前
PHP与C++:Web与系统编程的终极对决
前端·c++·php
SEO_juper1 小时前
外贸独立站流量翻倍后的转化优化
大数据·前端·seo·geo·外贸独立站·谷歌优化·2026
i学长的猫1 小时前
# Hermes + Web UI 本地 Docker 部署指南
前端·ui·docker
yanyu-yaya2 小时前
css篇之网格grid 学习
前端·css·学习
MandalaO_O2 小时前
Web 开发:计算机网络知识梳理
前端·网络·计算机网络