React 的组件

React 的核心概念就是组件。组件像是将 UI 拆分得到的独立的可重复使用的小模块,其能够接收属性的传入,并返回描述屏幕上展示内容的 React 元素。React 支持两种组件,一种是类组件,一种是函数组件。但是目前 React 推荐使用函数组件,官方文档也是以函数组件为主,并且函数组件比类组件更加简洁,代码量更少,尤其是在引入 Hooks 之后,避免了类组件中使用生命周期方法的繁琐。因此这个文档也只讲函数组件,不讲类组件

自定义组件

既然组件是独立可复用的,那我们肯定可以编写自己的组件并使用

src 文件夹下新建 Square.tsx 文件,并写入以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";

const Square = () => {
return (
<div
style={{
width: 100,
height: 100,
backgroundColor: "red",
}}
></div>
);
};

export default Square;

这样我们就自己编写了一个 100×100 的红色正方形组件,并用 export 关键字导出了这个组件,以便其他组件进行导入

接下来我们在 App.tsx 中实例化这个正方形组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";
import Square from "./Square.tsx";

const App: React.FC = () => {
return (
<>
<h1>Hello, React!</h1>
<Square></Square>
</>
);
};

export default App;

可以看到,实例化一个组件也是简单的,类似 HTML 的标签语法,下面都是合法的实例化 Square 组件的语句:

1
2
<Square />
<Square></Square>

我们按下 Ctrl + S 保存,React 就会自动热加载,无需进行刷新即可看到我们页面最新的变化:

image-20240127193617960

组件的状态

假设现在我们要完成这样一个任务:把上面的正方形变成能自由修改边长和颜色的

这里我们就需要通过传入属性的方式对组件进行控制,正如 html 的标签的属性

1
<Square size={150} color="green"></Square>

React 允许我们把这些属性以对象的形式捕获,也就是传入的这两个属性参数对象的两个成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Square.tsx
import React from "react";

const Square = (props) => {
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: props.color,
}}
></div>
);
};

export default Square;

按下 Ctrl + S 保存,页面确实变成了我们想要的样子

image-20240128170443924

但是我们会发现编译器报了一个警告:参数 "props" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。

根据 TypeScript 哲学,我们应该指定这个参数的类型,这样可以有效避免出现难以探查的 undefined 的问题。指定的方式是 interface,你需要在这个接口之中定义这个组件接受的所有属性。比如说 Square 只需要接受尺寸和颜色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Square.tsx
import React from "react";

interface SquareProps {
size: number;
color: string;
}

const Square = (props: SquareProps) => {
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: props.color,
}}
></div>
);
};

export default Square;

这里也可以指定部分属性是可选的(使用 ?: ),这样的话实例化组件就没必要传入这一部分属性,通过 props 访问这些属性会得到 undefined

组件的动态

假设现在我们要完成这样一个任务:正方形如果是红色,单击一下就变成绿色;正方形如果是绿色,单击一下就变成红色

为了实现动态的效果,需要让组件具有一定的记忆功能,让组件管理自己的状态,这就是 state

我们先考虑这样更改 Square.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
let color: string = "red";
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
color === "red" ? (color = "green") : (color = "red");
}}
></div>
);
};

export default Square;

诶?怎么我怎么点这个正方形颜色都不会变呢

onClick 方法传入的回调函数更新的是局部变量 color,但是局部变量无法在多次渲染中持续保存,也就是说,当我们按下 Ctrl + S 保存使得页面重新渲染时,color 又被重新初始化为 'red',它不会考虑之前对局部变量的任何修改。并且,局部变量的修改不会触发重新渲染,React 并不知道需要使用新的数据重新渲染页面。这两个原因导致我们的页面没有达到预期的效果

要使用新数据更新组件,就需要:保留渲染前后的数据触发重新渲染。巧了,useState 这个 Hook 函数提供了这个功能。

