在写 React 组件时,你是不是也遇到过这种情况:JSX 代码明明逻辑清晰,却非得在外层套一个多余的div
?否则就会报错 “JSX 相邻的元素必须被包裹在一个闭合标签中”。而<></>
的出现,就像给这个问题开了一剂特效药。今天我们就来聊聊这个神秘的符号,它到底是什么?
先看个痛点:为什么非得套个 div?
在 React 中,JSX 语法有个严格规定:最外层必须有唯一的父元素。比如下面这段代码会报错:
function UserInfo() {
return (
<h1>用户信息</h1>
<p>姓名:张三</p>
<p>年龄:25</p>
);
}
JSX 最终会被编译成
React.createElement
函数调用,而多个相邻元素会被解析成函数的多个参数,这在 JavaScript 中是不允许的。因此,我们必须给它们套一个父元素,最常用的就是div
但这种写法会带来两个问题:
- 多余的 DOM 层级:这个
div
没有任何实际意义,只是为了满足语法要求,却给 DOM 树增加了不必要的层级; - 性能浪费:浏览器渲染 DOM 时,会解析这个多余的
div
,增加内存占用和渲染时间,尤其在复杂组件中,多层嵌套的无意义div
会显著影响性能; - 样式污染风险:如果给这个
div
添加了样式或类名,可能会意外影响子元素,或被其他样式污染。
<></> 登场:解决问题的优雅方案
<></>
就是为解决上述问题而生的,它还有一个正式的名字 ——Fragment(碎片) 。
(1)<></> 是什么?
<></>
是 React 中Fragment
组件的语法糖,两者完全等价:
// 写法1:使用语法糖<></>
function UserInfo() {
return (
<>
<h1>用户信息</h1>
<p>姓名:张三</p>
<p>年龄:25</p>
</>
);
}
// 写法2:使用完整的Fragment组件
import { Fragment } from 'react';
function UserInfo() {
return (
<Fragment>
<h1>用户信息</h1>
<p>姓名:张三</p>
<p>年龄:25</p>
</Fragment>
);
}
这两种写法在功能上没有任何区别,最终都会被 React 处理为 “不生成实际 DOM 元素的容器”。
(2)它解决了什么问题?
简单说,<></>
的核心作用是:在满足 JSX 语法要求(唯一父元素)的同时,不生成多余的 DOM 节点。
对比使用div
和<></>
的 DOM 结构:
- DOM 结构更简洁,减少了浏览器的解析和渲染负担;
- 避免了因多余
div
导致的样式冲突或布局问题; - 代码更优雅,不用再为了满足语法而添加无意义的元素。
Fragment 的 “隐藏技能”:带 key 的场景
<></>
虽然方便,但有一个限制:不能添加 key 属性。而在列表渲染等场景中,我们需要给每个列表项添加key
(React 要求列表项必须有唯一key
以优化重渲染),这时就需要使用完整的Fragment
组件:
import { Fragment } from 'react';
function ArticleList({ articles }) {
return (
<div>
{articles.map(article => (
// 列表项必须有key,此时不能用<></>,需用Fragment
<Fragment key={article.id}>
<h2>{article.title}</h2>
<p>{article.content}</p>
<hr />
</Fragment>
))}
</div>
);
}
为什么<></>
不能加key
?因为它是语法糖,设计初衷是简化最常见的 “无属性容器” 场景,而带key
的场景相对特殊,因此 React 保留了完整Fragment
组件的用法。
底层原理:JavaScript 的文档碎片(DocumentFragment)
React 的 Fragment 灵感来源于 JavaScript 原生的文档碎片(DocumentFragment) ,它是一种轻量级的 DOM 容器,不会被渲染到页面中,但可以临时存放 DOM 节点。
(1)原生文档碎片的用法
看一个原生 JS 的例子:假设你需要动态添加 100 个列表项到页面,如果直接操作 DOM,会触发 100 次重排重绘,性能很差。而使用文档碎片,可以批量操作,只触发一次重排:
// 原生JS:用文档碎片优化DOM操作
const list = document.getElementById('list');
const fragment = document.createDocumentFragment(); // 创建文档碎片
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // 先添加到文档碎片(不触发渲染)
}
list.appendChild(fragment); // 一次性添加到DOM(只触发一次重排)
关键点:
- 文档碎片是 “虚拟容器”,不会出现在真实 DOM 树中;
- 所有操作都在内存中完成,最后一次性添加到 DOM,大幅提升性能。
(2)React Fragment 与文档碎片的关系
React 的 Fragment 借鉴了文档碎片的思想:只组织元素,不生成额外 DOM。两者的核心区别是:
- 文档碎片是原生 JS API,用于优化 DOM 操作;
- React Fragment是 React 组件,用于解决 JSX 必须有根节点的问题。
总结:Fragment 的使用原则
- 优先使用
<></>
:当不需要key
或其他属性时,用空标签语法更简洁; - 需要
key
时用<Fragment>
:在循环渲染或需要唯一标识的场景中使用; - 避免无意义的 div:任何时候,当你想添加一个 “只是为了满足语法” 的 div 时,考虑用 Fragment 替代;
- 性能敏感场景考虑文档碎片:在大量 DOM 操作(如动态列表)时,结合原生文档碎片进一步优化。