前言
React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
假设我们要使用 React 组件渲染以下这段真实 DOM 节点。
Some text. <h2>A heading</h2> More text. <h2>Another heading</h2> Even more text.
要怎么做呢?很简单,谁都知道...
React.Fragment 是在 React 16.2 新增的新特性,旧版本并不支持。下面我们从几个方面,说明 Fragment 的好处。
正文
一、React 16.0 之前
在低于 React 16.0 的版本,类组件或函数组件有很多限制。
比如,它们必须返回 React 元素或 null
。其中 React 元素包括类似 <MyComponent />
等自定义组件、类似 <div />
等 DOM 节点元素。
正确示例:
function MyComponent() { // ✅ 合法,也可以是其他 HTML 元素 return <div>...</div> } function MyComponent() { // ✅ 合法,返回 React 组件 return <ChildComponent /> } function MyComponent() { // ✅ 合法,不渲染任何真实 DOM 节点 return null }
错误示例:
function MyComponent() { // ❌ 不能返回数组 return [1, 2, 3].map((item, index) => ( <div key={index}>{item}</div> )) // ✅ 但注意,下面这种包裹在 {} 内是合法的, // map 方法返回的数组,目测是除了子元素时,做了扁平化处理。 // return ( // <div> // {[1, 2, 3].map((item, index) => ( // <div key={index}>{item}</div> // ))} // </div> // ) } function MyComponent() { // ❌ 一定要有返回值,跟 return null 是两回事 return undefined }
类组件同理。当不正确使用时,将会报错:
Warning: MyComponent(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.
这种方案的缺点也是显而易见的,在组件的返回值上,总需要一层 <div>、<span> 或其他 DOM 节点包装起来。当 React 渲染成真实 DOM 时,这个包装节点总是会存在的。
很多时候,往往这个包装节点对我们的 UI 层是没有意义的,反而加深了 DOM 树的层次。但很无奈,谁让我们要用 React 呢,人家语法限制就那样...
二、React 16.0 起
除了原来的 React 元素和 null
之外,新增了几种类型:
其中布尔值和
null
什么都不渲染,字符串或数值类型会渲染为文本节点。
例如:
function MyComponent() { // ✅ 合法,支持数组了,需要添加 key 属性去避免警告, // 这种情况下,底层会默认嵌套一个 <Fragment> 包裹起来。 return [1, 2, 3].map((item, index) => ( <div key={index}>{item}</div> )) // 或者是 // return [ // <div key="1">1</div>, // <div key="2">2</div>, // <div key="3">3</div> // ] } function MyComponent() { // ✅ 合法,自 React 16.2 起支持 Fragment 语法,不用像上面一样需要 key 了 return ( <React.Fragment> <div>1</div> <div>2</div> <div>3</div> </React.Fragment> ) } function MyComponent() { // ✅ 合法,最终会渲染为文本节点(注意,不是 <span>some string...</span> 哦) return 'some string...' }
相比 React 15.x 及更早版本,这种方式实在是太棒了。除了支持更多类型,最重要的是不会增加额外的节点。
前面提到,React 15.x 里的 React 组件总是避免不了需要一层可能是“无谓”的节点节点进行包装,那么 React 16.0 的改进,可以解决如下场景:
问题示例:
function Table() { return ( <table> <tbody> <tr> <Columns /> </tr> </tbody> </table> ) } function Columns() { // 按照 React 15.x 的语法要求,Columns 组件的返回值, // 必须要用一个类似 div 元素等包装起来 return ( <div> <td>Hello</td> <td>World</td> </div> ) }
根据 W3C 的要求,一个合法的 <table>
,<tr>
的子元素必须是 <td>
。而 React 这种组件的写法直接破坏了 <table>
结构,最终也得不到我们的预期结果。
一个合法的 <table> 结构应该是这样的,
table > thead/tbody/tfoot > tr > td > div/other
。
如果按照 React 16.x 提供的新特性,可以轻松解决...
function Columns() { // React.Fragment 最终渲染为真实 DOM 并不会产生任何 DOM 节点, // 因此,不会破坏 <table> 的结构了。(数组形式也是可以的) return ( <React.Fragment> <td>Hello</td> <td>World</td> </React.Fragment> ) }
三、Fragment
自 React 16.2 起,开始支持 React.Fragment 语法。前面提到该特性是对数组形式的一种增强用法。
语法
它的语法非常简单,把它是 React 内置的一个 React 组件。
<React.Fragment> // One or more child elements </React.Fragment>
key
是唯一可以传递给 Fragment 的属性。将来可能会添加对其他属性的支持,例如事件处理程序。
class App extends React.Component { state = { items: [ { id: '`2`', name: '计算机', description: '用来计算的仪器...' }, { id: '2', name: '显示器', description: '以视觉方式显示信息的装置...' } ] } render() { return <Glossary items={this.state.items} ></Glossary> } } function Glossary(props) { return ( <dl> {props.items.map(item => ( // 没有 `key`,React 会发出一个关键警告 <React.Fragment key={item.id}> <dt>{item.name}</dt> <dd>{item.description}</dd> </React.Fragment> ))} </dl> ) }
也可以使用它的简写语法 <></>
,但这种写法不接受任意属性,包括 key
。
JSX 中的片段语法受到现有技术的启发,例如 E4X 中的
XMLList() <></>
构造函数。使用一对空标签是为了表示它不会向 DOM 添加实际元素的想法。
对比
回到文章开头的示例,要渲染这样一段真实 DOM 节点。
Some text. <h2>A heading</h2> More text. <h2>Another heading</h2> Even more text.
前面提到,可以有几种解决方案,各有利弊。
解决方法一
低于 React 16.0 版本,由于不支持 Fragment 和数组形式,唯一的方法是将它们包装在一个额外的元素中,通常是 div
或 span
。如下:
function MyComponent() { return ( <div> Some text. <h2>A heading</h2> More text. <h2>Another heading</h2> Even more text. </div> ) }
但上述这种方法有个缺点,在渲染成真实 DOM 的时候,会增加一个节点,比如上述的 <div />
。
解决方法二
自 React 16.0 起,支持数组形式。因此可以这么做:
function MyComponent() { return [ 'Some text.', <h2 key="heading-1">A heading</h2>, 'More text.', <h2 key="heading-2">Another heading</h2>, 'Even more text.' ] }
这种方式有点麻烦,我们对比一下 Fragment 形式。
解决方法三(推荐)
自 React 16.2 起,支持 React.Fragment 语法,因此我们可以这样使用。
function MyComponent() { return ( <React.Fragment> Some text. <h2>A heading</h2> More text. <h2>Another heading</h2> Even more text. </React.Fragment> ) }
仔细对比数组和 Fragment 形式,可以发现数组形式有以下缺点:
- 数组中的子项必须用逗号分隔。
- 数组中的 children 必须有一个 key 来防止 React 的 key 警告。
- 字符串必须用引号括起来。
以上这些限制 Fragment 统统都没有,我们就按正常的思维去编写 DOM 节点就好了。