Fragment:React 的 `<></>` 和原生的 DocumentFragment,同名不同命

React 的 <></> 叫 Fragment,原生的 document.createDocumentFragment() 也叫 Fragment。它们都"不生成 DOM 节点",也都说"一次性挂载"。那它们到底是不是一个东西?这篇文章来彻底分清楚。


一、先看两个使用场景

场景一:原生 JS 中的 DocumentFragment

js 复制代码
const data = ["任务一", "任务二", "任务三"];
const oList = document.querySelector('#list');

// 逐个插入 --- 每次 appendChild 都触发浏览器重排
for (const task of data) {
    const item = document.createElement('li');
    item.innerText = task;
    oList.appendChild(item);  // 3 次 = 3 次重排
}

换成 DocumentFragment:

js 复制代码
const fragment = document.createDocumentFragment();

for (const task of data) {
    const item = document.createElement('li');
    item.innerText = task;
    fragment.appendChild(item);  // 内存操作,不触发重排
}
oList.appendChild(fragment);     // 一次性挂载,只触发 1 次重排

场景二:React JSX 中的 Fragment

jsx 复制代码
function App() {
    return (
        <>
            <h1>Hello</h1>
            <p>World</p>
        </>
    );
}

两个场景里看到了相似的名字和相似的概念------"批量操作"、"不产生实体"。但它们是同一个东西吗?


二、原生 DocumentFragment:浏览器的批量操作工具

它解决什么问题

浏览器每次操作真实 DOM 都要付出代价------重新计算布局(Layout)、重新绘制(Paint)。逐个插入 3 个 <li>,浏览器重排 3 次。100 个就是 100 次。

document.createDocumentFragment() 的思路很简单:先在内存里攒着,攒完了再一次性丢到页面上。

scss 复制代码
逐个插入:                        批量插入:
appendChild(li1) → 重排①          fragment.appendChild(li1)  ← 内存操作
appendChild(li2) → 重排②          fragment.appendChild(li2)  ← 内存操作
appendChild(li3) → 重排③          fragment.appendChild(li3)  ← 内存操作
                                  oList.appendChild(fragment) → 重排①(就一次)

工作机制

document.createDocumentFragment() 创建一个内存中的临时容器。这个容器不在 DOM 树上,往里面添加子节点是纯粹的内存操作,浏览器完全不管。等到 appendChild(fragment) 时,所有子节点被一次性转移到真实 DOM 中,fragment 自己则消失。

DocumentFragment = 临时搬运工:货物在仓库码好,一趟搬完。


三、React Fragment(<></>):解决一个完全不同的问题

它解决什么问题

React 组件的 return 只能返回一个根元素:

jsx 复制代码
// ❌ JSX 要求单一根元素,这样写不合法
function App() {
    return (
        <p>当前计数{count}</p>
        <button onClick={addCount}>+3</button>
    );
}

通常做法是套一个 <div>

jsx 复制代码
function App() {
    return (
        <div>  {/* 这个 <div> 纯粹是为了满足 JSX 语法,没有任何语义意义 */}
            <p>当前计数{count}</p>
            <button onClick={addCount}>+3</button>
        </div>
    );
}

但这个 <div> 会带来一系列问题:

  • 破坏 CSS 布局 :如果父元素是 flex 容器,多一层 <div> 意味着 flex 的直接子元素变了,布局可能完全错乱
  • 破坏 HTML 语义<table> 里不能夹 <div><ul> 的直接子元素必须是 <li>
  • DOM 层级冗余 :每个无意义的 <div> 都是浏览器要维护的一个节点,多了就慢

Fragment 就是这里的最佳答案------一个"透明的括号",满足 JSX 语法要求,但不产生任何 DOM 节点。

React Fragment 的内部原理

你写的 <></> 在 React 内部被处理成一个特殊的 Symbol:

js 复制代码
// React 源码中 Fragment 就是这行
const Fragment = Symbol.for('react.fragment');

React 在将虚拟 DOM 渲染为真实 DOM 时,遇到这个 Symbol 就直接跳过,把它的 children 挂到父节点上:

xml 复制代码
渲染前(虚拟 DOM):              渲染后(真实 DOM):
<div>                            <div>
  <>                               <p>当前计数0</p>
    <p>当前计数0</p>      →         <button>+3</button>
    <button>+3</button>          </div>
  </>
</div>

Fragment 在中间彻底消失了。

两种写法

jsx 复制代码
import { Fragment } from 'react';

// 短语法 --- 日常使用
<>
    <A />
    <B />
</>

// 完整语法 --- 需要 key 的场景(如 map 中)
items.map(item => (
    <Fragment key={item.id}>
        <dt>{item.term}</dt>
        <dd>{item.description}</dd>
    </Fragment>
))

短语法和完整语法的唯一区别:短语法不能接受任何属性(key、ref 等)


四、核心对比

React Fragment(<></> DocumentFragment
属于谁 React 组件体系 浏览器原生 DOM API
怎么创建 <>...</><Fragment> document.createDocumentFragment()
解决什么问题 JSX 需要单一根节点,但不想多套 <div> 多次 DOM 操作导致多次重排,想批量处理
问题的维度 语义 / 布局 --- 保持 DOM 树干净 性能 --- 减少浏览器 Layout/Paint 次数
在哪工作 React 虚拟 DOM 层 浏览器真实 DOM 层
最终产生 DOM 吗 ❌ 不产生 ❌ 不产生(子节点转移后自己消失)
性能收益 微小(减少 DOM 节点数) 显著(避免多次重排)

它们唯一的共同点

都像"透明的容器"------自己在最终的 DOM 树中不留痕迹,子节点直接归父节点所有。

但原因完全不同:

  • DocumentFragment 消失 → 浏览器规范规定 appendChild(fragment) 只转移子节点,丢弃 fragment 自身
  • React Fragment 消失 → React 渲染时遇到 Symbol(react.fragment) 就直接跳过

五、一个关键认识

DocumentFragment 是浏览器提供的性能工具。React Fragment 是 React 提供的语义工具。它们名字一样,概念相似,但解决的是两个完全不同的问题。

你不需要在 React 里用 DocumentFragment------React 的虚拟 DOM diff 和批处理机制已经帮你做了批量更新的事。你也不需要指望 React Fragment 能带来多少性能提升------它真正的价值是让你的 DOM 结构保持干净、语义正确、布局不受干扰。

相关推荐
不好听6131 小时前
useState 完全解析:初始化、异步更新、以及为什么传函数能解决闭包问题
前端·react.js
神明不懂浪漫3 小时前
【第七章】Java中的常用类
java·开发语言·前端·经验分享·笔记
GuWenyue9 小时前
放弃云端API!一套React+WebGPU本地LLM方案,零数据上传、离线可用
前端·人工智能·前端框架
原则猫10 小时前
函数/变量提升
前端
小林ixn11 小时前
深入 React useState:从闭包陷阱到性能优化的完整指南
react.js·性能优化
独泪了无痕11 小时前
Vue3 Hooks使用实战解析
前端·vue.js
shawxlee12 小时前
vue3在public下封装config.js自定义配置动态数据,可在打包后直接修改,方便后端部署及后续维护
前端·javascript·经验分享·vue·团队开发·js·项目优化
用户0595401744613 小时前
把记忆存储的回归测试从手工换成 Playwright + GitHub Actions,线上缺陷降低 80%
前端·css
触底反弹13 小时前
🚀 从 DOM0 级到 React 合成事件:前端事件监听的 20 年演进史
前端·react.js·面试