React Hooks 是 React 提供的一类函数的统称,用于在函数组件中添加状态、生命周期和其他 React 特性,而无需编写类组件。使用 Hooks,你可以在函数组件中使用状态和其他 React 特性,使其具有类组件的能力。(如果想要理解什么是生命周期等 React 特性,可以去稍微学一下类组件。正是因为 React 提供了这些 Hook 函数,才使得函数式组件如此的简洁)

我们先给一下用 useState 修改后的Square.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState } from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
const [color, setColor] = useState("red");
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
setColor(color === "red" ? "green" : "red");
}}
></div>
);
};

export default Square;

useState 这个函数返回一个包含两个元素的数组。数组的第一个元素是状态变量state),而第二个元素是用于更新状态的函数setState)。我们习惯性的将更新状态的函数使用小驼峰命名法命名为 setXxx,其中 xxx 是状态变量名。useState 传入的参数为状态变量的初始值

更新状态的函数接受一个新的值,当调用 setState 时,会使得状态变量被赋予新的值,同时 React 会重新渲染组件

对于初学者的建议是,不必深究 useState 是如何实现的,只需要照葫芦画瓢,知道他何时使用和如何使用

当你调用 useState 时,你是在告诉 React 你想让这个组件记住一些东西。在这个文档的例子中,Square 这个组件需要记住的就是 color

为什么这些函数会被叫做 Hook?因为它们正像钩子一样,允许你不编写componentDidMountcomponentWillUnmount 等生命周期方法就“钩入” React 的特性。在 React 中,useState 以及任何其他以 use 开头的函数都被称为 Hook。State 只是这些特性中的一个,你之后还会遇到其他 Hook

==注意==setState并不是即时更新的

这是什么意思呢,我们现在考虑这样一个需求:在正方形中展示出已经点击的次数

那我们就需要用 useState 来另定义一个状态,这个状态展示了我们的点击次数:

1
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
import React, { useState } from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
const [color, setColor] = useState("red");
const [clickCount, setClickCount] = useState(0);
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
setColor(color === "red" ? "green" : "red");
setClickCount(clickCount + 1);
}}
>
{clickCount}
</div>
);
};

export default Square;

image-20240128195400009

需求已经达成了,每次点击显示的数字都会 + 1

那我们再修改一下代码,我们让 onClick 方法里的 setClickCount方法调用两次:

1
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
import React, { useState } from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
const [color, setColor] = useState("red");
const [clickCount, setClickCount] = useState(0);
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
setColor(color === "red" ? "green" : "red");
setClickCount(clickCount + 1);
setClickCount(clickCount + 1);
}}
>
{clickCount}
</div>
);
};

export default Square;

那按照我们正常人的思路就会想, setClickCount 方法调用两次,点击一次正方形数字就会 + 2。其实不然,即使你把 setClickCount方法调用一万次也是只 + 1

以下是这个正方形的点击事件处理函数通知 React 要做的事情:

1、setColor(color === 'red' ? 'green' : 'red'):判断 color 的值,React 准备在下一次渲染时更改 color

2、setClickCount(clickCount + 1)clickCount 是 0 所以 setClickCount(0 + 1),React 准备在下一次渲染时clickCount 更改为 1

3、setClickCount(clickCount + 1)clickCount 是 0 所以 setClickCount(0 + 1),React 准备在下一次渲染时clickCount 更改为 1

尽管调用了两次 setClickCount,但在这次渲染的事件处理函数中 clickCount 一直是 0 ,所以 React 会连续两次clickCount 置 1

这也就警告我们,不要在 setClickCount方法执行之后立刻去访问 clickCount,很有可能他的值与你预想的不同

那如果我们就是想连续的调用 setClickCount,就是想让他连续的 + 2,我们可以在 setClickCount 中传入回调函数:

1
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
import React, { useState } from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
const [color, setColor] = useState("red");
const [clickCount, setClickCount] = useState(0);
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
setColor(color === "red" ? "green" : "red");
setClickCount((o) => o + 1);
setClickCount((o) => o + 1);
}}
>
{clickCount}
</div>
);
};

