React17 核心概念
JSX简介
1 | const element = <h1>Hello, world!</h1>; |
它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。
为什么使用 JSX?
React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。
React 并没有采用将标记与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为“组件”的松散耦合单元之中,来实现关注点分离。
在 JSX 中嵌入表达式
1 | const name = 'Josh Perez';const element = <h1>Hello, {name}</h1>; |
在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式。
1 | function formatName(user) { |
JSX 也是一个表达式
可以在 if
语句和 for
循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX:
1 | function getGreeting(user) { |
JSX 特定属性
可以通过使用引号,来将属性值指定为字符串字面量:
1 | const element = <div tabIndex="0"></div>; |
可以使用大括号,来在属性值中插入一个 JavaScript 表达式:
1 | const element = <img src={user.avatarUrl}></img>; |
在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。
TIP
JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用
camelCase
(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。
JSX 防止注入攻击
可以安全地在 JSX 当中插入用户输入内容:
1 | const title = response.potentiallyMaliciousInput; |
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。
JSX 表示对象
Babel 会把 JSX 转译成一个名为 React.createElement()
函数调用。
以下两种示例代码完全等效:
1 | const element = ( |
1 | const element = React.createElement( |
实际上它创建了一个这样的对象:
1 | // 注意:这是简化过的结构 |
元素渲染
元素描述了你在屏幕上想看到的内容。
与浏览器的 DOM 元素不同,React 元素是创建开销极小的普通对象。React DOM 会负责更新 DOM 来与 React 元素保持一致。
将一个元素渲染为 DOM
1 | <div id="root"></div> |
我们将其称为“根” DOM 节点,因为该节点内的所有内容都将由 React DOM 管理。
仅使用 React 构建的应用通常只有单一的根 DOM 节点。
如果你在将 React 集成进一个已有应用,那么你可以在应用中包含任意多的独立根 DOM 节点。
想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render()
:
1 | const element = <h1>Hello, world</h1>; |
更新已渲染的元素
React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。
更新 UI 唯一的方式是创建一个全新的元素,并将其传入 ReactDOM.render()
。
1 | function tick() { |
React 只更新它需要更新的部分
React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。
组件 & Props
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
函数组件与 class 组件
定义组件最简单的方式就是编写 JavaScript 函数:
1 | function Welcome(props) { |
该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。
同时还可以使用 ES6 的 class 来定义组件:
1 | class Welcome extends React.Component { |
上述两个组件在 React 里是等效的。
渲染组件
React 元素可以是用户自定义的组件:
1 | const element = <Welcome name="Sara" />; |
当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。
例如,这段代码会在页面上渲染 “Hello, Sara”:
1 | function Welcome(props) { return <h1>Hello, {props.name}</h1>; |
- 我们调用
ReactDOM.render()
函数,并传入<Welcome name="Sara" />
作为参数。 - React 调用
Welcome
组件,并将{name: 'Sara'}
作为 props 传入。 Welcome
组件将<h1>Hello, Sara</h1>
元素作为返回值。- React DOM 将 DOM 高效地更新为
<h1>Hello, Sara</h1>
。
TIP
组件名称必须以大写字母开头
组合组件
组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。
1 | function Welcome(props) { |
通常来说,每个新的 React 应用程序的顶层组件都是 App
组件。
Props 的只读性
组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。
1 | function sum(a, b) { |
这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。
相反,下面这个函数则不是纯函数,因为它更改了自己的入参:
1 | function withdraw(account, amount) { |
React 非常灵活,但它也有一个严格的规则:所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
State & 生命周期
在元素渲染章节中,我们只了解了一种更新 UI 界面的方法。通过调用 ReactDOM.render()
来修改我们想要渲染的元素:
在本章节中,我们将学习如何封装真正可复用的 Clock
组件。它将设置自己的计时器并每秒更新一次。
可以从封装时钟的外观开始:
1 | function Clock(props) { |
Clock
组件需要设置一个计时器,并且需要每秒更新 UI。
1 | ReactDOM.render( |
需要在 Clock
组件中添加 “state” 来实现这个功能。
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
将函数组件转换成 class 组件
通过以下五步将 Clock
的函数组件转成 class 组件:
- 创建一个同名的 ES6 class,并且继承于
React.Component
。 - 添加一个空的
render()
方法。 - 将函数体移动到
render()
方法之中。 - 在
render()
方法中使用this.props
替换props
。 - 删除剩余的空函数声明。
1 | class Clock extends React.Component { |
每次组件更新时 render
方法都会被调用,但只要在相同的 DOM 节点中渲染 <Clock />
,就仅有一个 Clock
组件的 class 实例被创建使用。
向 class 组件中添加局部的 state
我们通过以下三步将 date
从 props 移动到 state 中:
- 把
render()
方法中的this.props.date
替换成this.state.date
:
1 | <h2>It is {this.state.date.toLocaleTimeString()}.</h2> |
- 添加一个 class 构造函数,然后在该函数中为
this.state
赋初值:
1 | constructor(props) { |
Class 组件应该始终使用 props
参数来调用父类的构造函数。
- 移除
<Clock />
元素中的date
属性:
1 | ReactDOM.render( |
将生命周期方法添加到 Class 中
当 Clock
组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器。这在 React 中被称为“挂载(mount)”。
同时,当 DOM 中 Clock
组件被删除的时候,应该清除计时器。这在 React 中被称为“卸载(unmount)”。
我们可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法:
1 | componentDidMount() { |
这些方法叫做“生命周期方法”。
最终得到计时器:
1 | class Clock extends React.Component { |
- 当
<Clock />
被传给ReactDOM.render()
的时候,React 会调用Clock
组件的构造函数。因为Clock
需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化this.state
。我们会在之后更新 state。 - 之后 React 会调用组件的
render()
方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配Clock
渲染的输出。 - 当
Clock
的输出被插入到 DOM 中后,React 就会调用ComponentDidMount()
生命周期方法。在这个方法中,Clock
组件向浏览器请求设置一个计时器来每秒调用一次组件的tick()
方法。 - 浏览器每秒都会调用一次
tick()
方法。 在这方法之中,Clock
组件会通过调用setState()
来计划进行一次 UI 更新。得益于setState()
的调用,React 能够知道 state 已经改变了,然后会重新调用render()
方法来确定页面上该显示什么。这一次,render()
方法中的this.state.date
就不一样了,如此一来就会渲染输出更新过的时间。React 也会相应的更新 DOM。 - 一旦
Clock
组件从 DOM 中被移除,React 就会调用componentWillUnmount()
生命周期方法,这样计时器就停止了。
正确地使用 State
关于 setState()
你应该了解三件事:
不要直接修改 State
应该使用 setState()
:
1 | // Wrong |
State 的更新可能是异步的
出于性能考虑,React 可能会把多个 setState()
调用合并成一个调用。
因为 this.props
和 this.state
可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
要解决这个问题,可以让 setState()
接收一个函数而不是一个对象。
1 | // Wrong |
State 的更新会被合并
当你调用 setState()
的时候,React 会把你提供的对象合并到当前的 state。
可以分别调用 setState()
来单独地更新它们
数据是向下流动的
不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。
这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。
组件可以选择把它的 state 作为 props 向下传递到它的子组件中:
1 | <FormattedDate date={this.state.date} /> |
FormattedDate
组件会在其 props 中接收参数 date
,但是组件本身无法知道它是来自于 Clock
的 state,或是 Clock
的 props,还是手动输入的。
这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。
事件处理
- React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
1 | // 传统的 HTML |
在 React 中另一个不同点是你不能通过返回 false
的方式阻止默认行为。你必须显式的使用 preventDefault
。
1 | function ActionLink() { |
使用 React 时,你一般不需要使用 addEventListener
为已创建的 DOM 元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加监听器即可。
当你使用 ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。
1 | class Toggle extends React.Component { |
你必须谨慎对待 JSX 回调函数中的 this
,在 JavaScript 中,class 的方法默认不会绑定 this
。如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
。
这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 ()
,例如 onClick={this.handleClick}
,你应该为这个方法绑定 this
。
向事件处理程序传递参数
在循环中,通常我们会为事件处理函数传递额外的参数。
1 | <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> |
上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind
来实现。
在这两种情况下,React 的事件对象 e
会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。
条件渲染
React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if
或者条件运算符去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。
1 | function Greeting(props) { |
元素变量
你可以使用变量来储存元素。 它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。
1 | render() { |
与运算符 &&
通过花括号包裹代码,你可以在 JSX 中嵌入任何表达式。这也包括 JavaScript 中的逻辑与 (&&) 运算符。
1 | {unreadMessages.length > 0 && |
之所以能这样做,是因为在 JavaScript 中,true && expression
总是会返回 expression
, 而 false && expression
总是会返回 false
。
因此,如果条件是 true
,&&
右侧的元素就会被渲染,如果是 false
,React 会忽略并跳过它。
三目运算符
另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 condition ? true : false
。
1 | render() { |
同样的,它也可以用于较为复杂的表达式中,虽然看起来不是很直观:
1 | render() { |
就像在 JavaScript 中一样,你可以根据团队的习惯来选择可读性更高的代码风格。需要注意的是,如果条件变得过于复杂,那你应该考虑如何提取组件。
阻止组件渲染
在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render
方法直接返回 null
,而不进行任何渲染。
列表 & Key
渲染多个组件
可以通过使用 {}
在 JSX 内构建一个元素集合。
使用 Javascript 中的 map()
方法来遍历 numbers
数组。
1 | const numbers = [1, 2, 3, 4, 5]; |
把整个 listItems
插入到 <ul>
元素中,然后渲染进 DOM:
1 | ReactDOM.render( |
key
key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key:
1 | const todoItems = todos.map((todo) => |
当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key:
1 | const todoItems = todos.map((todo, index) => |
如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。
要是你有兴趣了解更多的话,这里有一篇文章深入解析为什么 key 是必须的可以参考。
用 key 提取组件
元素的 key 只有放在就近的数组上下文中才有意义。
key 只是在兄弟节点之间必须唯一
数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值。
key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key
属性的值,请用其他属性名显式传递这个值:
1 | const content = posts.map((post) => |
在 JSX 中嵌入 map()
JSX 允许在大括号中嵌入任何表达式,所以我们可以内联 map()
返回的结果:
1 | function NumberList(props) { |
表单
受控组件
在 HTML 中,表单元素(如<input>
、 <textarea>
和 <select>
)之类的表单元素通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()
来更新。
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
1 | class NameForm extends React.Component { |
由于在表单元素上设置了 value
属性,因此显示的值将始终为 this.state.value
,这使得 React 的 state 成为唯一数据源。由于 handlechange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
textarea 标签
在 React 中,<textarea>
使用 value
属性代替。这样,可以使得使用 <textarea>
的表单和使用单行 input 的表单非常类似:
1 | class EssayForm extends React.Component { |
select 标签
React 并不会使用 selected
属性,而是在根 select
标签上使用 value
属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。
1 | class FlavorForm extends React.Component { |
总的来说,这使得 <input type="text">
, <textarea>
和 <select>
之类的标签都非常相似—它们都接受一个 value
属性,你可以使用它来实现受控组件。
TIP
你可以将数组传递到
value
属性中,以支持在select
标签中选择多个选项:
1 <select multiple={true} value={['B', 'C']}>
文件 input 标签
在 HTML 中,<input type="file">
允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的 File API 进行控制。
1 | <input type="file" /> |
它的 value 只读,所以它是 React 中的一个非受控组件。
处理多个输入
当需要处理多个 input
元素时,我们可以给每个元素添加 name
属性,并让处理函数根据 event.target.name
的值选择要执行的操作。
1 | class Reservation extends React.Component { |
这里使用了 ES6 计算属性名称的语法更新给定输入名称对应的 state 值:
1 | this.setState({ |
受控输入空值
在受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value
,但输入仍可编辑,则可能是你意外地将value
设置为 undefined
或 null
。
受控组件的替代品
有时使用受控组件会很麻烦,因为你需要为数据变化的每种方式都编写事件处理函数,并通过一个 React 组件传递所有的输入 state。
当你将之前的代码库转换为 React 或将 React 应用程序与非 React 库集成时,这可能会令人厌烦。
在这些情况下,你可能希望使用非受控组件, 这是实现输入表单的另一种方式。
成熟的解决方案
如果你想寻找包含验证、追踪访问字段以及处理表单提交的完整解决方案,使用 Formik 是不错的选择。
状态提升
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。
1 | class TemperatureInput extends React.Component { |
1 | class Calculator extends React.Component { |
通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
虽然提升 state 方式比双向绑定方式需要编写更多的“样板”代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。
组合 vs 继承
React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。
包含关系
有些组件无法提前知晓它们子组件的具体内容。在 Sidebar
(侧边栏)和 Dialog
(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。
我们建议这些组件使用一个特殊的 children
prop 来将他们的子组件传递到渲染结果中:
1 | function FancyBorder(props) { |
这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递给它们。
1 | function WelcomeDialog() { |
<FancyBorder>
JSX 标签中的所有内容都会作为一个 children
prop 传递给 FancyBorder
组件。因为 FancyBorder
将 {props.children}
渲染在一个 <div>
中,被传递的这些子组件最终都会出现在输出结果中。
少数情况下,你可能需要在一个组件中预留出几个“洞”。
1 | function SplitPane(props) { |
那么继承呢?
在 Facebook,我们在成百上千个组件中使用 React。我们并没有发现需要使用继承来构建组件层次的情况。
Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。
如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。