React基础:描述UI

React 官方文档《描述 UI》整理:组件定义与导入导出、JSX 语法与大括号、Props 传递、条件渲染、列表与 key、保持组件纯粹、把 UI 视为树。

#React#前端

你的第一个组件

组件 是 React 的核心概念之一。它们是构建用户界面(UI)的基础,是你开始 React 之旅的最佳起点!

组件:UI 构成要素

在 Web 当中,HTML 允许我们使用其内置的标签集(如 <h1><li>)创建丰富的结构化文档:

html
<article>
  <h1>我的第一个组件</h1>
  <ol>
    <li>组件:UI 构成要素</li>
    <li>定义组件</li>
    <li>使用组件</li>
  </ol>
</article>

<article> 表示这篇文章,<h1> 表示文章的标题,<ol> 以有序列表的形式表示文章的(缩写的)目录。每一个侧边栏、头像、模态框、下拉框的背后是都是像这样的(结合了用于样式的 CSS 和用于交互的 JavaScript 的)标签——你在 Web 上看到的每一个 UI 模块。

React 允许你将标签、CSS 和 JavaScript 组合成自定义“组件”,即 应用程序中可复用的 UI 元素。上文中表示目录的代码可以改写成一个能够在每个页面中渲染的 <TableOfContents /> 组件。实际上,使用的依然是 <article><h1> 等相同的 HTML 标签。

随着项目的发展,你会发现很多布局可以通过复用已经完成的组件来实现,从而加快开发进程。上文中提到的目录可以通过 <TableOfContents /> 组件添加到任意的画面中!你也可以使用 React 开源社区分享的大量组件(例如 Chakra UIMaterial UI)来快速启动项目。

定义组件

一直以来,创建网页时,Web 开发人员会用标签描述内容,然后通过 JavaScript 来增加交互。React 最为重视交互性且使用了相同的处理方式:React 组件是一段可以 使用标签进行扩展 的 JavaScript 函数。如下所示:

js
export default function Profile() {
  return (
    <img
      src="https://react.dev/images/docs/scientists/MK3eW3Am.jpg"
      alt="Katherine Johnson"
    />
  )
}

以下是构建组件的方法:

第一步:导出组件

export default 前缀是一种 JavaScript 标准语法(非 React 的特性)。它允许你导出一个文件中的主要函数以便你以后可以从其他文件引入它。

第二步:定义函数

使用 function Profile() { } 定义名为 Profile 的 JavaScript 函数。

React 组件是常规的 JavaScript 函数,但 组件的名称必须以大写字母开头,否则它们将无法运行!

第三步:添加标签

这个组件返回一个带有 srcalt 属性的 <img /> 标签。<img /> 写得像 HTML,但实际上是 JavaScript!这种语法被称为 JSX,它允许你在 JavaScript 中嵌入标签。

返回语句可以全写在一行上,如下面组件中所示:

js
return <img src="https://react.dev/images/docs/scientists/MK3eW3As.jpg" alt="Katherine Johnson" />;

但是,如果你的标签和 return 关键字不在同一行,则必须把它包裹在一对括号中,如下所示:

jsx
return (
  <div>
    <img src="https://react.dev/images/docs/scientists/MK3eW3As.jpg" alt="Katherine Johnson" />
  </div>
);

没有括号包裹的话,任何在 return 下一行的代码都将被忽略!

使用组件

现在你已经定义了 Profile 组件,你可以在其他组件中使用它。例如,你可以导出一个内部使用了多个 Profile 组件的 Gallery 组件:

jsx
function Profile() {
  return (
    <img
      src="https://react.dev/images/docs/scientists/MK3eW3As.jpg"
      alt="Katherine Johnson"
    />
  );
}

