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

相关推荐
gis开发之家11 小时前
《Vue3 从入门到大神29篇》单元测试与 E2E 测试 —— 保障 Vue3 项目的质量
前端·javascript·vue.js·单元测试·前端框架·vue3
鲲穹AI全能助理11 小时前
实用音频合成工具分享:灵境配音 使用场景与功能说明
ffmpeg·音视频
爱勇宝11 小时前
AI 时代,更稀缺的是「提出好问题」还是「判断好答案」?
前端·人工智能·后端
李伟_Li慢慢11 小时前
three.js 中的WebGPU 有几成熟了?
前端·three.js
阿酷tony11 小时前
纯HTML5播放器带倍速、带画质切换的播放器
前端·html·html5
ShiXZ21311 小时前
指令集-NPM 常用指令速查手册
前端·npm·node.js
এ慕ོ冬℘゜11 小时前
前端核心知识体系进阶:从安全机制、网络协议到原生 JS 实战全解析
前端·网络协议·安全
前端H11 小时前
Biome & Rolldown:Rust 工具链接管前端基建
开发语言·前端·rust
柒和远方11 小时前
从等待到流式:LLM 流式输出的原理、协议与工程实践
javascript·llm
forever_world11 小时前
# 💧 从前端视角,彻底搞懂 AI 流式输出 —— 从"接根水管"到 Vue 3 实战
前端·vue.js