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

相关推荐
lizz22764 小时前
Babel插件手册
javascript
用户938515635074 小时前
从小米前端面试题,彻底搞懂闭包、作用域链与 useState 惰性初始化
前端·面试
赛博仓鼠5 小时前
MiniMax H3 实测:8GB显存跑2K视频+原生音频,开源视频生成的新卷王
开源·aigc·音视频
粥里有勺糖5 小时前
视野修炼第133期 | Native 回春了?
前端·github·agent
aichitang20245 小时前
前端小skill
前端·人工智能·算法·ai·前端框架
志尊宝6 小时前
Vue3 零基础每日笔记(040):Vite 创建 Vue3 + TS 项目——企业标配从这一篇开始
笔记·vue·html·前端开发·软件开发
byte轻骑兵6 小时前
【LE Audio】PBP精讲[4]: 公共广播通告的设计逻辑与数据交互流程
人工智能·音视频·le audio·低功耗蓝牙音频
hz567896 小时前
手术室视频示教系统解决方案:让临床教学突破空间限制
安全·音视频·实时音视频·信息与通信·智能硬件
前端·柱子6 小时前
q-floodfill白边锯齿?一招搞定抗锯齿边缘问题
前端·html·canva可画
葡萄城技术团队6 小时前
SpreadJS V19.2新特性揭秘:功能区键盘提示
前端