React 里的 <></> 是啥?为啥说它是优化 DOM 的小能手?

0 阅读4分钟

在写 React 组件时,你是不是也遇到过这种情况:JSX 代码明明逻辑清晰,却非得在外层套一个多余的div?否则就会报错 “JSX 相邻的元素必须被包裹在一个闭合标签中”。而<></>的出现,就像给这个问题开了一剂特效药。今天我们就来聊聊这个神秘的符号,它到底是什么?

先看个痛点:为什么非得套个 div?

在 React 中,JSX 语法有个严格规定:最外层必须有唯一的父元素。比如下面这段代码会报错:

function UserInfo() {
  return (
    <h1>用户信息</h1>
    <p>姓名:张三</p>
    <p>年龄:25</p>
  );
}

image.png JSX 最终会被编译成React.createElement函数调用,而多个相邻元素会被解析成函数的多个参数,这在 JavaScript 中是不允许的。因此,我们必须给它们套一个父元素,最常用的就是div

image.png

但这种写法会带来两个问题:

  1. 多余的 DOM 层级:这个div没有任何实际意义,只是为了满足语法要求,却给 DOM 树增加了不必要的层级;
  2. 性能浪费:浏览器渲染 DOM 时,会解析这个多余的div,增加内存占用和渲染时间,尤其在复杂组件中,多层嵌套的无意义div会显著影响性能;
  3. 样式污染风险:如果给这个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>
  );
}

image.png

这两种写法在功能上没有任何区别,最终都会被 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 的使用原则

  1. 优先使用<></> :当不需要key或其他属性时,用空标签语法更简洁;
  2. 需要key时用<Fragment> :在循环渲染或需要唯一标识的场景中使用;
  3. 避免无意义的 div:任何时候,当你想添加一个 “只是为了满足语法” 的 div 时,考虑用 Fragment 替代;
  4. 性能敏感场景考虑文档碎片:在大量 DOM 操作(如动态列表)时,结合原生文档碎片进一步优化。