export default function Gallery() {
  return (
    <section>
      <h1>了不起的科学家</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}

浏览器所看到的

注意下面两者的区别:

  • <section> 是小写的,所以 React 知道我们指的是 HTML 标签。
  • <Profile /> 以大写 P 开头,所以 React 知道我们想要使用名为 Profile 的组件。

然而 Profile 包含更多的 HTML:<img />。这是浏览器最后所看到的:

html
<section>
  <h1>了不起的科学家</h1>
  <img src="https://react.dev/images/docs/scientists/MK3eW3As.jpg" alt="Katherine Johnson" />
  <img src="https://react.dev/images/docs/scientists/MK3eW3As.jpg" alt="Katherine Johnson" />
  <img src="https://react.dev/images/docs/scientists/MK3eW3As.jpg" alt="Katherine Johnson" />
</section>

嵌套和组织组件

组件是常规的 JavaScript 函数,所以你可以将多个组件保存在同一份文件中。如果这个文件变得臃肿,你也可以随时将 Profile 移动到单独的文件中。

警告

组件可以渲染其他组件,但是 请不要嵌套他们的定义

jsx
export default function Gallery() {
  // 🔴 永远不要在组件中定义组件
  function Profile() {
    // ...
  }
  // ...

}

上面这段代码 非常慢,并且会导致 bug 产生。因此,你应该在顶层定义每个组件:

jsx
export default function Gallery() {
  // ...
}
// ✅ 在顶层声明组件
function Profile() {
  // ...
}

当子组件需要使用父组件的数据时,你需要通过 props 的形式进行传递,而不是嵌套定义。

备注

你的 React 应用程序从“根”组件开始。通常,它会在启动新项目时自动创建。例如,根组件定义在 src/App.js 中。如果使用 Next.js 框架,根组件定义在 pages/index.js 中。在这些示例中,一直有导出根组件。

组件的导入与导出

组件的神奇之处在于它们的可重用性:你可以创建一个由其他组件构成的组件。但当你嵌套了越来越多的组件时,则需要将它们拆分成不同的文件。这样可以使得查找文件更加容易,并且能在更多地方复用这些组件。

根组件文件

App.js

jsx
function Profile() {
  return (
    <img
      src="https://react.dev/images/docs/scientists/MK3eW3As.jpg"
      alt="Katherine Johnson"
    />
  );
}

export default function Gallery() {
  return (
    <section>
      <h1>了不起的科学家们</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}

在此示例中,所有组件目前都定义在 根组件 App.js 文件中。具体还需根据项目配置决定,有些根组件可能会声明在其他文件中。如果你使用的框架基于文件进行路由,如 Next.js,那你每个页面的根组件都会不一样。

导出和导入一个组件

如果将来需要在首页添加关于科学书籍的列表,亦或者需要将所有的资料信息移动到其他文件。这时将 Gallery 组件和 Profile 组件移出根组件文件会更加合理。这会使组件更加模块化,并且可在其他文件中复用。你可以根据以下三个步骤对组件进行拆分:

  1. 创建 一个新的 JS 文件来存放该组件。
  2. 导出 该文件中的函数组件(可以使用 默认导出具名导出
  3. 在需要使用该组件的文件中 导入(可以根据相应的导出方式使用 默认导入具名导入)。

这里将 Profile 组件和 Gallery 组件,从 App.js 文件中移动到了 Gallery.js 文件中。修改后,即可在 App.js 中导入 Gallery.js 中的 Gallery 组件:

App.js

js
import Gallery from './Gallery.js';

export default function App() {
  return (
    <Gallery />
  );
}

Gallery.js.js

jsx
function Profile() {
  return (
    <img
      src="https://react.dev/images/docs/scientists/QIrZWGIs.jpg"
      alt="Alan L. Hart"
    />
  );
}

export default function Gallery() {
  return (
    <section>
      <h1>了不起的科学家们</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}

该示例中需要注意的是,如何将组件拆分成两个文件:

  1. Gallery.js:
    • 定义了 Profile 组件,该组件仅在该文件内使用,没有被导出。
    • 使用 默认导出 的方式,将 Gallery 组件导出
  2. App.js:
    • 使用 默认导入 的方式,从 Gallery.js 中导入 Gallery 组件。
    • 使用 默认导出 的方式,将根组件 App 导出。

引入过程中,你可能会遇到一些文件并未添加 .js 文件后缀,如下所示:

jsx
import Gallery from './Gallery';

无论是 './Gallery.js' 还是 './Gallery',在 React 里都能正常使用,只是前者更符合 原生 ES 模块

重要

默认导出 vs 具名导出

这是 JavaScript 里两个主要用来导出值的方式:默认导出和具名导出。到目前为止,我们的示例中只用到了默认导出。但你可以在一个文件中,选择使用其中一种,或者两种都使用。一个文件里有且仅有一个 默认 导出,但是可以有任意多个 具名 导出。

image-20260531124145066

组件的导出方式决定了其导入方式。当你用默认导入的方式,导入具名导出的组件时,就会报错。如下表格可以帮你更好地理解它们:

语法导出语句导入语句
默认export default function Button() {}import Button from './Button.js';
具名export function Button() {}import { Button } from './Button.js';

当使用默认导入时,你可以在 import 语句后面进行任意命名。比如 import Banana from './Button.js',如此你能获得与默认导出一致的内容。相反,对于具名导入,导入和导出的名字必须一致。这也是称其为 具名 导入的原因!

从同一文件中导出和导入多个组件

如果你只想展示一个 Profile 组,而不展示整个图集。你也可以导出 Profile 组件。但 Gallery.js 中已包含 默认 导出,此时,你不能定义 两个 默认导出。但你可以将其在新文件中进行默认导出,或者将 Profile 进行 具名 导出。同一文件中,有且仅有一个默认导出,但可以有多个具名导出!

首先,用具名导出的方式,将 Profile 组件从 Gallery.js 导出(不使用 default 关键字):

js
export function Profile() {
  // ...
}

接着,用具名导入的方式,从 Gallery.js 文件中 导入 Profile 组件(用大括号):

js
import { Profile } from './Gallery.js';

最后,在 App 组件里 渲染 <Profile />

js
export default function App() {
  return <Profile />;
}

现在,Gallery.js 包含两个导出:一个是默认导出的 Gallery,另一个是具名导出的 ProfileApp.js 中均导入了这两个组件。尝试将 <Profile /> 改成 <Gallery />,回到示例中:

使用 JSX 书写标签语言

JSX 是 JavaScript 语法扩展,可以让你在 JavaScript 文件中书写类似 HTML 的标签。虽然还有其它方式可以编写组件,但大部分 React 开发者更喜欢 JSX 的简洁性,并且在大部分代码库中使用它。

JSX: 将标签引入 JavaScript

网页是构建在 HTML、CSS 和 JavaScript 之上的。多年以来,web 开发者都是将网页内容存放在 HTML 中,样式放在 CSS 中,而逻辑则放在 JavaScript 中 —— 通常是在不同的文件中!页面的内容通过标签语言描述并存放在 HTML 文件中,而逻辑则单独存放在 JavaScript 文件中。

image-20260531124554383

但随着 Web 的交互性越来越强,逻辑越来越决定页面中的内容。JavaScript 控制着 HTML 的内容!这也是为什么 在 React 中,渲染逻辑和标签共同存在于同一个地方——组件。

image-20260531124613893

将一个按钮的渲染逻辑和标签放在一起可以确保它们在每次编辑时都能保持互相同步。反之,彼此无关的细节是互相隔离的,例如按钮的标签和侧边栏的标签。这样我们在修改其中任意一个组件时会更安全。

每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。React 组件使用一种被称为 JSX 的语法扩展来描述这些标签。JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息。了解这些区别最好的方式就是将一些 HTML 标签转化为 JSX 标签。

JSX and React 是相互独立的 东西。但它们经常一起使用,但你 可以 单独使用它们中的任意一个,JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。

// 纯 React
function App() {
  return React.createElement(
    'div',
    { className: 'container' },
    React.createElement('h1', null, '标题'),
    React.createElement('p', null, '内容')
  );
}

为什么说它们独立:

  • JSX 只是语法糖,Babel 会把它编译成 createElement 调用。React 本身从不”看见”JSX,只接收编译后的函数调用。
  • JSX 可以配别的库用(比如 Preact 编译到 h(),Vue 也能用 JSX)。
  • React 也能完全不用 JSX(就像上面那样手写 createElement),只是没人愿意这么写,因为太啰嗦。

新版本(React 17+)的自动运行时还会编译成 _jsx() 而非 createElement,但原理一样:JSX 终归是被转译掉的语法,运行时跑的永远是普通 JS 函数调用。

将 HTML 转化为 JSX

假设你现在有一些HTML 标签:

html
<h1>海蒂·拉玛的待办事项</h1>
<img
  src="https://react.dev/images/docs/scientists/yXOvdOSs.jpg"
  alt="Hedy Lamarr"
  class="photo"
>
<ul>
    <li>发明一种新式交通信号灯
    <li>排练一个电影场景
    <li>改进频谱技术
</ul>

而现在想要把这些标签迁移到组件中:

js
export default function TodoList() {
  return (
    // ???
  )
}

如果直接复制到组件中,并不能正常工作:

image-20260531124720845

这是因为 JSX 语法更加严格并且相比 HTML 有更多的规则!

JSX 规则

1. 只能返回一个根元素

如果想要在一个组件中包含多个元素,需要用一个父标签把它们包裹起来

例如,你可以使用一个 <div> 标签:

jsx
<div>
  <h1>海蒂·拉玛的待办事项</h1>
  <img
    src="https://react.dev/images/docs/scientists/yXOvdOSs.jpg"
    alt="Hedy Lamarr"
    class="photo"
  >
  <ul>
    ...
  </ul>
</div>

如果你不想在标签中增加一个额外的 <div>,可以用 <></> 元素来代替:

jsx
<>
  <h1>海蒂·拉玛的待办事项</h1>
  <img
    src="https://react.dev/images/docs/scientists/yXOvdOSs.jpg"
    alt="Hedy Lamarr"
    class="photo"
  >
  <ul>
    ...
  </ul>
</>

这个空标签被称作 Fragment。React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。

2. 标签必须闭合

JSX 要求标签必须正确闭合。像 <img> 这样的自闭合标签必须书写成 <img />,而像 <li>oranges 这样只有开始标签的元素必须带有闭合标签,需要改为 <li>oranges</li>

3. 使用驼峰式命名法给所有大部分属性命名!

JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。在你自己的组件中,经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制。例如,变量名称不能包含 - 符号或者像 class 这样的保留字。

这就是为什么在 React 中,大部分 HTML 和 SVG 属性都用驼峰式命名法表示。例如,需要用 strokeWidth 代替 stroke-width。由于 class 是一个保留字,所以在 React 中需要用 className 来代替。

jsx
<img
  src="https://react.dev/images/docs/scientists/yXOvdOSs.jpg"
  alt="Hedy Lamarr"
  className="photo"
/>

你可以 在 React DOM 元素中找到所有对应的属性

高级提示:使用 JSX 转化器

将现有的 HTML 中的所有属性转化 JSX 的格式是很繁琐的。我们建议使用 转化器 将 HTML 和 SVG 标签转化为 JSX。这种转化器在实践中非常有用。但我们依然有必要去了解这种转化过程中发生了什么,这样你就可以编写自己的 JSX 了。

在 JSX 中通过大括号使用 JavaScript

JSX 允许你在 JavaScript 中编写类似 HTML 的标签,从而使渲染的逻辑和内容可以写在一起。有时候,你可能想要在标签中添加一些 JavaScript 逻辑或者引用动态的属性。这种情况下,你可以在 JSX 的大括号内来编写 JavaScript。

使用引号传递字符串

当你想把一个字符串属性传递给 JSX 时,把它放到单引号或双引号中:

App.js

jsx
export default function Avatar() {
  return (
    <img
      className="avatar"
      src="https://react.dev/images/docs/scientists/7vQD0fPs.jpg"
      alt="Gregorio Y. Zara"
    />
  );
}

这里的 "https://react.dev/images/docs/scientists/7vQD0fPs.jpg""Gregorio Y. Zara" 就是被作为字符串传递的。

但是如果你想要动态地指定 srcalt 的值呢?你可以 {} 替代 "" 以使用 JavaScript 变量

jsx
export default function Avatar() {
  const avatar = 'https://react.dev/images/docs/scientists/7vQD0fPs.jpg';
  const description = 'Gregorio Y. Zara';
  return (
    <img
      className="avatar"
      src={avatar}
      alt={description}
    />
  );
}

请注意 className="avatar"src={avatar} 之间的区别,className="avatar" 指定了一个就叫 "avatar" 的使图片在样式上变圆的 CSS 类名,而 src={avatar} 这种写法会去读取 JavaScript 中 avatar 这个变量的值。这是因为大括号可以使你直接在标签中使用 JavaScript!

使用大括号:一扇进入 JavaScript 世界的窗户

JSX 是一种编写 JavaScript 的特殊方式。这为在大括号 { } 中使用 JavaScript 带来了可能。下面的示例中声明了科学家的名字,name,然后在 <h1> 后的大括号内嵌入它:

jsx
export default function TodoList() {
  const name = 'Gregorio Y. Zara';
  return (
    <h1>{name}的待办事项列表</h1>
  );
}

试着将 name 的值从 'Gregorio Y. Zara' 更改成 'Hedy Lamarr'。看看这个 To Do List 的标题将如何变化?

大括号内的任何 JavaScript 表达式都能正常运行,包括像 formatDate() 这样的函数调用:

jsx
const today = new Date();

function formatDate(date) {
  return new Intl.DateTimeFormat(
    'zh-CN',
    { weekday: 'long' }
  ).format(date);
}

export default function TodoList() {
  return (
    <h1>To Do List for {formatDate(today)}</h1>
  );
}

可以在哪使用大括号

在 JSX 中,只能在以下两种场景中使用大括号:

  1. 用作 JSX 标签内的文本<h1>{name}'s To Do List</h1> 是有效的,但是 <{tag}>Gregorio Y. Zara's To Do List</{tag}> 无效。
  2. 用作紧跟在 = 符号后的 属性src={avatar} 会读取 avatar 变量,但是 src="{avatar}" 只会传一个字符串 {avatar}

使用 “双大括号”:JSX 中的 CSS 和 对象

除了字符串、数字和其它 JavaScript 表达式,你甚至可以在 JSX 中传递对象。对象也用大括号表示,例如 { name: "Hedy Lamarr", inventions: 5 }。因此,为了能在 JSX 中传递,你必须用另一对额外的大括号包裹对象:person={{ name: "Hedy Lamarr", inventions: 5 }}

你可能在 JSX 的内联 CSS 样式中就已经见过这种写法了。React 不要求你使用内联样式(使用 CSS 类就能满足大部分情况)。但是当你需要内联样式的时候,你可以给 style 属性传递一个对象:

jsx
export default function TodoList() {
  return (
    <ul style={{
      backgroundColor: 'black',
      color: 'pink'
    }}>
      <li>Improve the videophone</li>
      <li>Prepare aeronautics lectures</li>
      <li>Work on the alcohol-fuelled engine</li>
    </ul>
  );
}

所以当你下次在 JSX 中看到 {{}}时,就知道它只不过是包在大括号里的一个对象罢了!

重要

内联 style 属性 使用驼峰命名法编写。例如,HTML <ul style="background-color: black"> 在你的组件里应该写成 <ul style={{ backgroundColor: 'black' }}>

将 Props 传递给组件

React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。Props 可能会让你想起 HTML 属性,但你可以通过它们传递任何 JavaScript 值,包括对象、数组和函数。

熟悉的 props

Props 是你传递给 JSX 标签的信息。例如,classNamesrcaltwidthheight 便是一些可以传递给 <img> 的 props:

App.js

js
function Avatar() {
  return (
    <img
      className="avatar"
      src="https://react.dev/images/docs/scientists/1bX5QH6.jpg"
      alt="Lin Lanying"
      width={100}
      height={100}
    />
  );
}

export default function Profile() {
  return (
    <Avatar />
  );
}

你可以传递给 <img> 标签的 props 是预定义的。但是你可以将任何 props 传递给 你自己的 组件,例如 <Avatar> ,以便自定义它们。 就像这样!

向组件传递 props

在这段代码中, Profile 组件没有向它的子组件 Avatar 传递任何 props :

js
export default function Profile() {
  return (
    <Avatar />
  );
}

你可以分两步给 Avatar 一些 props。

步骤 1: 将 props 传递给子组件

首先,将一些 props 传递给 Avatar。例如,让我们传递两个 props:person(一个对象)和 size(一个数字):

js
export default function Profile() {
  return (
    <Avatar
      person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
      size={100}
    />
  );
}

现在,你可以在 Avatar 组件中读取这些 props 了。

步骤 2: 在子组件中读取 props

你可以通过在 function Avatar 之后直接列出它们的名字 person, size 来读取这些 props。这些 props 在 ({}) 之间,并由逗号分隔。这样,你可以在 Avatar 的代码中使用它们,就像使用变量一样。

js
function Avatar({ person, size }) {
  // 在这里 person 和 size 是可访问的
}

向使用 personsize props 渲染的 Avatar 添加一些逻辑,你就完成了。

你可以将 props 想象成可以调整的“旋钮”。它们的作用与函数的参数相同 —— 事实上,props 正是 组件的唯一参数! React 组件函数接受一个参数,一个 props 对象:

js
function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}

通常你不需要整个 props 对象,所以可以将它解构为单独的 props。

警告

在声明 props 时, 不要忘记 () 之间的一对花括号 {}

js
function Avatar({ person, size }) {
  // ...
}

这种语法被称为 “解构”,等价于于从函数参数中读取属性:

js
function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}

给 prop 指定一个默认值

如果你想在没有指定值的情况下给 prop 一个默认值,你可以通过在参数后面写 = 和默认值来进行解构:

js
function Avatar({ person, size = 100 }) {
  // ...
}

现在, 如果 <Avatar person={...} /> 渲染时没有 size prop, size 将被赋值为 100

默认值仅在缺少 size prop 或 size={undefined} 时生效。 但是如果你传递了 size={null}size={0},默认值将 被使用。

使用 JSX 展开语法传递 props

有时候,传递 props 会变得非常重复:

jsx
function Profile({ person, size, isSepia, thickBorder }) {
  return (
    <div className="card">
      <Avatar
        person={person}
        size={size}
        isSepia={isSepia}
        thickBorder={thickBorder}
      />
    </div>
  );
}

重复代码没有错(它可以更清晰)。但有时你可能会重视简洁。一些组件将它们所有的 props 转发给子组件,正如 Profile 转给 Avatar 那样。因为这些组件不直接使用他们本身的任何 props,所以使用更简洁的“展开”语法是有意义的:

jsx
function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}

这会将 Profile 的所有 props 转发到 Avatar,而不列出每个名字。

请克制地使用展开语法。 如果你在所有其他组件中都使用它,那就有问题了。 通常,它表示你应该拆分组件,并将子组件作为 JSX 传递。 接下来会详细介绍!

将 JSX 作为子组件通过Prop传递

嵌套浏览器内置标签是很常见的:

html
<div>
  <img />
</div>

有时你会希望以相同的方式嵌套自己的组件:

jsx
<Card>
  <Avatar />
</Card>

当你将内容嵌套在 JSX 标签中时,父组件将在名为 children 的 prop 中接收到该内容。例如,下面的 Card 组件将接收一个被设为 <Avatar />children prop 并将其包裹在 div 中渲染:

jsx
import Avatar from './Avatar.js';

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}

