React新手进阶学习(一)
官方的学习文档地址 (用于构建 Web 和原生交互界面的库) : https://react.docschina.org/
安装
推荐 Next.js React 框架
Next.js 是一个全栈式的 React 框架。
它用途广泛,可以让你创建任意规模的 React 应用——可以是静态博客,也可以是复杂的动态应用。
要创建一个新的 Next.js 项目,请在你的终端运行(需要先安装npm install -g npx):
1 | npx create-next-app |
Readme (前言)
计数器
命令式编程(给他一个命令就执行一个命令): js, jquery
声明式编程: React, Vue
两者的区别是什么?
命令式编程和声明式编程是两种不同的编程范式。
- 命令式编程需要明确指定程序的每一个步骤和控制流程,关注具体的实现细节和算法。
- 声明式编程只需描述要实现的目标,不需要详细指定步骤和顺序,关注问题的描述和抽象。
- 在命令式编程中,处理更低级的细节,控制数据的状态和变化;在声明式编程中,关注问题的描述和抽象。
- 声明式编程更可读、可维护,代码更接近自然语言;命令式编程可能需要更多注释和解释。
- 声明式编程更具可复用性,可以定义通用规则和函数;命令式编程更依赖具体的状态和操作。
- 声明式编程更容易进行并行处理和优化,编译器或运行时系统可以更好地进行优化;命令式编程中的优化和并行化可能较具挑战性。
- 不同的编程语言和框架可以结合两种编程范式的特点,并根据需求和个人偏好进行选择。
第一个组件
组件 是 React 的核心概念之一。它们是构建用户界面(UI)的基础,是你开始 React 之旅的最佳起点!
摘要
你第一次体验 React!让我们先记住一些关键点,带着这些来进行深入学习。
在本章节中,你将学到:
什么是组件?
- React 允许你创建组件,应用程序为可复用 UI 元素。
组件在 React 应用中扮演的角色
- 在 React 应用程序中,每一个 UI 模块都是一个组件。
如何编写你的第一个 React 组件
- React 是常规的 JavaScript 函数,除了:
- 它们的名字总是以大写字母开头。
- 它们返回 JSX 标签。
- React 是常规的 JavaScript 函数,除了:
什么是组件?
React 组件是一段可以 使用标签进行扩展 的 JavaScript 函数。
示例:
App.js
1 | ## 创建一个App.js,拷贝下方的代码 |
解析说明
注意事项(非常重要)
React 组件是常规的 JavaScript 函数,但 组件的名称必须以大写字母开头,否则它们将无法运行!
导出组件
export default
前缀是一种 JavaScript 标准语法(非 React 的特性)。它允许你标签一个文件中的主要函数以便你以后可以从其他文件引入它。
定义函数
使用 function Profile() { }
定义名为 Profile
的 JavaScript 函数。
添加标签
这个组件返回一个带有 src
和 alt
属性的 <img />
标签。<img />
写得像 HTML,但实际上是 JavaScript!这种语法被称为 JSX,它允许你在 JavaScript 中嵌入使用标签。
返回语句可以全写在一行上,如下面组件中所示:
1 | return <img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" >; |
但是,如果你的标签和 return
关键字不在同一行,则必须把它包裹在一对括号中,没有括号包裹的话,任何在 return
下一行的代码都 将被忽略
如下所示:
1 | # 包裹必须用 <> </>或者 <div> </div> |
使用组件
现在你已经定义了 Profile
组件,你可以在其他组件中使用它。
例如,你可以导出一个内部使用了多个 Profile
组件的 Gallery
组件:
App.js
1 | function Profile() { |
解析上方的代码
注意下面两者的区别:
<section>
是小写的,所以 React 知道我们指的是 HTML 标签。<Profile />
以大写P
开头,所以 React 知道我们想要使用名为Profile
的组件。
然而 Profile
包含更多的 HTML:<img />
。这是浏览器最后所看到的:
1 | <section> |
嵌套和组织组件
组件是常规的 JavaScript 函数,所以你可以将多个组件保存在同一份文件中。当组件相对较小或彼此紧密相关时,这是一种省事的处理方式。如果这个文件变得臃肿,你也可以随时将
Profile
移动到单独的文件中。
因为 Profile
组件在 Gallery
组件中渲染——甚至好几次!——我们可以认为 Gallery
是一个 父组件,将每个 Profile
渲染为一个“孩子”。
这是 React 的神奇之处:你可以只定义组件一次,然后按需多处和多次使用。
注意事项
组件可以渲染其他组件,但是 请不要嵌套他们的定义,例如:
1 | export default function Gallery() { |
上面这段代码 非常慢,并且会导致 bug 产生。因此,你应该在顶层定义每个组件, 当子组件需要使用父组件的数据时,你需要 通过 props 的形式进行传递,而不是嵌套定义。:
1 | export default function Gallery() { |
组件的导入与导出
组件的神奇之处在于它们的可重用性:
- 你可以创建一个由其他组件构成的组件。
- 但当你嵌套了越来越多的组件时,则需要将它们拆分成不同的文件。
- 这样可以使得查找文件更加容易,并且能在更多地方复用这些组件。
摘要
在本章节中,你将学到:
- 何为根组件
- 如何导入和导出一个组件
- 默认导出 和 具名导出 区别是什么
- 何时和如何使用默认和具名导入导出
- 如何在一个文件里导出多个组件
根组件文件
在 你的第一个组件 中,你创建了一个 Profile
组件,并且渲染在 Gallery
组件里。
在下方的示例中,所有组件目前都定义在根组件 App.js
文件中,在 Create React App 中,你的应用应在 src/App.js
文件中定义。具体还需根据项目配置决定,有些根组件可能会声明在其他文件中。如果你使用的框架基于文件进行路由,如 Next.js,那你每个页面的根组件都会不一样。
1 | function Profile() { |
导出和导入一个组件
如果将来需要在首页添加关于科学书籍的列表,亦或者需要将所有的资料信息移动到其他文件。这时将 Gallery
组件和 Profile
组件移出根组件文件会更加合理。这会使组件更加模块化,并且可在其他文件中复用。你可以根据以下三个步骤对组件进行拆分:
- 创建 一个新的 JS 文件来存放该组件。
- 导出 该文件中的函数组件(可以使用 默认导出 或 具名导出)
- 在需要使用该组件的文件中 导入(可以根据相应的导出方式使用 默认导入 或 具体名称导入)。
这里将 Profile
组件和 Gallery
组件,从 App.js
文件中移动到了 Gallery.js
文件中 :
1 | ## 创建 Gallery.js , 复制以下代码 |
修改后,即可在 App.js
中导入 Gallery.js
中的 Gallery
组件:
1 | import Gallery from './Gallery.js'; |
注意事项
该示例中需要注意的是,如何将组件拆分成两个文件:
Gallery.js
:- 定义了
Profile
组件,该组件仅在该文件内使用,没有被导出。 - 使用 默认导出 的方式,将
Gallery
组件导出
- 定义了
App.js
:- 使用 默认导入 的方式,从
Gallery.js
中导入Gallery
组件。 - 使用 默认导出 的方式,将根组件
App
导出。
- 使用 默认导入 的方式,从
引入过程中,你可能会遇到一些文件并未添加 .js
文件后缀,如下所示:
1 | import Gallery from './Gallery'; |
无论是 './Gallery.js'
还是 './Gallery'
,在 React 里都能正常使用,只是前者更符合 原生 ES 模块。
默认导出 vs 具名导出
这是 JavaScript 里两个主要用来导出值的方式:默认导出和具名导出。到目前为止,我们的示例中只用到了默认导出。但你可以在一个文件中,选择使用其中一种,或者两种都使用。一个文件里有且仅有一个 *默认* 导出,但是可以有任意多个 *具名* 导出。
组件的导出方式决定了其导入方式。当你用默认导入的方式,导入具名导出的组件时,就会报错。如下表格可以帮你更好地理解它们:
语法 | 导出语句 | 导入语句 |
---|---|---|
默认 | export default function Button() {} |
import Button from './Button.js'; |
具名 | export function Button() {} |
import { Button } from './Button.js'; |
当使用默认导入时,你可以在 import
语句后面进行任意命名。比如 import Banana from './Button.js'
,如此你能获得与默认导出一致的内容。相反,对于具名导入,导入和导出的名字必须一致。这也是为什么称其为 具名 导入的原因!
通常,文件中仅包含一个组件时,人们会选择默认导出,而当文件中包含多个组件或某个值需要导出时,则会选择具名导出。 无论选择哪种方式,请记得给你的组件和相应的文件命名一个有意义的名字。我们不建议创建未命名的组件,比如 export default () => {}
,因为这样会使得调试变得异常困难。
从同一文件中导出和导入多个组件
如果你只想展示一个 Profile
组,而不展示整个图集。你也可以导出 Profile
组件。但 Gallery.js
中已包含 默认 导出,此时,你不能定义 两个 默认导出。但你可以将其在新文件中进行默认导出,或者将 Profile
进行 具名 导出。同一文件中,有且仅有一个默认导出,但可以有多个具名导出!
注意:
为了减少在默认导出和具名导出之间的混淆,一些团队会选择只使用一种风格(默认或者具名),或者禁止在单个文件内混合使用。这因人而异,选择最适合你的即可!
首先,用具名导出的方式,将 Profile
组件从 Gallery.js
导出(不使用 default
关键字):
1 | export function Profile() { |
接着,用具名导入的方式,从 Gallery.js
文件中 导入 Profile
组件(用大括号):
1 | import { Profile } from './Gallery.js'; |
最后,在 App
组件里 渲染 <Profile />
:
1 | export default function App() { |
现在,Gallery.js
包含两个导出:一个是默认导出的 Gallery
,另一个是具名导出的 Profile
。App.js
中均导入了这两个组件。尝试将 <Profile />
改成 <Gallery />
,回到示例中:
APP.js
1 | import Gallery from './Gallery.js'; |
Gallery.js
1 | export function Profile() { |
示例中混合使用了默认导出和具名导出:
Gallery.js
:- 使用 具名导出 的方式,将
Profile
组件导出,并取名为Profile
。 - 使用 默认导出 的方式,将
Gallery
组件导出。
- 使用 具名导出 的方式,将
App.js
:- 使用 具名导入 的方式,从
Gallery.js
中导入Profile
组件,并取名为Profile
。 - 使用 默认导入 的方式,从
Gallery.js
中导入Gallery
组件。 - 使用 默认导出 的方式,将根组件
App
导出。
- 使用 具名导入 的方式,从
使用 JSX 书写标签语言
JSX 是 JavaScript 语法扩展,可以让你在 JavaScript 文件中书写类似 HTML 的标签。虽然还有其它方式可以编写组件,但大部分 React 开发者更喜欢 JSX 的简洁性,并且在大部分代码库中使用它。
摘要
你将会学习到:
为什么 React 将标签和渲染逻辑耦合在一起?
- 由于渲染逻辑和标签是紧密相关的,所以 React 将它们存放在一个组件中。
JSX 与 HTML 有什么区别?
- JSX 类似 HTML,不过有一些区别。如果需要的话可以使用 转化器 将 HTML 转化为 JSX。
如何通过 JSX 展示信息
- 错误提示通常会指引你将标签修改为正确的格式。
JSX: 将标签引入 JavaScript
网页是构建在 HTML、CSS 和 JavaScript 之上的。多年以来,web 开发者都是将网页内容存放在 HTML 中,样式放在 CSS 中,而逻辑则放在 JavaScript 中 —— 通常是在不同的文件中!页面的内容通过标签语言描述并存放在 HTML 文件中,而逻辑则单独存放在 JavaScript 文件中。
但随着 Web 的交互性越来越强,逻辑越来越决定页面中的内容。JavaScript 负责 HTML 的内容!这也是为什么 在 React 中,渲染逻辑和标签共同存在于同一个地方——组件。
将一个按钮的渲染逻辑和标签放在一起可以确保它们在每次编辑时都能保持互相同步。反之,彼此无关的细节是互相隔离的,例如按钮的标签和侧边栏的标签。这样我们在修改其中任意一个组件时会更安全。
每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。React 组件使用一种被称为 JSX 的语法扩展来描述这些标签。JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息。了解这些区别最好的方式就是将一些 HTML 标签转化为 JSX 标签。
注意:
JSX and React 是相互独立的 东西。但它们经常一起使用,但你 可以 单独使用它们中的任意一个,JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。
将 HTML 转化为 JSX
假设你现在有一些(完全有效的)HTML 标签:
1 | <h1>海蒂·拉玛的代办事项</h1> |
而现在想要把这些标签迁移到组件中:
1 | export default function TodoList() { |
如果直接复制到组件中,并不能正常工作:
App.js
1 | export default function TodoList() { |
这是因为 JSX 语法更加严格并且相比 HTML 有更多的规则!
注意
大部分情况下,React 在屏幕上显示的错误提示就能帮你找到问题所在,如果在编写过程中遇到问题就参考一下提示吧。
JSX 规则
只能返回一个根元素
如果想要在一个组件中包含多个元素,需要用一个父标签把它们包裹起来。
例如,你可以使用一个 div
标签 :
1 | <div> |
如果你不想在标签中增加一个额外的 div 标签,可以用 <> 和 </> 元素来代替:
1 | <> |
这个空标签被称作 Fragment. React Fragment 允许你将子元素分组,而不会在 HTML 结构中添加额外节点。
为什么多个 JSX 标签需要被一个父元素包裹?
JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。
标签必须闭合
JSX 要求标签必须正确闭合。像 <img>
这样的自闭合标签必须书写成 <img />
,而像 <li>oranges
这样只有开始标签的元素必须带有闭合标签,需要改为 <li>oranges</li>
。
海蒂·拉玛的照片和代办事项的标签经修改后变为:
1 | <> |
使用驼峰式命名法给 所有 大部分属性命名!
JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。在你自己的组件中,经常会遇到需要用变量的方式读取这些属性的时候。但 JavaScript 对变量的命名有限制。例如,变量名称不能包含 -
符号或者像 class
这样的保留字。
这就是为什么在 React 中,大部分 HTML 和 SVG 属性都用驼峰式命名法表示。例如,需要用 strokeWidth 代替 stroke-width。由于 class 是一个保留字,所以在 React 中需要用 className 来代替。这也是 DOM 属性中的命名:
1 | <img |
你可以 在 React DOM 元素中找到所有对应的属性。如果你在编写属性时发生了错误,不用担心 —— React 会在 浏览器控制台 中打印一条可能的更正信息。
注意
由于历史原因,aria-* 和 data-* 属性是以带 - 符号的 HTML 格式书写的。
高级提示:使用 JSX 转化器
将现有的 HMTL 中的所有属性转化 JSX 的格式是很繁琐的。我们建议使用 转化器 将 HTML 和 SVG 标签转化为 JSX。这种转化器在实践中非常有用。但我们依然有必要去了解这种转化过程中发生了什么,这样你就可以编写自己的 JSX 了。
这是最终的结果:
1 | export default function TodoList() { |
在 JSX 中通过大括号使用 JavaScript
JSX 允许你在 JavaScript 中编写类似 HTML 的标签,从而使渲染的逻辑和内容可以写在一起。有时候,你可能想要在标签中添加一些 JavaScript 逻辑或者引用动态的属性。这种情况下,你可以在 JSX 的大括号内来编写 JavaScript。
摘要
你将会学习并了解:
如何使用引号传递字符串
- JSX 引号内的值会作为字符串传递给属性。
在 JSX 的大括号内引用 JavaScript 变量
- 大括号让你可以将 JavaScript 的逻辑和变量带入到标签中。
在 JSX 的大括号内调用 JavaScript 函数
- 它们会在 JSX 标签中的内容区域或紧随属性的
=
后起作用。
- 它们会在 JSX 标签中的内容区域或紧随属性的
在 JSX 的大括号内使用 JavaScript 对象
{{` 和 `}}
并不是什么特殊的语法:它只是包在 JSX 大括号内的 JavaScript 对象。
使用引号传递字符串
当你想把一个字符串属性传递给 JSX 时,把它放到单引号或双引号中:
App.js
1 | export default function Avatar() { |
这里的 "https://i.imgur.com/7vQD0fPs.jpg"
和 "Gregorio Y. Zara"
就是被作为字符串传递的。
但是如果你想要动态地指定 src
或 alt
的值呢?你可以 用 {
和 }
替代 "
和 "
以使用 JavaScript 变量 :
App.js
1 | export default function Avatar() { |
请注意 className="avatar"
和 src={avatar}
之间的区别,className="avatar"
指定了一个就叫 "avatar"
的使图片在样式上变圆的 CSS 类名,而 src={avatar}
这种写法会去读取 JavaScript 中 avatar
这个变量的值。这是因为大括号可以使你直接在标签中使用 JavaScript!
使用大括号:一扇进入 JavaScript 世界的窗户
JSX 是一种编写 JavaScript 的特殊方式。这为在大括号 { }
中使用 JavaScript 带来了可能。下面的示例中声明了科学家的名字,name
,然后在 <h1>
后的大括号内嵌入它:
App.js
1 | export default function TodoList() { |
试着将 name
的值从 'Gregorio Y. Zara'
更改成 'Hedy Lamarr'
。看看这个 To Do List 的标题将如何变化?
大括号内的任何 JavaScript 表达式都能正常运行,包括像 formatDate()
这样的函数调用:
App.js
1 | const today = new Date(); |
可以在哪使用大括号
在 JSX 中,只能在以下两种场景中使用大括号:
- 用作 JSX 标签内的文本:
<h1>{name}'s To Do List</h1>
是有效的,但是<{tag}>Gregorio Y. Zara's To Do List</{tag}>
无效。- 用作紧跟在
=
符号后的 属性: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
属性传递一个对象:
App.js
1 | export default function TodoList() { |
试着更改一下 backgroundColor
和 color
的值。
当你写成这样时,你可以很清楚地看到大括号里包着的对象:
1 | <ul style={ |
所以当你下次在 JSX 中看到 {{` 和 `}}
时,就知道它只不过是包在大括号里的一个对象罢了!
注意
内联
style
属性 使用驼峰命名法编写。例如,HTML<ul style="background-color: black">
在你的组件里应该写成<ul style={{ backgroundColor: 'black' }}>
。
JavaScript 对象和大括号的更多可能
你可以将多个表达式合并到一个对象中,在 JSX 的大括号内分别使用它们:
App.js
1 | const person = { |
在这个示例中,person
JavaScript 对象包含 name
中存储的字符串和 theme
对象:
1 | const person = { |
该组件可以这样使用来自 person
的值:
1 | <div style={person.theme}> |
JSX 是一种模板语言的最小实现,因为它允许你通过 JavaScript 来组织数据和逻辑。
将 Props 传递给组件
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。Props 可能会让你想起 HTML 属性,但你可以通过它们传递任何 JavaScript 值,包括对象、数组和函数。
摘要
你将会学习并了解:
如何向组件传递 props
- 要传递 props,请将它们添加到 JSX,就像使用 HTML 属性一样。
如何从组件读取 props
- 要读取 props,请使用
function Avatar({ person, size })
解构语法。
- 要读取 props,请使用
如何为 props 指定默认值
- 你可以指定一个默认值,如
size = 100
,用于缺少值或值为undefined
的 props 。
- 你可以指定一个默认值,如
如何给组件传递 JSX
- 你可以使用
<Avatar {...props} />
JSX 展开语法转发所有 props,但不要过度使用它! - 像
<Card><Avatar /></Card>
这样的嵌套 JSX,将被视为Card
组件的children
prop。
- 你可以使用
Props 如何随时间变化
- Props 是只读的时间快照:每次渲染都会收到新版本的 props。
- 你不能改变 props。当你需要交互性时,你可以设置 state。
熟悉的 props
Props 是你传递给 JSX 标签的信息。例如,className
、src
、alt
、width
和 height
便是一些可以传递给 <img>
的 props:
App.js
1 | function Avatar() { |
你可以传递给 img 标签的 props 是预定义的(ReactDOM 符合 HTML 标准)。但是你可以将任何 props 传递给 你自己的 组件,例如 Avatar 标签 ,以便自定义它们。 就像这样!
向组件传递 props
在这段代码中, Profile
组件没有向它的子组件 Avatar
传递任何 props :
1 | export default function Profile() { |
你可以分两步给 Avatar
一些 props。
步骤 1: 将 props 传递给子组件
首先,将一些 props 传递给 Avatar
。例如,让我们传递两个 props:person
(一个对象)和 size
(一个数字):
1 | export default function Profile() { |
注意
如果 person= 后面的双花括号让你感到困惑,请记住,在 JSX 花括号中,它们只是一个对象。
现在,你可以在 Avatar
组件中读取这些 props 了。
步骤 2: 在子组件中读取 props
你可以通过在 function Avatar
之后直接列出它们的名字 person, size
来读取这些 props。这些 props 在 ({
和 })
之间,并由逗号分隔。这样,你可以在 Avatar
的代码中使用它们,就像使用变量一样。
1 | function Avatar({ person, size }) { |
向使用 person
和 size
props 渲染的 Avatar
添加一些逻辑,你就完成了。
现在你可以配置 Avatar
,通过不同的 props,使它能以多种不同的方式进行渲染。尝试变换值吧!
utils.js
1 | export function getImageUrl(person, size = 's') { |
App.js
1 | import { getImageUrl } from './utils.js'; |
Props 使你独立思考父组件和子组件。 例如,你可以改变 Profile
中的 person
或 size
props,而无需考虑 Avatar
如何使用它们。 同样,你可以改变 Avatar
使用这些 props 的方式,不必考虑 Profile
。
你可以将 props 想象成可以调整的“旋钮”。它们的作用与函数的参数相同 —— 事实上,props 正是 组件的唯一参数! React 组件函数接受一个参数,一个 props
对象:
1 | function Avatar(props) { |
通常你不需要整个 props
对象,所以可以将它解构为单独的 props。
注意
在声明 props 时, 不要忘记
(
和)
之间的一对花括号{
和}
:
1
2
3 function Avatar({ person, size }) {
// ...
} 这种语法被称为 “解构”,等价于于从函数参数中读取属性:
1
2
3
4
5 function Avatar(props) {
let person = props.person;
let size = props.size;
// ...
}
给 prop 指定一个默认值
如果你想在没有指定值的情况下给 prop 一个默认值,你可以通过在参数后面写 =
和默认值来进行解构:
1 | function Avatar({ person, size = 100 }) { |
现在, 如果 <Avatar person={...} />
渲染时没有 size
prop, size
将被赋值为 100
。
默认值仅在缺少 size
prop 或 size={undefined}
时生效。 但是如果你传递了 size={null}
或 size={0}
,默认值将 不 被使用。
使用 JSX 展开语法传递 props
有时候,传递 props 会变得非常重复:
1 | function Profile({ person, size, isSepia, thickBorder }) { |
重复代码没有错(它可以更清晰)。但有时你可能会重视简洁。一些组件将它们所有的 props 转发给子组件,正如 Profile
转给 Avatar
那样。因为这些组件不直接使用他们本身的任何 props,所以使用更简洁的“展开”语法是有意义的:
1 | function Profile(props) { |
这会将 Profile
的所有 props 转发到 Avatar
,而不列出每个名字。
请克制地使用展开语法。 如果你在所有其他组件中都使用它,那就有问题了。 通常,它表示你应该拆分组件,并将子组件作为 JSX 传递。 接下来会详细介绍!
将 JSX 作为子组件传递
嵌套浏览器内置标签是很常见的:
1 | <div> |
有时你会希望以相同的方式嵌套自己的组件:
1 | <Card> |
当您将内容嵌套在 JSX 标签中时,父组件将在名为 children
的 prop 中接收到该内容。例如,下面的 Card
组件将接收一个被设为 <Avatar />
的 children
prop 并将其包裹在 div 中渲染:
utils.js
1 | export function getImageUrl(person, size = 's') { |
Avatar.js
1 | import { getImageUrl } from './utils.js'; |
App.js
1 | import Avatar from './Avatar.js'; |
尝试用一些文本替换 <Card>
中的 <Avatar>
,看看 Card
组件如何包裹任意嵌套内容。它不必“知道”其中渲染的内容。你会在很多地方看到这种灵活的模式。
可以将带有 children
prop 的组件看作有一个“洞”,可以由其父组件使用任意 JSX 来“填充”。你会经常使用 children
prop 来进行视觉包装:面板、网格等等。
Props 如何随时间变化
下面的 Clock
组件从其父组件接收两个 props:color
和 time
。(父组件的代码被省略,因为它使用 state,我们暂时不会深入研究。)
尝试在下面的选择框中更改颜色:
Clock.js
1 | export default function Clock({ color, time }) { |
这个例子说明,一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!在这里,time
prop 每秒都在变化。当你选择另一种颜色时,color
prop 也改变了。Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。
然而,props 是 不可变的(一个计算机科学术语,意思是“不可改变”)。当一个组件需要改变它的 props(例如,响应用户交互或新数据)时,它不得不“请求”它的父组件传递 不同的 props —— 一个新对象!它的旧 props 将被丢弃,最终 JavaScript 引擎将回收它们占用的内存。
不要尝试“更改 props”。 当你需要响应用户输入(例如更改所选颜色)时,你可以“设置 state”,你可以在 State: 一个组件的内存 中继续了解。
条件渲染
通常你的组件会需要根据不同的情况显示不同的内容。在 React 中,你可以通过使用 JavaScript 的 if 语句、&& 和 ? : 运算符来选择性地渲染 JSX。
摘要
你将会学习并了解:
如何根据不同条件返回不同的 JSX
- 在 React,你可以使用 JavaScript 来控制分支逻辑。
- 你可以使用
if
语句来选择性地返回 JSX 表达式。 - 你可以选择性地将一些 JSX 赋值给变量,然后用大括号将其嵌入到其他 JSX 中。
如何根据不同条件包含或者去掉部分 JSX
- 在 JSX 中,
{cond ? <A /> : <B />}
表示 “当cond
为真值时, 渲染<A />
,否则<B />
”。 - 在 JSX 中,
{cond && <A />}
表示 “当cond
为真值时, 渲染<A />
,否则不进行渲染”。
- 在 JSX 中,
一些你会在 React 代码库里遇到的常用的条件语法快捷表达式
- 快捷的表达式很常见,但如果你更倾向于使用
if
,你也可以不使用它们。
- 快捷的表达式很常见,但如果你更倾向于使用
条件返回 JSX
假设有一个 PackingList
组件,里面渲染多个 Item
组件,每个物品可标记为打包与否:
App.js
1 | function Item({ name, isPacked }) { |
需要注意的是,有些 Item 组件的 isPacked 属性是被设为 true 而不是 false。你可以在那些满足 isPacked={true} 条件的物品旁加上一个勾选符号(✔)。
你可以用 if/else 语句 去判断:
1 | if (isPacked) { |
如果 isPacked 属性是 true,这段代码会返回一个不一样的 JSX。通过这样的改动,一些物品的名字后面会出现一个勾选符号:
App.js
1 | function Item({ name, isPacked }) { |
动手尝试一下,看看各种情况会出现什么不同的结果!
留意这里你是怎么使用 JavaScript 的 if
和 return
语句来写分支逻辑。在 React 中,是由 JavaScript 来处理控制流的(比如条件)。
选择性地返回 null
在一些情况下,你不想有任何东西进行渲染。比如,你不想显示已经打包好的物品。但一个组件必须返回一些东西。这种情况下,你可以直接返回 null。
1 | if (isPacked) { |
如果组件的 isPacked
属性为 true
,那么它将只返回 null
。否则,它将返回相应的 JSX 用来渲染。
App.js
1 | function Item({ name, isPacked }) { |
实际上,在组件里返回 null
并不常见,因为这样会让想使用它的开发者感觉奇怪。通常情况下,你可以在父组件里选择是否要渲染该组件。让我们接着往下看吧!
选择性地包含 JSX
在之前的例子里,你在组件内部控制哪些 JSX 树(如果有的话!)会返回。你可能已经发现了在渲染输出里会有一些重复的内容:
1 | <li className="item">{name} ✔</li> |
和下面的写法很像:
1 | <li className="item">{name}</li> |
两个条件分支都会返回 <li className="item">...</li>
:
1 | if (isPacked) { |
虽然这些重复的内容没什么害处,但这样可能会导致你的代码更难维护。比如你想更改 className?你就需要修改两个地方!针对这种情况,你可以通过选择性地包含一小段 JSX 来让你的代码更加 DRY。
三目运算符(? :
)
JavaScript 有一种紧凑型语法来实现条件判断表达式——条件运算符 又称“三目运算符”。
除了这样:
1 | if (isPacked) { |
你还可以这样实现:
1 | return ( |
你可以认为,“如果 isPacked 为 true 时,则(?)渲染 name + ‘ ✔’,否则(:)渲染 name。”
两个例子完全一样吗?
如果你之前是习惯面向对象开发的,你可能会认为上面的两个例子略有不同,因为其中一个可能会创建两个不同的
现在,假如你想将对应物品的文本放到另一个 HTML 标签里,比如用 <del>
来显示删除线。你可以添加更多的换行和括号,以便在各种情况下更好地去嵌套 JSX:
App.js
1 | function Item({ name, isPacked }) { |
对于简单的条件判断,这样的风格可以很好地实现,但需要适量使用。如果你的组件里有很多的嵌套式条件表达式,则需要考虑通过提取为子组件来简化这些嵌套表达式。在 React 里,标签也是你代码中的一部分,所以你可以使用变量和函数来整理一些复杂的表达式。
与运算符(&&
)
你会遇到的另一个常见的快捷表达式是 JavaScript 逻辑与(&&)运算符。在 React 组件里,通常用在当条件成立时,你想渲染一些 JSX,或者不做任何渲染。使用 &&,你也可以实现仅当 isPacked 为 true 时,渲染勾选符号。
1 | return ( |
你可以认为,“当 isPacked 为真值时,则(&&)渲染勾选符号,否则,不渲染。”
下面为具体的例子:
App.js
1 | function Item({ name, isPacked }) { |
当 JavaScript && 表达式 的左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但条件的结果是 false,则整个表达式会变成 false。在 JSX 里,React 会将 false 视为一个“空值”,就像 null 或者 undefined,这样 React 就不会在这里进行任何渲染。
陷阱
切勿将数字放在 && 左侧.
JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。
例如,一个常见的错误是 messageCount &&
New messages
。其原本是想当 messageCount 为 0 的时候不进行渲染,但实际上却渲染了 0。 为了更正,可以将左侧的值改成布尔类型:messageCount > 0 &&
New messages
。选择性地将 JSX 赋值给变量
当这些快捷方式妨碍写普通代码时,可以考虑使用 if 语句和变量。因为你可以使用 let 进行重复赋值,所以一开始你可以将你想展示的(这里指的是物品的名字)作为默认值赋予给该变量。
1 | let itemContent = name; |
结合 if
语句,当 isPacked
为 true
时,将 JSX 表达式的值重新赋值给 itemContent
:
1 | if (isPacked) { |
在 JSX 中通过大括号使用 JavaScript。将变量用大括号嵌入在返回的 JSX 树中,来嵌套计算好的表达式与 JSX:
1 | <li className="item"> |
这种方式是最冗长的,但也是最灵活的。下面是相关的例子:
App.js
1 | function Item({ name, isPacked }) { |
跟之前的一样,这个方式不仅仅适用于文本,任意的 JSX 均适用:
如果对 JavaScript 不熟悉,这些不同的风格一开始可能会让你感到不知所措。但是,学习这些将有助于你理解和写任何的 JavaScript 代码,而不仅仅是 React 组件。一开始可以选择一个你喜欢的来用,然后当你忘记其他的怎么用时,可以再翻阅这份参考资料。
渲染列表
你可能经常需要通过 JavaScript 的数组方法 来操作数组中的数据,从而将一个数据集渲染成多个相似的组件。在这篇文章中,你将学会如何在 React 中使用 filter() 筛选需要渲染的组件和使用 map() 把数组转换成组件数组。
摘要
你将会学习并了解:
如何从组件中抽离出数据,并把它们放入像数组、对象这样的数据结构中。
如何通过 JavaScript 的
map()
方法从数组中生成组件如何通过 JavaScript 的
filter()
筛选需要渲染的组件为何以及如何给集合中的每个组件设置一个 key 值:它使 React 能追踪这些组件,即便后者的位置或数据发生了变化。
从数组中渲染数据
这里我们有一个列表。
1 | <ul> |
可以看到,这些列表项之间唯一的区别就是其中的内容/数据。未来你可能会碰到很多类似的情况,在那些场景中,你想基于不同的数据渲染出相似的组件,比如评论列表或者个人资料的图库。在这样的场景下,可以把要用到的数据存入 JavaScript 对象或数组,然后用 map() 或 filter() 这样的方法来渲染出一个组件列表。
这里给出一个由数组生成一系列列表项的简单示例:
- 首先,把数据 存储 到数组中:
1 | const people = [ |
- 遍历
people
这个数组中的每一项,并获得一个新的 JSX 节点数组listItems
:
1 | const listItems = people.map(person => <li>{person}</li>); |
- 把
listItems
用<ul>
包裹起来,然后 返回 它:
1 | return <ul>{listItems}</ul>; |
来看看运行的结果:
App.js
1 | const people = [ |
注意
上面的沙盒可能会输出这样一个控制台错误:
等会我们会学到怎么修复它。在此之前,我们先来看看如何把这个数组变得更加结构化。
对数组项进行过滤
让我们把 people
数组变得更加结构化一点。
1 | const people = [ |
现在,假设你只想在屏幕上显示职业是 化学家
的人。那么你可以使用 JavaScript 的 filter()
方法来返回满足条件的项。这个方法会让数组的子项经过 “过滤器”(一个返回值为 true
或 false
的函数)的筛选,最终返回一个只包含满足条件的项的新数组。
既然你只想显示 profession 值是 化学家 的人,那么这里的 “过滤器” 函数应该长这样:(person) => person.profession === ‘化学家’。下面我们来看看该怎么把它们组合在一起:
- 首先,创建 一个用来存化学家们的新数组
chemists
,这里用到filter()
方法过滤people
数组来得到所有的化学家,过滤的条件应该是person.profession === '化学家'
:
1 | const chemists = people.filter(person => |
- 接下来 用 map 方法遍历
chemists
数组:
1 | const listItems = chemists.map(person => |
- 最后,返回
listItems
:
1 | return <ul>{listItems}</ul>; |
让我们来看看完整的代码:
utils.js
1 | export function getImageUrl(person) { |
data.js
1 | export const people = [ |
App.js
1 | import { people } from './data.js'; |
陷阱
因为箭头函数会隐式地返回位于 => 之后的表达式,所以你可以省略 return 语句。
1 | const listItems = chemists.map(person => |
不过,如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!
1 | const listItems = chemists.map(person => { // 花括号 |
箭头函数 => { 后面的部分被称为 “块函数体”,块函数体支持多行代码的写法,但要用 return 语句才能指定返回值。假如你忘了写 return,那这个函数什么都不会返回!
用 key 保持列表项的顺序
如果把上面任何一个沙盒示例在新标签页打开,你就会发现控制台有这样一个报错:
这是因为你必须给数组中的每一项都指定一个 key
——它可以是字符串或数字的形式,只要能唯一标识出各个数组项就行:
1 | <li key={person.id}>...</li> |
注意
直接放在 map()
方法里的 JSX 元素一般都需要指定 key
值!
这些 key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要。一个合适的 key
可以帮助 React 推断发生了什么,从而得以正确地更新 DOM 树。
用作 key 的值应该在数据中提前就准备好,而不是在运行时才随手生成:
utils.js
1 | export function getImageUrl(person) { |
data.js
1 | export const people = [ |
App.js
1 | import { people } from './data.js'; |
为每个列表项显示多个 DOM 节点
如果你想让每个列表项都输出多个 DOM 节点而非一个的话,该怎么做呢?
Fragment 语法的简写形式 <> </> 无法接受 key 值,所以你只能要么把生成的节点用一个 div 标签包裹起来,要么使用长一点但更明确的 Fragment 写法:
1 | import { Fragment } from 'react'; |
这里的 Fragment 标签本身并不会出现在 DOM 上,这串代码最终会转换成 <h1>
、<p>
、<h1>
、<p>
…… 的列表。
如何设定 key 值
不同来源的数据往往对应不同的 key 值获取方式:
- 来自数据库的数据: 如果你的数据是从数据库中获取的,那你可以直接使用数据表中的主键,因为它们天然具有唯一性。
- 本地产生数据: 如果你数据的产生和保存都在本地(例如笔记软件里的笔记),那么你可以使用一个自增计数器或者一个类似 uuid 的库来生成 key。
key 需要满足的条件
- key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。
- key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key。
React 中为什么需要 key?
设想一下,假如你桌面上的文件都没有文件名,取而代之的是,你需要通过文件的位置顺序来区分它们———第一个文件,第二个文件,以此类推。也许你也不是不能接受这种方式,可是一旦你删除了其中的一个文件,这种组织方式就会变得混乱无比。原来的第二个文件可能会变成第一个文件,第三个文件会成为第二个文件……
React 里需要 key 和文件夹里的文件需要有文件名的道理是类似的。它们(key 和文件名)都让我们可以从众多的兄弟元素中唯一标识出某一项(JSX 节点或文件)。而一个精心选择的 key 值所能提供的信息远远不止于这个元素在数组中的位置。即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它。
陷阱
你可能会想直接把数组项的索引当作 key 值来用,实际上,如果你没有显式地指定 key 值,React 确实默认会这么做。但是数组项的顺序在插入、删除或者重新排序等操作中会发生改变,此时把索引顺序用作 key 值会产生一些微妙且令人困惑的 bug。
与之类似,请不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建。这不仅会造成运行变慢的问题,更有可能导致用户输入的丢失。所以,使用能从给定数据中稳定取得的值才是明智的选择。
有一点需要注意,组件不会把 key 当作 props 的一部分。Key 的存在只对 React 本身起到提示作用。如果你的组件需要一个 ID,那么请把它作为一个单独的 prop 传给组件:
保持组件纯粹
部分 JavaScript 函数是 纯粹 的,这类函数通常被称为纯函数。纯函数仅执行计算操作,不做其他操作。你可以通过将组件按纯函数严格编写,以避免一些随着代码库的增长而出现的、令人困扰的 bug 以及不可预测的行为。但为了获得这些好处,你需要遵循一些规则。
摘要
你将会学习并了解:
纯函数是什么,以及它如何帮助你避免 bug
- 一个组件必须是纯粹的,就意味着:
- 只负责自己的任务。 它不会更改在该函数调用前就已存在的对象或变量。
- 输入相同,则输出相同。 给定相同的输入,组件应该总是返回相同的 JSX。
- 一个组件必须是纯粹的,就意味着:
如何将数据变更与渲染过程分离,以保持组件的纯粹
- 渲染随时可能发生,因此组件不应依赖于彼此的渲染顺序。
- 你不应该改变组件用于渲染的任何输入。这包括 props、state 和 context。通过 “设置” state 来更新界面,而不要改变预先存在的对象。
如何使用严格模式发现组件中的错误
纯函数:组件作为公式
在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:
- 只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。
- 输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。
举个你非常熟悉的纯函数示例:数学中的公式。
考虑如下数学公式:y = 2x。
若 x = 2 则 y = 4。永远如此。
若 x = 3 则 y = 6。永远如此。
若 x = 3,那么 y 并不会因为时间或股市的影响,而有时等于 9 、 –1 或 2.5。
若 y = 2x 且 x = 3, 那么 y 永远 等于 6.
我们使用 JavaScript 的函数实现,看起来将会是这样:
1 | function double(number) { |
上述例子中,double() 就是一个 纯函数。如果你传入 3 ,它将总是返回 6 。
React 便围绕着这个概念进行设计。React 假设你编写的所有组件都是纯函数。
也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX。
App.js
1 | function Recipe({ drinkers }) { |
当你给函数 Recipe 传入 drinkers={2} 参数时,它将返回包含 2 cups of water 的 JSX。永远如此。
而当你传入 drinkers={4} 时,它将返回包含 4 cups of water 的 JSX。永远如此。
就像数学公式一样。
你可以把你的组件当作食谱:如果你遵循它们,并且在烹饪过程中不引入新食材,你每次都会得到相同的菜肴。那这道 “菜肴” 就是组件用于 React 渲染 的 JSX。
副作用:(不符合)预期的后果
React 的渲染过程必须自始至终是纯粹的。组件应该只 返回 它们的 JSX,而不 改变 在渲染前,就已存在的任何对象或变量 — 这将会使它们变得不纯粹!
以下是违反这一规则的组件示例:
App.js
1 | let guest = 0; |
该组件正在读写其外部声明的 guest 变量。这意味着 多次调用这个组件会产生不同的 JSX!并且,如果 其他 组件读取 guest ,它们也会产生不同的 JSX,其结果取决于它们何时被渲染!这是无法预测的。
回到我们的公式 y = 2x ,现在即使 x = 2 ,我们也不能相信 y = 4 。我们的测试可能会失败,我们的用户可能会感到困扰,飞机可能会从天空坠毁——你将看到这会引发多么扑朔迷离的 bugs!
你可以 将 guest 作为 prop 传入 来修复此组件:
App.js
1 | function Cup({ guest }) { |
现在你的组件就是纯粹的,因为它返回的 JSX 只依赖于 guest prop。
一般来说,你不应该期望你的组件以任何特定的顺序被渲染。调用 y = 5x 和 y = 2x 的先后顺序并不重要:这两个公式相互独立。同样地,每个组件也应该“独立思考”,而不是在渲染过程中试图与其他组件协调,或者依赖于其他组件。渲染过程就像是一场学校考试:每个组件都应该自己计算 JSX!
使用严格模式检测不纯的计算
尽管你可能还没使用过,但在 React 中,你可以在渲染时读取三种输入:props,state 和 context。你应该始终将这些输入视为只读。
当你想根据用户输入 更改 某些内容时,你应该 设置状态,而不是直接写入变量。当你的组件正在渲染时,你永远不应该改变预先存在的变量或对象。
React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。
我们注意到,原始示例显示的是 “Guest #2”、“Guest #4” 和 “Guest #6”,而不是 “Guest #1”、“Guest #2” 和 “Guest #3”。原来的函数并不纯粹,因此调用它两次就出现了问题。但对于修复后的纯函数版本,即使调用该函数两次也能得到正确结果。纯函数仅仅执行计算,因此调用它们两次不会改变任何东西 — 就像两次调用 double(2) 并不会改变返回值,两次求解 y = 2x 不会改变 y 的值一样。相同的输入,总是返回相同的输出。
严格模式在生产环境下不生效,因此它不会降低应用程序的速度。如需引入严格模式,你可以用 <React.StrictMode> 包裹根组件。一些框架会默认这样做。
局部 mutation:组件的小秘密
上述示例的问题出在渲染过程中,组件改变了 预先存在的 变量的值。为了让它听起来更可怕一点,我们将这种现象称为 突变(mutation) 。纯函数不会改变函数作用域外的变量、或在函数调用前创建的对象——这会使函数变得不纯粹!
但是,你完全可以在渲染时更改你 *刚刚* 创建的变量和对象。在本示例中,你创建一个 []
数组,将其分配给一个 cups
变量,然后 push
一打 cup 进去:
App.js
1 | function Cup({ guest }) { |
如果 cups
变量或 []
数组是在 TeaGathering
函数之外创建的,这将是一个很大的问题!因为如果那样的话,当你调用数组的 push 方法时,就会更改 预先存在的 对象。
但是,这里不会有影响,因为每次渲染时,你都是在 TeaGathering
函数内部创建的它们。TeaGathering
之外的代码并不会知道发生了什么。这就被称为 “局部 mutation” — 如同藏在组件里的小秘密。
哪些地方 可能 引发副作用
函数式编程在很大程度上依赖于纯函数,但 某些事物 在特定情况下不得不发生改变。这是编程的要义!这些变动包括更新屏幕、启动动画、更改数据等,它们被称为 副作用。它们是 “额外” 发生的事情,与渲染过程无关。
在 React 中,副作用通常属于 事件处理程序。事件处理程序是 React 在你执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在你的组件 内部 定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数。
如果你用尽一切办法,仍无法为副作用找到合适的事件处理程序,你还可以调用组件中的 useEffect 方法将其附加到返回的 JSX 中。这会告诉 React 在渲染结束后执行它。然而,这种方法应该是你最后的手段。
如果可能,请尝试仅通过渲染过程来表达你的逻辑。你会惊讶于这能带给你多少好处!
React 为何侧重于纯函数?
编写纯函数需要遵循一些习惯和规程。但它开启了绝妙的机遇:
- 你的组件可以在不同的环境下运行 — 例如,在服务器上!由于它们针对相同的输入,总是返回相同的结果,因此一个组件可以满足多个用户请求。
- 你可以为那些输入未更改的组件来 跳过渲染,以提高性能。这是安全的做法,因为纯函数总是返回相同的结果,所以可以安全地缓存它们。
- 如果在渲染深层组件树的过程中,某些数据发生了变化,React 可以重新开始渲染,而不会浪费时间完成过时的渲染。纯粹性使得它随时可以安全地停止计算。
我们正在构建的每个 React 新特性都利用到了纯函数。从数据获取到动画再到性能,保持组件的纯粹可以充分释放 React 范式的能力。