4.2.3.4 框架与库 ——React
开始之前
因为 React 的官方文档写得并不好,所以我决定自己写一篇导读。确保大家阅读官方文档的时候不会有阅读障碍和自我怀疑。这里简要讲下 React 大致是什么东西,然后大家就可以去官方教程学习具体的用法,而不会被他们的叙述带偏。
React 给我们带来了什么
这是一个简单的 React 组件
const subject = "React";
function Component({ id }) {
const [count, setCount] = useState(0)
return <div>
<h1>Hello, {subject}, I am {id}!</h1>
<p>Counter: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increase</button>
<div>
}
2
3
4
5
6
7
8
9
React 给我们发明了一些新写法
- JSX: 一些看起来像是 HTML 和 JavaScript 的混合的神奇模板代码
- Functional Component: 看起来和写起来都很奇葩的组件,其实原来是很正常的 OOP 的组件的,新版改的,他们觉得高端
- Hooks: 一些匪夷所思的函数,用来处理一些跨单个函数生命周期的值和函数(等下会解释)
我们会依次解释这些东西
JSX 和 Functional Component
通过上面的示例我们可以看到 Functional Component 是一种特殊的函数。它在 JavaScript 看来就是一个普通的函数,但是 React 会把它挂载到自己的逻辑里面,按照自己的方式群运行这个函数。所以它会拥有比普通的 JS 函数拥有更多的功能,比如 JSX 和 Hooks。
JSX 是 React 自创的写法,原生的 JS 是没有这种写法的。所以我们一般使用 .jsx
后缀表示文件里面包含 JSX, 这样我们的构建工具(比如 Vite) 就可以认出来,并且把这些 JSX 翻译成纯 JS 代码。 虽然 JSX 看起来很抽象,但是写起来还算体验可以。
我们看看上面的代码的片段
return <div>
<h1>Hello, {subject}, I am {id}!</h1>
<p>Counter: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increase</button>
<div>
2
3
4
5
可以看到,JSX 是一系列的元素混合成的。包括:
- 组件,比如
<div>
<h1>
这种原生 HTML 组件。你也可以使用自己的组件,比如定义一个function Component() { return <p>Component</p>;} }
就可以用<Component />
组件了。 - JS 代码,使用一对
{
}
包裹。一般来说在一对标签中可以作为字符串传入,比如{subject}
就会被解析出里面的字符串值React
并将其渲染进去,成为Hello, React!
。如果在组件内部也可以作为prop
被传入比如<button>
的onClick
就就可以传入一个函数。然后这个按钮被点击的时候就会调用这个函数。我们这里演示的id
也是一个prop
。我们稍后会讲解。
这些东西看起来有点乱,但是习惯了感觉也还行。
Props 和 Hooks
这是 React 设计当中的原创部分。本来挺好用的,但是文档里面没讲清楚。
Props
这是组件声明的片段
function Component({ id })
我们可以看到这个 { id }
。它的意思是这个函数接受一个对象作为参数,对象包含了 id
这个成员。React 管这个 id
叫 props
。
一般来说 JS 里面调用函数的逻辑是这样的
Component({ id: 1 })
然而这样的方式直接写进代码,就不能表明这个函数是一个函数式组件,应该由 React 来处理并且调用,因此,我们会在 JSX 中使用一些另外的写法来表明这个函数是一个组件,并且由 React 管理。就像这样:
function App() {
return <Component id={1} />
}
2
3
这里我们使用 JSX 写法调用这个函数,将函数名直接作为组件名,将参数作为 prop
,这样 React 就可以管理这个函数,去干预这个函数的执行。我们接下来会提到生命周期。
如果 prop
接受字符串作为值,也可以直接写一个字符串作为常量传入,省掉 {}
。
function App() {
return <Component id="1" />
}
2
3
Hooks
React 的函数式组件是有生命周期的,在生命周期的不同部分会反复调用这个函数得到新的 JSX 结果,并将其渲染。而一个普通的函数,理想状态下它只是接受参数,并且返回一个返回值。但是有些值需要在不同的调用之间被存储下来。普通的函数可以使用全局变量。但是全局变量无法被 React 管理,这就需要引入特殊的管理机制,就是 Hooks 所解决的事情。
我们先从一个纯 JS 的函数开始,比如我想维护一个 getCounter
函数,每次返回值自增 1, 这在 JS 当中用一个全局变量就可以实现。
let counter = 0;
function getCounter() {
return coutner++;
}
2
3
4
这段函数可以在每次被调用的时候返回新的值。那么我们如果要在 React 当中实现一个按钮,点击的时候需要渲染的值加一,就需要这么写。
function Counter() {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(c => c + 1)}>{ counter }</button>
}
2
3
4
在这里,我们即将进入最难理解的部分,就是这个 useState
到底做了什么。
从字面上看,这个函数接受一个参数,返回两个值。接受的是初始值。返回的第一个值是对这个 state 的引用,第二个是修改 state 的用的函数,可以传入一个值或者一个接受当前值返回更新值的函数,来修改这个值。我们这里采用了传入函数的方式来使用这个 setCounter
函数,这样是为了避免一些异步问题(比如用户点击太快了点出 bug)。
但是!我们并不能理解这个 Hook 是为什么会在渲染时起效的,它为什么可以去被更新?为什么更新它这个新的值就渲染进来了呢?
这里我们就要进入生命周期的介绍了。这段介绍 React 官网根本没有,我找了半天找不到,不知道他们是不是觉得这个是常识。
React 组件的生命周期分为三个阶段:
- 挂载阶段:在挂载阶段,函数被调用,初始化组件,得到 JSX 返回值,并添加到 DOM 中。React 开始追踪组件的
prop
和state
变化。 - 更新阶段:如果函数的
prop
或者state
发生变化,用新的prop
或者state
替换原来的值,重新调用函数,得到新的 JSX, 替换 DOM 中旧的结果。 - 卸载阶段:如果调用这个组件的父组件(在 JSX 中用了这个组件的组件),清理组件的值。
我们可以看到,同一个函数会出现在生命周期的两个地方被调用了,而这些给函数的上下文是不一样的,这是新手混淆的根源。拿我们这个 Counter 来说。在打开网页的时候被挂载,每次按一下按钮就更新。这里,第一次调用的时候, useState
的第一个返回值是 0
,就是我们给定的初始值。而后续调用的时候,则是拿到最近一次用 setCounter
函数设置的最新值。因为这个函数是 React 提供的库函数,它可以很好地处理这些值之间的更新关系,让这些值能够在函数外面保存。
所以我们知道了 useState
这个 Hook 是用来管理那些需要活的比这个函数长的值的。因为函数内部的所有变量都是调用的时候创建,返回的时候销毁的,在普通的函数中我们一般用全局变量处理这些值。但是 React 不会管你这个全局变量,无法做到自动触发重新渲染等操作,所以我们在这里使用 React 提供的 state
。理解了这一点我们就知道不是所有 JSX 当中用到的值都需要 state
的。因为你不会写出这种 getCounter
:
let counter = 0;
let counter2 = 0;
function getCounter() {
coutner2 = ++coutner;
return coutner2;
}
2
3
4
5
6
所以也请不要写这样的代码
function Counter() {
const [counter, setCounter] = useState(0);
const [counter2, setCounter2] = useState(0);
useEffect(() => {
setCounter2(counter + 1);
}, [counter])
return <button onClick={() => setCounter(c => c + 1)}>{ counter2 }</button>
}
2
3
4
5
6
7
8
关于 useEffect
的生命周期我想应该也能够理解。 useEffect
在接受 []
作为数据依赖的时候,相当于一个在挂载阶段被执行的函数, 如果给 useEffect
的函数有返回函数,它就会在卸载的时候被执行。
当有实际的 state 作为依赖,则会在 state 变化的时候调用这个函数。如果给 useEffect
的函数有返回函数,它就会在数据依赖下一次变化时先被执行,再执行这个函数。
入门
请阅读并完成页面中的练习:
顺便提一下,关于副作用的介绍也需要阅读,避免写出 bug: React: 保持组件纯粹
以及我们需要使用树形组织方式: React: 将 UI 视为树
TypeScript in React
在 React 中使用 TypeScript 可以参考 React: 使用 TypeScript 中提到的写法。
当然也还有一个邪门的用法:在 VSCode 中是自带错误检查支持的,在标红的地方加上 Type 标注消去红色标出的 Error 即可,这种方法也挺好用。
Hooks
::: tips 如果看不懂关于 Hooks 的描述(这很正常,因为他们的蹩脚函数式设计),可以去看看一些其他写得更加人类的教程,理解原理之后再去阅读官方教程,比如:
- React Lifecycle Methods and Hooks – a Beginner's Guide (英文) 这篇教程从 Class Component 开始讲解生命周期,并将其迁移到 Functional Component 上面,从而利用更简单更拟人的 OOP 的思想理解了 React 的蹩脚函数式 “现代” 设计。 :::
钩子让开发者能够在函数组件中使用状态和生命周期相关的功能。React 当中会提供一些钩子 用来处理不同的需求。最常用的就是 useState
useEffect
和 useRef
。
我们在这里给出简单的介绍,确保大家在有需求的时候可以快速找到自己需要的用法,详细的可以查阅各部分的 React 官方文档。
useState
useState
是最常用的 Hook,它允许你在函数组件中引入状态。 useState
返回一个数组,包含当前状态值和用于更新状态的函数。
注意 useState
和 React 的渲染机制绑定。你需要阅读下面的文章来确保理解 state 的工作方式以及使用规范:
另外我们在 state 中使用数组和对象的时候,如果需要更新 state, 我们会用新的对象的复制,而不是在原来的对象上更改。
对于数组和对象,更新时记住用拷贝而不是修改原来的对象,否则不会触发更新。我们更推荐用 Immer 处理这样的情况,因为修改嵌套对象很麻烦。我们已经使用 npm 安装过了,所以这里给出示例:
import { useState } from 'react';
import { produce } from 'immer'; // 用于处理嵌套对象和数组的更新
function ShoppingList() {
// 初始化一个对象状态,包含名字和商品数组
const [list, setList] = useState({
name: 'My Shopping List',
items: ['Apples', 'Bananas'],
});
// 添加商品到购物列表
const addItem = (item) => {
setList((prevList) => produce(prevList, (draft) => {
draft.items.push(item); // 使用 Immer 进行不可变更新
}));
};
// 更新购物列表的名字
const updateListName = (newName) => {
setList((prevList) => produce(prevList, (draft) => {
draft.name = newName;
}));
};
return (
<div>
<h1>{list.name}</h1>
<ul>
{list.items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<button onClick={() => addItem('Oranges')}>Add Oranges</button>
<button onClick={() => updateListName('Grocery List')}>Rename List</button>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
如果你还是没有理解,可以在遇到问题的时候查阅 React: 添加交互 章节中的其他教程,确保理解并掌握。
useEffect
useEffect
允许你在组件渲染后执行副作用,例如数据获取、订阅或手动修改 DOM。具体的使用需要阅读: React: 使用 Effect 进行同步
示例:
import { useEffect, useState } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
// 模拟数据获取,很多人会这么写,但是非常不推荐这样用:(想一想,为什么?提示:加上加载显示?错误处理?)
fetch('https://api.example.com/data')
.then(response => response.json())
.then(result => setData(result));
// 清理副作用(可选)
return () => {
console.log('Cleanup'); // 这段代码会被执行两次,如果你还不清楚原因,请重新阅读 [React: 使用 Effect 进行同步]
};
}, []); // 空数组作为依赖项意味着该副作用只在组件挂载和卸载时执行,你也可以添加变量来监听更改触发
return (
<div>
<p>Fetched data: {JSON.stringify(data)}</p>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对于示例中提到的获取数据,我们更推荐使用 React Query 中的 useQuery
钩子,因为这样能更好地实现 query 逻辑的解耦,也因为它不会 fetch 两次。
useRef
useRef
允许你访问 DOM 元素或存储任意可变值,而无需重新渲染组件。它通常用于访问 DOM 元素或保持某些跨渲染周期不变的值。一般来说以下用法完全够用。
示例:
import { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 在组件挂载后让输入框自动获得焦点
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
2
3
4
5
6
7
8
9
10
11
12
useMemo
和 useCallback
useMemo
和 useCallback
主要用于性能优化。 useMemo
返回一个计算值的缓存,而 useCallback
则缓存一个回调函数,避免不必要的重新计算和渲染。
useMemo
示例:
import { useMemo } from 'react';
function ExpensiveComputationComponent({ number }) {
const computedValue = useMemo(() => {
return number * 2; // 假设这是一个昂贵的计算
}, [number]);
return <div>Computed Value: {computedValue}</div>;
}
2
3
4
5
6
7
8
9
useCallback
示例:
import { useCallback } from 'react';
function Button({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return <Button onClick={handleClick} />;
}
2
3
4
5
6
7
8
9
10
11
12
13