条件渲染

通常你的组件会需要根据不同的情况显示不同的内容。在 React 中,你可以通过使用 JavaScript 的 if 语句、&&? : 运算符来选择性地渲染 JSX。

条件返回 JSX

假设有一个 PackingList 组件,里面渲染多个 Item 组件,每个物品可标记为打包与否:

app.js

jsx
function Item({ name, isPacked }) {
  return <li className="item">{name}</li>;
}

export default function PackingList() {
  return (
    <section>
      <h1>Sally Ride 的行李清单</h1>
      <ul>
        <Item
          isPacked={true}
          name="宇航服"
        />
        <Item
          isPacked={true}
          name="带金箔的头盔"
        />
        <Item
          isPacked={false}
          name="Tam 的照片"
        />
      </ul>
    </section>
  );
}

需要注意的是,有些 Item 组件的 isPacked 属性是被设为 true 而不是 false。你可以在那些满足 isPacked={true} 条件的物品旁加上一个勾选符号(✅)。

你可以用 if/else 语句 去判断:

jsx
if (isPacked) {
  return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;

如果 isPacked 属性是 true,这段代码会返回一个不一样的 JSX。通过这样的改动,一些物品的名字后面会出现一个勾选符号:

jsx
function Item({ name, isPacked }) {
  if (isPacked) {
    return <li className="item">{name} ✅</li>;
  }
  return <li className="item">{name}</li>;
}

export default function PackingList() {
  return (
    <section>
      <h1>Sally Ride 的行李清单</h1>
      <ul>
        <Item
          isPacked={true}
          name="宇航服"
        />
        <Item
          isPacked={true}
          name="带金箔的头盔"
        />
        <Item
          isPacked={false}
          name="Tam 的照片"
        />
      </ul>
    </section>
  );
}

选择性地返回 null

在一些情况下,你不想有任何东西进行渲染。比如,你不想显示已经打包好的物品。但一个组件必须返回一些东西。这种情况下,你可以直接返回 null

jsx
function Item({ name, isPacked }) {
  if (isPacked) {
    return null;
  }
  return <li className="item">{name}</li>;
}

如果组件的 isPacked 属性为 true,那么它将只返回 null。否则,它将返回相应的 JSX 用来渲染。

实际上,在组件里返回 null 并不常见,因为这样会让想使用它的开发者感觉奇怪。通常情况下,你可以在父组件里选择是否要渲染该组件。

选择性地包含 JSX

在之前的例子里,你在组件内部控制哪些 JSX 树(如果有的话!)会返回。你可能已经发现了在渲染输出里会有一些重复的内容:

jsx
<li className="item">{name} ✅</li>

和下面的写法很像:

jsx
<li className="item">{name}</li>

两个条件分支都会返回 <li className="item">...</li>

jsx
if (isPacked) {
  return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;

虽然这些重复的内容没什么害处,但这样可能会导致你的代码更难维护。比如你想更改 className?你就需要修改两个地方!针对这种情况,你可以通过选择性地包含一小段 JSX 来让你的代码更加 DRY

三目运算符(? :

JavaScript 有一种紧凑型语法来实现条件判断表达式——条件运算符 又称“三目运算符”。

除了这样:

jsx
if (isPacked) {
  return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;

你还可以这样实现:

jsx
return (
  <li className="item">
    {isPacked ? name + ' ✅' : name}
  </li>
);

你可以认为,“如果 isPacked 为 true 时,则(?)渲染 name + ' ✅',否则(:)渲染 name。”

与运算符(&&

你会遇到的另一个常见的快捷表达式是 JavaScript 逻辑与(&&)运算符。在 React 组件里,通常用在当条件成立时,你想渲染一些 JSX,或者不做任何渲染。使用 &&,你也可以实现仅当 isPackedtrue 时,渲染勾选符号。

jsx
return (
  <li className="item">
    {name} {isPacked && '✅'}
  </li>
);

你可以认为,“当 isPacked 为真值时,则(&&)渲染勾选符号,否则,不渲染。”

当 JavaScript && 表达式 的左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但条件的结果是 false,则整个表达式会变成 false。在 JSX 里,React 会将 false 视为一个“空值”,就像 null 或者 undefined,这样 React 就不会在这里进行任何渲染。

警告

切勿将数字放在 && 左侧.

JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。

例如,一个常见的错误是 messageCount && <p>New messages</p>。其原本是想当 messageCount 为 0 的时候不进行渲染,但实际上却渲染了 0

为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>

选择性地将 JSX 赋值给变量

当这些快捷方式妨碍写普通代码时,可以考虑使用 if 语句和变量。因为你可以使用 let 进行重复赋值,所以一开始你可以将你想展示的(这里指的是物品的名字)作为默认值赋予给该变量。

jsx
let itemContent = name;

结合 if 语句,当 isPackedtrue 时,将 JSX 表达式的值重新赋值给 itemContent

jsx
if (isPacked) {
  itemContent = name + " ✅";
}

在 JSX 中通过大括号使用 JavaScript。将变量用大括号嵌入在返回的 JSX 树中,来嵌套计算好的表达式与 JSX:

jsx
<li className="item">
  {itemContent}
</li>

这种方式是最冗长的,但也是最灵活的。

渲染列表

你可能经常需要通过 JavaScript 的数组方法 来操作数组中的数据,从而将一个数据集渲染成多个相似的组件。在这篇文章中,你将学会如何在 React 中使用 filter() 筛选需要渲染的组件和使用 map() 把数组转换成组件数组。

从数组中渲染数据

这里我们有一个列表。

html
<ul>
  <li>凯瑟琳·约翰逊: 数学家</li>
  <li>马里奥·莫利纳: 化学家</li>
  <li>穆罕默德·阿卜杜勒·萨拉姆: 物理学家</li>
  <li>珀西·莱温·朱利亚: 化学家</li>
  <li>苏布拉马尼扬·钱德拉塞卡: 天体物理学家</li>
</ul>

可以看到,这些列表项之间唯一的区别就是其中的内容/数据。未来你可能会碰到很多类似的情况,在那些场景中,你想基于不同的数据渲染出相似的组件,比如评论列表或者个人资料的图库。在这样的场景下,可以把要用到的数据存入 JavaScript 对象或数组,然后用 map()filter() 这样的方法来渲染出一个组件列表。

这里给出一个由数组生成一系列列表项的简单示例:

  1. 首先,把数据 存储 到数组中:
js
const people = [
  '凯瑟琳·约翰逊: 数学家',
  '马里奥·莫利纳: 化学家',
  '穆罕默德·阿卜杜勒·萨拉姆: 物理学家',
  '珀西·莱温·朱利亚: 化学家',
  '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',
];
  1. 遍历 people 这个数组中的每一项,并获得一个新的 JSX 节点数组 listItems
jsx
const listItems = people.map(person => <li>{person}</li>);
  1. listItems<ul> 包裹起来,然后 返回 它:
jsx
return <ul>{listItems}</ul>;

来看看运行的结果:

jsx
const people = [
  '凯瑟琳·约翰逊: 数学家',
  '马里奥·莫利纳: 化学家',
  '穆罕默德·阿卜杜勒·萨拉姆: 物理学家',
  '珀西·莱温·朱利亚: 化学家',
  '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',
];

export default function List() {
  const listItems = people.map(person =>
    <li>{person}</li>
  );
  return <ul>{listItems}</ul>;
}

注意上面的沙盒可能会输出这样一个控制台错误:

image-20260531142641450

等会我们会学到怎么修复它。在此之前,我们先来看看如何把这个数组变得更加结构化。

对数组项进行过滤

让我们把 people 数组变得更加结构化一点。

js
const people = [{
  id: 0,
  name: '凯瑟琳·约翰逊',
  profession: '数学家',
}, {
  id: 1,
  name: '马里奥·莫利纳',
  profession: '化学家',
}, {
  id: 2,
  name: '穆罕默德·阿卜杜勒·萨拉姆',
  profession: '物理学家',
}, {
  id: 3,
  name: '珀西·莱温·朱利亚',
  profession: '化学家',
}, {
  id: 4,
  name: '苏布拉马尼扬·钱德拉塞卡',
  profession: '天体物理学家',
}];

现在,假设你只想在屏幕上显示职业是 化学家 的人。那么你可以使用 JavaScript 的 filter() 方法来返回满足条件的项。这个方法会让数组的子项经过 “过滤器”(一个返回值为 truefalse 的函数)的筛选,最终返回一个只包含满足条件的项的新数组。

既然你只想显示 profession 值是 化学家 的人,那么这里的 “过滤器” 函数应该长这样:(person) => person.profession === '化学家'。下面我们来看看该怎么把它们组合在一起:

首先,创建 一个用来存化学家们的新数组 chemists,这里用到 filter() 方法过滤 people 数组来得到所有的化学家,过滤的条件应该是 person.profession === '化学家'

jsx
const chemists = people.filter(person =>
  person.profession === '化学家'
);

接下来 用 map 方法遍历 chemists 数组:

jsx
const listItems = chemists.map(person =>
  <li>
     <img
       src={getImageUrl(person)}
       alt={person.name}
     />
     <p>
       <b>{person.name}:</b>
       {' ' + person.profession + ' '}
       因{person.accomplishment}而闻名世界
     </p>
  </li>
);

最后,返回 listItems

jsx
return <ul>{listItems}</ul>;

key 保持列表项的顺序

如果把上面任何一个沙盒示例在新标签页打开,你就会发现控制台有这样一个报错:

image-20260531142725190

这是因为你必须给数组中的每一项都指定一个 key——它可以是字符串或数字的形式,只要能唯一标识出各个数组项就行:

jsx
<li key={person.id}>...</li>

备注

直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值!

这些 key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要。一个合适的 key 可以帮助 React 推断发生了什么,从而得以正确地更新 DOM 树。

用作 key 的值应该在数据中提前就准备好,而不是在运行时才随手生成

备注

如果你想让每个列表项都输出多个 DOM 节点而非一个的话,该怎么做呢?

Fragment 语法的简写形式 <> </> 无法接受 key 值,所以你只能要么把生成的节点用一个 <div> 标签包裹起来,要么使用长一点但更明确的 <Fragment> 写法:

jsx
import { Fragment } from 'react';
// ...
const listItems = people.map(person =>
  <Fragment key={person.id}>
    <h1>{person.name}</h1>
    <p>{person.bio}</p>
  </Fragment>

);

这里的 Fragment 标签本身并不会出现在 DOM 上,这串代码最终会转换成 <h1><p><h1><p>…… 的列表。

如何设定 key

不同来源的数据往往对应不同的 key 值获取方式:

来自数据库的数据: 如果你的数据是从数据库中获取的,那你可以直接使用数据表中的主键,因为它们天然具有唯一性。

本地产生数据: 如果你数据的产生和保存都在本地(例如笔记软件里的笔记),那么你可以使用一个自增计数器,crypto.randomUUID() 或者一个类似 uuid 的库来生成 key。

key 需要满足的条件

key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。

key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key。

React 中为什么需要 key?

设想一下,假如你桌面上的文件都没有文件名,取而代之的是,你需要通过文件的位置顺序来区分它们———第一个文件,第二个文件,以此类推。也许你也不是不能接受这种方式,可是一旦你删除了其中的一个文件,这种组织方式就会变得混乱无比。原来的第二个文件可能会变成第一个文件,第三个文件会成为第二个文件……

React 里需要 key 和文件夹里的文件需要有文件名的道理是类似的。它们(key 和文件名)都让我们可以从众多的兄弟元素中唯一标识出某一项(JSX 节点或文件)。而一个精心选择的 key 值所能提供的信息远远不止于这个元素在数组中的位置。即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它。

警告

有一点需要注意,组件不会把 key 当作 props 的一部分。Key 的存在只对 React 本身起到提示作用。如果你的组件需要一个 ID,那么请把它作为一个单独的 prop 传给组件: <Profile key={id} userId={id} />

保持组件纯粹

部分 JavaScript 函数是 纯粹 的,这类函数通常被称为纯函数。纯函数仅执行计算操作,不做其他操作。你可以通过将组件按纯函数严格编写,以避免一些随着代码库的增长而出现的、令人困扰的 bug 以及不可预测的行为。但为了获得这些好处,你需要遵循一些规则。

纯函数:组件作为公式

在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:

  • 只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。
  • 输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。

举个你非常熟悉的纯函数示例:数学中的公式。

我们使用 JavaScript 的函数实现,看起来将会是这样:

js
function double(number) {
  return 2 * number;
}

上述例子中,double() 就是一个 纯函数。如果你传入 3 ,它将总是返回 6

React 便围绕着这个概念进行设计。React 假设你编写的所有组件都是纯函数。也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX

jsx
function Recipe({ drinkers }) {
  return (
    <ol>
      <li>Boil {drinkers} cups of water.</li>
      <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>
      <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Spiced Chai Recipe</h1>
      <h2>For two</h2>
      <Recipe drinkers={2} />
      <h2>For a gathering</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

当你给函数 Recipe 传入 drinkers={2} 参数时,它将返回包含 2 cups of water 的 JSX。永远如此。

而当你传入 drinkers={4} 时,它将返回包含 4 cups of water 的 JSX。永远如此。

就像数学公式一样。

副作用:(不符合)预期的后果

React 的渲染过程必须始终保持纯粹。组件只应返回自己的 JSX,而不应去修改任何在渲染前就已存在的对象或变量——那样会破坏组件的纯粹性!

以下是违反这一规则的组件示例:

jsx
let guest = 0;

function Cup() {
  // Bad:正在更改预先存在的变量!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

该组件正在读写其外部声明的 guest 变量。这意味着 多次调用这个组件会产生不同的 JSX!并且,如果 其他 组件读取 guest ,它们也会产生不同的 JSX,其结果取决于它们何时被渲染!这是无法预测的。

你可以 将 guest 作为 prop 传入 来修复此组件:

jsx
function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

现在你的组件就是纯粹的,因为它返回的 JSX 只依赖于 guest prop。

一般来说,你不应该期望你的组件以任何特定的顺序被渲染。调用 y = 5x 和 y = 2x 的先后顺序并不重要:这两个公式相互独立。同样地,每个组件也应该“独立思考”,而不是在渲染过程中试图与其他组件协调,或者依赖于其他组件。渲染过程就像是一场学校考试:每个组件都应该自己计算 JSX!

提示

尽管你可能还没使用过,但在 React 中,你可以在渲染时读取三种输入propsstatecontext。你应该始终将这些输入视为只读

当你想根据用户输入 更改 某些内容时,你应该setState,而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。

React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件

纯函数仅仅执行计算,因此调用它们两次不会改变任何东西 — 就像两次调用 double(2) 并不会改变返回值,两次求解 y = 2x 不会改变 y 的值一样。相同的输入,总是返回相同的输出。

严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用<React.StrictMode> 包裹根组件。一些框架会默认这样做。

哪些地方可能引发副作用

函数式编程在很大程度上依赖纯函数,但凡事总有例外——某些操作在特定情况下必须改变点什么。这正是编程的意义所在!这类改变包括更新屏幕、启动动画、修改数据等,它们被称为副作用。副作用是在渲染之外”顺带”发生的事情,与渲染过程本身无关。

在 React 中,副作用通常应放在事件处理函数里。事件处理函数是当你执行某些操作(比如点击按钮)时,React 才会运行的函数。尽管事件处理函数定义在组件内部,它们却不会在渲染期间运行——因此事件处理函数无需保持纯粹。

如果你绞尽脑汁,仍找不到合适的事件处理函数来安放某个副作用,你还可以在组件中调用 useEffect,把它附加到返回的 JSX 上。这会告诉 React 在渲染完成后再执行它。不过,这种做法应当是你最后的选择。

如果可以,请尽量只用渲染过程本身来表达你的逻辑。你会惊讶于这样做能带来多少好处!

渲染就只需要渲染,不要添加修改数据的逻辑(比如向服务器发请求导致下一次渲染结果不一样(浏览量),这个应该放置在事件处理函数里面最合适,迫不得已才加进组建的生命周期里)

将 UI 视为树

当 React 应用程序逐渐成形时,许多组件会出现嵌套。那么 React 是如何跟踪应用程序组件结构的?

React 以及许多其他 UI 库,将 UI 建模为树。将应用程序视为树对于理解组件之间的关系以及调试性能和状态管理等未来将会遇到的一些概念非常有用。

将 UI 视为树

树是项目和 UI 之间的关系模型,通常使用树结构来表示 UI。例如,浏览器使用树结构来建模 HTML(DOM)与CSS(CSSOM)。移动平台也使用树来表示其视图层次结构。

image-20260531143257273

与浏览器和移动平台一样,React 还使用树结构来管理和建模 React 应用程序中组件之间的关系。这些树是有用的工具,用于理解数据如何在 React 应用程序中流动以及如何优化呈现和应用程序大小。

渲染树

组件的一个主要特性是能够由其他组件组合而成。在 嵌套组件 中有父组件和子组件的概念,其中每个父组件本身可能是另一个组件的子组件。

当渲染 React 应用程序时,可以在一个称为渲染树的树中建模这种关系。

下面的 React 应用程序渲染了一些鼓舞人心的引语。

jsx
import FancyText from './FancyText';
import InspirationGenerator from './InspirationGenerator';
import Copyright from './Copyright';

export default function App() {
  return (
    <>
      <FancyText title text="Get Inspired App" />
      <InspirationGenerator>
        <Copyright year={2004} />
      </InspirationGenerator>
    </>
  );
}

image-20260531143324536

通过示例应用程序,可以构建上面的渲染树。

这棵树由节点组成,每个节点代表一个组件。例如,AppFancyTextCopyright 等都是我们树中的节点。

在 React 渲染树中,根节点是应用程序的 根组件。在这种情况下,根组件是 App,它是 React 渲染的第一个组件。树中的每个箭头从父组件指向子组件。

提示

也许会注意到在上面的渲染树中,没有提到每个组件渲染的 HTML 标签。这是因为渲染树仅由 React 组件 组成。

渲染树表示 React 应用程序的单个渲染过程。在条件渲染中,父组件可以根据传递的数据渲染不同的子组件。

我们可以更新应用程序以有条件地渲染励志语录或颜色。

jsx
import FancyText from './FancyText';
import InspirationGenerator from './InspirationGenerator';
import Copyright from './Copyright';

export default function App() {
  return (
    <>
      <FancyText title text="Get Inspired App" />
      <InspirationGenerator>
        <Copyright year={2004} />
      </InspirationGenerator>
    </>
  );
}

image-20260531143428016

在这个示例中,根据 inspiration.type 的值可能会渲染 <FancyText><Color>。每次渲染过程的渲染树可能都不同。

模块依赖树

在 React 应用程序中,可以使用树来建模的另一个关系是应用程序的模块依赖关系。当拆分组件和逻辑到不同的文件中时,就创建了JavaScript 模块,在这些模块中可以导出组件、函数或常量。

模块依赖树中的每个节点都是一个模块,每个分支代表该模块中的 import 语句。

以之前的 Inspirations 应用程序为例,可以构建一个模块依赖树,简称依赖树。

image-20260531143448577

树的根节点是根模块,也称为入口文件。它通常包含根组件的模块。

与同一应用程序的渲染树相比,存在相似的结构,但也有一些显著的差异:

  • 构成树的节点代表模块,而不是组件。
  • 非组件模块,如 inspirations.js,在这个树中也有所体现。渲染树仅封装组件。
  • Copyright.js 出现在 App.js 下,但在渲染树中,Copyright 作为 InspirationGenerator 的子组件出现。这是因为 InspirationGenerator 接受 JSX 作为 children props,因此它将 Copyright 作为子组件渲染,但不导入该模块。

依赖树对于确定运行 React 应用程序所需的模块非常有用。在为生产环境构建 React 应用程序时,通常会有一个构建步骤,该步骤将捆绑所有必要的 JavaScript 以供客户端使用。负责此操作的工具称为 bundler(捆绑器),并且 bundler 将使用依赖树来确定应包含哪些模块。

随着应用程序的增长,捆绑包大小通常也会增加。大型捆绑包大小对于客户端来说下载和运行成本高昂,并延迟 UI 绘制的时间。了解应用程序的依赖树可能有助于调试这些问题。

评论

评论加载中……