写 React时 每次返回多个元素,都得用个 div 把它们包起来。有时候明明不需要这个 div,纯粹是为了满足 "JSX 必须有唯一父元素" 的规则,结果 DOM 结构里就多了一堆没用的 div,看着特别别扭。
直到某天看到其他人代码里写了 <></> 这种奇怪的标签,好奇问了才知道,这玩意儿叫 Fragment(碎片),是 React 专门用来解决 "多余父元素" 问题的。今天就来分享一下这个看似简单,实则藏着不少细节的小工具。
为啥会有这东西?
先说说没 Fragment 时的尴尬。比如写个列表项,每个项里有标题和内容:
// 错误写法:多个根元素
function ListItem({ item }) {
return (
<h3>{item.title}</h3>
<p>{item.desc}</p>
);
}
这时候 React 会直接报错,说 JSX 必须有一个根元素。没办法,只能套个 div:
// 无奈的解决办法:多加一个div
function ListItem({ item }) {
return (
<div>
<h3>{item.title}</h3>
<p>{item.desc}</p>
</div>
);
}
但这样一来,渲染后的 DOM 里就多了很多不必要的 div。如果是嵌套较深的组件,可能会出现
<div><div><div>...</div></div></div> 这种 "套娃" 结构,不仅看着乱,还可能影响 CSS 选择器(比如用后代选择器时多了一层层级),甚至影响布局(比如 flex 或 grid 布局里多出来的 div 可能打乱排列)。
Fragment 就是来解决这个问题的。它能像容器一样把多个元素包起来,但不会在 DOM 里生成实际的节点。上面的代码用 Fragment 改写后是这样:
// 正确写法:用Fragment包裹
function ListItem({ item }) {
return (
<Fragment>
<h3>{item.title}</h3>
<p>{item.desc}</p>
</Fragment>
);
}
而 <></> 就是 <Fragment> 的简写,用法完全一样,只是更简洁:
// 简写形式,更常用
function ListItem({ item }) {
return (
<>
<h3>{item.title}</h3>
<p>{item.desc}</p>
</>
);
}
渲染后你会发现,DOM 里只有 h3 和 p 元素,没有多余的容器,清爽多了。
这东西不只是省个 div 那么简单
其实这和原生 JavaScript 里的 DocumentFragment 有点像。以前用原生 JS 动态创建多个 DOM 节点时,为了减少页面回流重绘,我们会先把节点放到 DocumentFragment 里,最后一次性插入页面:
const container = document.getElementById('list')
const fragment = document.createDocumentFragment() // 原生文档碎片
items.forEach(item => {
const wrapper = document.createElement('div')
const title = document.createElement('h3')
const desc = document.createElement('p')
title.textContent = item.title
desc.textContent = item.content
wrapper.appendChild(title)
wrapper.appendChild(desc)
fragment.appendChild(wrapper) // 先添加到碎片,不触发渲染
})
container.appendChild(fragment) // 最后一次性插入,只触发一次渲染
React 的 Fragment 和这个思路很像,都是作为 "临时容器" 存在,但不会生成实际 DOM 节点。不过不同的是,DocumentFragment 主要优化渲染性能,而 React Fragment 更多是解决 JSX 语法限制和 DOM 结构冗余的问题。
比如渲染一个列表,每个项用 Fragment 包裹,还得加个 key:
function UserList({ users }) {
return (
<ul>
{users.map(user => (
// 这里必须用完整的Fragment,不能用简写
<Fragment key={user.id}>
<li>{user.name}</li>
<li>{user.email}</li>
</Fragment>
))}
</ul>
);
}
这时候就不能用 <></> 简写了,因为简写形式没法加 key 属性。而列表渲染时 key 又是必须的,用来帮助 React 识别元素变化,提升渲染性能。这才明白,<Fragment> 和 <></> 其实是一回事,只是简写形式不支持加属性罢了。
再深入一点看,Fragment 在 React 内部是怎么工作的?它在虚拟 DOM 里是一个特殊的节点类型,标记为 REACT_FRAGMENT_TYPE。当 React 处理虚拟 DOM 时,遇到这个类型的节点,会直接跳过它,只渲染它包含的子元素,不会像 div 那样生成实际的 DOM 节点。
这就意味着,用 Fragment 包裹元素,既满足了 JSX 的语法要求,又不会给 DOM 增加额外层级。对于那些对 DOM 结构敏感的场景(比如表格布局),这简直是救星。
比如写个表格,想在 tbody 里动态插入多行:
// 正确:用Fragment保持表格结构
function TableRows({ data }) {
return (
<>
{data.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.age}</td>
</tr>
))}
</>
);
}
// 组件使用
function MyTable({ data }) {
return (
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
</tr>
</thead>
<tbody>
<TableRows data={data} />
</tbody>
</table>
);
}
如果这里用 div 包裹,渲染后表格结构就乱了,浏览器解析时可能会把 div 放到 table 外面,导致布局错乱。而用 Fragment 就不会有这个问题,因为它根本不会生成实际节点, tbody 里直接就是 tr 元素,完美符合表格的 DOM 结构要求。
哪些时候该用它?
只要不需要额外的 DOM 节点,就用 <></> 代替 div 作为根元素。总结下来,这几种场景特别适合用 Fragment:
- 渲染多个同级元素时,比如列表项、表格行、多段落文本
- 组件返回多个元素,但不想影响父组件的布局和样式
- 避免添加不必要的 div 导致 CSS 选择器层级出错
- 优化 DOM 结构,减少无意义的节点,提升渲染性能
不过也要注意,不是所有情况都非得用 Fragment。如果本身就需要一个容器来设置样式或绑定事件,那直接用 div、section 这些语义化标签反而更合适。比如:
// 这种情况就该用div,因为需要样式容器
function Card({ children }) {
return (
<div className="card-container">
{children}
</div>
);
}
最后
Fragment 这东西,看着简单,实则体现了 React 的设计哲学:既满足语法要求,又尽量不干扰实际的 DOM 结构。刚开始可能会觉得 "不就是个标签吗",但用得多了就会发现,它能让代码更干净,DOM 结构更合理,甚至在某些场景下能避免难以排查的样式问题。