写 React 时总多一个 div?试试<></> 这个神奇的空标签

86 阅读4分钟

写 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>
  );
}

image.png 但这样一来,渲染后的 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 元素,没有多余的容器,清爽多了。

image.png

这东西不只是省个 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:

  1. 渲染多个同级元素时,比如列表项、表格行、多段落文本
  2. 组件返回多个元素,但不想影响父组件的布局和样式
  3. 避免添加不必要的 div 导致 CSS 选择器层级出错
  4. 优化 DOM 结构,减少无意义的节点,提升渲染性能

不过也要注意,不是所有情况都非得用 Fragment。如果本身就需要一个容器来设置样式或绑定事件,那直接用 div、section 这些语义化标签反而更合适。比如:

// 这种情况就该用div,因为需要样式容器
function Card({ children }) {
  return (
    <div className="card-container">
      {children}
    </div>
  );
}

最后

Fragment 这东西,看着简单,实则体现了 React 的设计哲学:既满足语法要求,又尽量不干扰实际的 DOM 结构。刚开始可能会觉得 "不就是个标签吗",但用得多了就会发现,它能让代码更干净,DOM 结构更合理,甚至在某些场景下能避免难以排查的样式问题。