export default Square;

setState 允许我们传入一个回调函数,在状态更新完成并且组件重新渲染后会被调用。这个回调函数接收一个参数,是当前 State 的值,返回值就是 State 的新的要更新的值

这里还有另外一个影响因素需要讨论,React 会等到事件处理函数中的所有代码都运行完毕再处理你的 State 更新,这就是为什么重新渲染只会发生在所有这些 setClickCount() 调用之后的原因

当你传递一个回调函数给 setState 时,React 会将此函数加入一个队列,以便在事件处理函数中的所有其他代码运行后进行处理。之后在下一次渲染期间,React 会遍历队列并给你更新之后的最终 State。

以下是这个正方形的点击事件处理函数通知 React 要做的事情:

1、setColor(color === 'red' ? 'green' : 'red'):判断 color 的值,React 准备在下一次渲染时更改 color

2、setClickCount(o => o + 1)o => o + 1 是一个函数,React 将它加入队列

3、setClickCount(o => o + 1)o => o + 1 是一个函数,React 将它加入队列

当你在下次渲染期间调用 useState 时,React 会遍历队列。之前的 clickCount 的值是 0 ,所以这就是 React 作为参数 o 传递给第一个更新函数的值。然后 React 会获取你上一个更新函数的返回值,并将其作为 o 传递给下一个更新函数,以此类推

React 会保存 2 为最终结果并从 useState 中返回

如果我们把传入更新值和传入回调函数混着用会怎么样:

1
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
import React, { useState } from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
const [color, setColor] = useState("red");
const [clickCount, setClickCount] = useState(0);
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
setColor(color === "red" ? "green" : "red");
setClickCount(clickCount + 1);
setClickCount((o) => o + 1);
}}
>
{clickCount}
</div>
);
};

export default Square;

1、setColor(color === 'red' ? 'green' : 'red'):判断 color 的值,React 准备在下一次渲染时更改 color

2、setClickCount(clickCount + 1)clickCount 是 0 所以 setClickCount(0 + 1),React 将 “替换为 1 ” 添加到其队列中

3、setClickCount(o => o + 1)o => o + 1 是一个函数,React 将它加入队列

React 会保存 2 为最终结果并从 useState 中返回

这告诉我们,setState(x) 实际上会像 setState(n => x) 一样运行,只是没有使用 n

如果我们调换 setClickCount(clickCount + 1)setClickCount(o => o + 1) 的执行顺序:

1、setColor(color === 'red' ? 'green' : 'red'):判断 color 的值,React 准备在下一次渲染时更改 color

2、setClickCount(o => o + 1)o => o + 1 是一个函数,React 将它加入队列

3、setClickCount(clickCount + 1)clickCount 是 0 所以 setClickCount(0 + 1),React 将 “替换为 1 ” 添加到其队列中

React 会保存 1 为最终结果并从 useState 中返回

瀑布数据流与反向数据流

瀑布数据流指数据的传递只能自上而下,从父组件传递到子组件。比如在“组件的状态”的例子中,App 是父组件,Square 是子组件,父组件通过 props 的方式将 sizecolor 传递给子组件,而子组件不能向父组件传递数据

但是在开发中我们不可避免的会需要父组件获取到子组件的数据,这就称作反向数据流

比如我们需要在正方形的下面显示一句话:点击了正方形 clickCount 次

这时候就需要修改 App.tsx,让他获取到子组件中的 clickCount

根本获取不了!父组件不能得到子组件的数据!

解决方案是将 [clickCount, setClickCount] 的声明从子组件提升到父组件,并通过 props 的方式传递下去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Square.tsx
import React from "react";

interface SquareProps {
addCount: () => void;
size: number;
color: string;
}

const Square = (props: SquareProps) => {
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: props.color,
}}
onClick={props.addCount}
></div>
);
};

