代码下载
React官网已经都是函数式组件文档,没有类组件文档,但是还是支持类组件这种写法。
UI 描述
组件
组件 是 React 的核心概念之一,它们是构建用户界面(UI)的基础。React 允许你将标签、CSS 和 JavaScript 组合成自定义"组件",即 应用程序中可复用的 UI 元素。
React 最为重视交互性且使用了相同的处理方式:React 组件是一段可以 使用标签进行扩展 的 JavaScript 函数。
组件可以渲染其他组件,但是 请不要嵌套他们的定义。当子组件需要使用父组件的数据时,你需要 通过 props 的形式进行传递,而不是嵌套定义。
JSX
每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。React 组件使用一种被称为 JSX 的语法扩展来描述这些标签。JSX 看起来和 HTML 很像,但它的语法更加严格并且可以动态展示信息。了解这些区别最好的方式就是将一些 HTML 标签转化为 JSX 标签。
JSX and React 是相互独立的 东西。但它们经常一起使用,但你 可以 单独使用它们中的任意一个,JSX 是一种语法扩展,而 React 则是一个 JavaScript 的库。
JSX 规则
- 只能返回一个根元素,如果想要在一个组件中包含多个元素,需要用一个父标签把它们包裹起来,如果你不想在标签中增加一个额外的
<div>
,可以用<>
和</>
元素来代替。 - 标签必须闭合,JSX 要求标签必须正确闭合。像
<img>
这样的自闭合标签必须书写成<img />
,而像<li>oranges
这样只有开始标签的元素必须带有闭合标签,需要改为<li>oranges</li>
。 - 使用驼峰式命名法给 所有 大部分属性命名! JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对。但 JavaScript 对变量的命名有限制。例如,变量名称不能包含
-
符号或者像class
这样的保留字。
由于历史原因,aria-* 和 data-* 属性是以带 - 符号的 HTML 格式书写的。
转化器 将 HTML 和 SVG 标签转化为 JSX。
在 JSX 中通过大括号使用 JavaScript
在 JSX 中,只能在以下两种场景中使用大括号:
- 用作 JSX 标签内的文本:
<h1>{name}'s To Do List</h1>
是有效的,但是<{tag}>Gregorio Y. Zara's To Do List</{tag}>
无效。 - 用作紧跟在 = 符号后的 属性:
src={avatar}
会读取 avatar 变量,但是src="{avatar}"
只会传一个字符串{avatar}
。
props
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。可以通过它们传递任何 JavaScript 值,包括对象、数组和函数。
可以通过解构语法来读取 props 的值,也可以通过在参数后面写 = 和默认值来进行解构:
function Avatar({ person, size = 100 }) {
// ...
}
一些组件将它们所有的 props 转发给子组件,因为这些组件不直接使用他们本身的任何 props,所以使用更简洁的"展开"语法是有意义的:
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}
条件渲染
在一些情况下,不想有任何东西进行渲染,可以直接返回 null。
当 JavaScript && 表达式
的左侧(我们的条件)为 true 时,它则返回其右侧的值(在我们的例子里是勾选符号)。但条件的结果是 false,则整个表达式会变成 false。在 JSX 里,React 会将 false 视为一个"空值",就像 null 或者 undefined,这样 React 就不会在这里进行任何渲染。
列表渲染 key
必须给数组中的每一项都指定一个 key------它可以是字符串或数字的形式,只要能唯一标识出各个数组项就行。key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要。一个合适的 key 可以帮助 React 推断发生了什么,从而得以正确地更新 DOM 树。
不同来源的数据往往对应不同的 key 值获取方式:
- 来自数据库的数据: 如果你的数据是从数据库中获取的,那你可以直接使用数据表中的主键,因为它们天然具有唯一性。
- 本地产生数据: 如果你数据的产生和保存都在本地(例如笔记软件里的笔记),那么你可以使用一个自增计数器或者一个类似 uuid 的库来生成 key。
key 需要满足的条件:
- key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。
- key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 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 传给组件:<Profile key={id} userId={id} />
。
保持组件纯粹
在计算机科学中(尤其是函数式编程的世界中),纯函数 通常具有如下特征:
- 只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。
- 输入相同,则输出相同。给定相同的输入,纯函数应总是返回相同的结果。
在渲染过程中,组件改变了 预先存在的 变量的值,将这种现象称为 突变(mutation) 。纯函数不会改变函数作用域外的变量、或在函数调用前创建的对象------这会使函数变得不纯粹!
React 的渲染过程必须自始至终是纯粹的。组件应该只 返回 它们的 JSX,而不 改变 在渲染前,就已存在的任何对象或变量 --- 这将会使它们变得不纯粹!但是可以在渲染时更改刚刚 创建的变量和对象:
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaGathering() {
let cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}
每次渲染时都是在 TeaGathering 函数内部创建的它们。TeaGathering 之外的代码并不会知道发生了什么。这就被称为 "局部 mutation" --- 如同藏在组件里的小秘密。
函数式编程在很大程度上依赖于纯函数,但 某些事物 在特定情况下不得不发生改变这些变动包括更新屏幕、启动动画、更改数据等,它们被称为 副作用。它们是 "额外" 发生的事情,与渲染过程无关。在 React 中,副作用通常属于 事件处理程序。事件处理程序是 React 在执行某些操作(如单击按钮)时运行的函数。即使事件处理程序是在你的组件 内部 定义的,它们也不会在渲染期间运行! 因此事件处理程序无需是纯函数。
将 UI 视为树
React 以及许多其他 UI 库,将 UI 建模为树。将应用程序视为树对于理解组件之间的关系以及调试性能和状态管理等未来将会遇到的一些概念非常有用。树是项目和 UI 之间的关系模型,通常使用树结构来表示 UI。
渲染树表示 React 应用程序的单个渲染过程。在 条件渲染 中,父组件可以根据传递的数据渲染不同的子组件。尽管渲染树可能在不同的渲染过程中有所不同,但通常这些树有助于识别 React 应用程序中的顶级和叶子组件。顶级组件是离根组件最近的组件,它们影响其下所有组件的渲染性能,通常包含最多复杂性。叶子组件位于树的底部,没有子组件,通常会频繁重新渲染。识别这些组件类别有助于理解应用程序的数据流和性能。
模块依赖树
在 React 应用程序中,可以使用树来建模的另一个关系是应用程序的模块依赖关系。当 拆分组件 和逻辑到不同的文件中时,就创建了 JavaScript 模块,在这些模块中可以导出组件、函数或常量。模块依赖树中的每个节点都是一个模块,每个分支代表该模块中的 import 语句。
与同一应用程序的渲染树相比,存在相似的结构,但也有一些显著的差异:
- 构成树的节点代表模块,而不是组件。
- 非组件模块,在这个树中也有所体现。渲染树仅封装组件。
- 组件可以 接受 JSX 作为 children props,因此它将 其他组件 作为子组件渲染,但不导入该模块。
在为生产环境构建 React 应用程序时,通常会有一个构建步骤,该步骤将捆绑所有必要的 JavaScript 以供客户端使用。负责此操作的工具称为 bundler(捆绑器),并且 bundler 将使用依赖树来确定应包含哪些模块。随着应用程序的增长,捆绑包大小通常也会增加。大型捆绑包大小对于客户端来说下载和运行成本高昂,并延迟 UI 绘制的时间。了解应用程序的依赖树可能有助于调试这些问题。
交互
useState
在 React 中,useState 以及任何其他以"use"开头的函数都被称为 Hook。Hook 是特殊的函数,只在 React 渲染时有效。它们能让你 "hook" 到不同的 React 特性中去。
Hooks ------以 use 开头的函数------只能在组件或自定义 Hook 的最顶层调用。 不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 "use" React 特性,类似于在文件顶部"导入"模块。
State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
Hook 是能让你的组件使用 React 功能的特殊函数(状态是这些功能之一)。useState Hook 让你声明一个状态变量。它接收初始状态并返回一对值:当前状态,以及一个让你更新状态的设置函数。
function StateHook() {
const [index, setIndex] = React.useState(0)
return (<>
<p>index: {index}</p>
<button onClick={() => {
setIndex(index + 1)
console.log('index: ', index);
}}>+1</button>
</>)
}
ReactDOM.createRoot(document.getElementById('stateHook')).render(<StateHook></StateHook>)
状态的行为更像一个快照。设置它并不改变已有的状态变量,而是触发一次重新渲染。可以通过在设置状态时传递一个 更新器函数 来解决这个问题,在需要排队进行多次状态更新,那么这非常方便:
setIndex(index + 1)
setIndex(i => i + 1)
setIndex((i, p) => {
// 不支持第二个参数 p
console.log('i: ', i, 'p: ', p);
return i + 1
// 不支持 更新后的回调函数
}, () => console.log('i: ', index))
注意:与类组件 setState 不同的是 useState 更新器函数只有一个参数就是最新的状态值,并且不支持更新回调函数。类组件的 setState 是异步更新数据而 useState 状态的行为更像一个快照,通过延时函数就可以看出他们的差别,一个 useState 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的:
class StateCom extends React.Component {
state = { index: 0 }
render() {
return (<>
<p>index: {this.state.index}</p>
<button onClick={() => {
this.setState({index: this.state.index + 1})
setTimeout(() => {
// 输出 1
console.log('index: ', this.state.index);
}, 2000);
}}>+1</button>
</>)
}
}
function UseStateCom() {
const [i, setIndex] = useState(0)
return (<>
<p>index: {i}</p>
<button onClick={() => {
setIndex(i + 1)
setTimeout(() => {
// 输出 0
console.log('index: ', i);
}, 2000);
}}>+1</button>
</>)
}
严格来说 React state 中存放的对象是可变的,但你应该像处理数字、布尔值、字符串一样将它们视为不可变的,因此应该替换它们的值,而不是对它们进行修改。数组只是另一种对象。同对象一样,需要将 React state 中的数组视为只读的。这意味着你不应该使用类似于 arr[0] = 'bird' 这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如 push() 和 pop()。