export default Square;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// App.tsx
import React, { useState } from "react";
import Square from "./Square.tsx";

const App: React.FC = () => {
const [clickCount, setClickCount] = useState(0);
return (
<>
<h1>Hello, React!</h1>
<Square
size={150}
color={"red"}
addCount={() => setClickCount((o) => o + 1)}
></Square>
<p>点击了正方形 {clickCount} 次</p>
</>
);
};

export default App;

image-20240129173237171

所谓的子组件控制父组件,并没有违反瀑布数据流,本质是父组件通过 propsState 的控制权下放给子组件

组件的副作用

什么是副作用

定义:除了主作用以外的作用都叫副作用

但是确实是这样的,函数组件的主作用是渲染组件,所有其他的操作都归类于副作用,比如发送网络请求,从网络中获取数据,监听窗口的大小等。React 通过useEffect这个 Hook 函数来处理组件的副作用

处理副作用

现在有这样一个需求:修改页面的标题为 点击了正方形 clickCount 次

修改页面标题不属于渲染,因此需要用 useEffect 实现这个功能

useEffect 接受两个参数,第一个参数是一个回调函数,函数体内编写实现副作用相关代码;第二个参数是可选的,作用是告诉 useEffect 什么时候执行回调函数

useEffect 的返回值是 undefined

1
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
// Square.tsx
import React, { useEffect, useState } from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
const [color, setColor] = useState("red");
const [clickCount, setClickCount] = useState(0);
useEffect(() => {
document.title = `点击了正方形 ${clickCount} 次`;
});
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
setColor(color === "red" ? "green" : "red");
setClickCount(clickCount + 1);
}}
>
{clickCount}
</div>
);
};

export default Square;

image-20240129155849468

这里我们没有传第二个参数,这表示每一次渲染都会执行副作用

useEffect 的第二个参数是依赖列表,是一个数组,例如 [dep1, dep2, dep3],这表示当依赖列表中变量发生改变的时候执行副作用

如果依赖列表是空列表([]),那么只在第一次渲染时执行副作用(多用于组件初始化的时候从服务器拉取数据)

副作用的清除

我们在 useEffect Hooks 内规定副作用清除逻辑的方法为在回调函数内返回一个函数,在返回的函数内规定副作用清除方式

比方说我们要让这个正方形定时变色:

1
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
35
36
// Square.tsx
import React, { useEffect, useState } from "react";

interface SquareProps {
size: number;
}

const Square = (props: SquareProps) => {
let timer: any;
const [color, setColor] = useState("red");
const [clickCount, setClickCount] = useState(0);
const switchColor = () => {
setColor(color === "red" ? "green" : "red");
};
useEffect(() => {
document.title = `点击了正方形 ${clickCount} 次`;
timer = setInterval(switchColor, 1000);
return () => clearInterval(timer);
});
return (
<div
style={{
width: props.size,
height: props.size,
backgroundColor: color,
}}
onClick={() => {
setClickCount(clickCount + 1);
}}
>
{clickCount}
</div>
);
};

export default Square;

为了使内存更安全,我们在使用 setInterval 之后应使用 clearInterval 来停止定时器,防止继续执行周期性的代码

在上面的例子中,在 useEffect 中第一个参数的回调函数的返回的回调函数,在两种情况下会被调用

1、如果 useEffect 有依赖列表,那么会在依赖项发生变化时调用

2、如果 useEffect 没有依赖列表,或者依赖列表为空,则会在这个组件被清理时调用

编码上建议将副作用拆开,一个副作用对应一个副作用的清除

结语

至此,你已经知道了什么是 React Hooks,什么是 React的组件,如何编写 React的组件,成功走入了 React 的世界。由于 React 的官方文档目前已经非常完备,本博客将不会继续更新后续 React 的内容,如有学习兴趣请自行阅读 React 官方文档。如果前面所讲的东西你已经掌握,可以从 迁移状态逻辑至 Reducer 中 开始学习,跳过前面的部分