react学习记录

很早之前有学过一点简单的前端,主要就是原生的 js 和 html 那些玩意儿,但是我这条懒狗也没坚持下去。。。趁着暑假,我得高低整点能看的玩意儿出来,不然学了小一年的计算机连个界面都搓不出来也太挫了。

原先定的计划是搞点 vue,但是捏,模板语法看着确实有点奇怪,而且 react 函数式的写法看起来有点意思,所以就决定是你了,react!

The Basis

环境准备

工欲善其事,必先利其器

              --《论语·卫灵公》

毕竟现在已经是 2202 年了,项目开发一般都会有专门的脚手架来配置,这里我就采用了 pnpm + vite 的配置。

pnpm 是个新的包管理器,但是它有很多的创新之处,所以节省空间还比较快。vite 也是一样的道理,这个新的打包工具构建速度非常的快,比 webpack 快到不知道哪里去了 🤓。

编辑器我选择的是 vscode,以下为需要安装的插件

  1. ES7+ React/Redux/React-Native/JS snippets 这个插件提供了 react 相关的补全
  2. vite 用来搭配 vite 项目使用
  3. eslint 提供针对 js/ts 的静态检查
  4. prettier 代码格式化工具

首先来创建项目

1
pnpm create vite

项目的配置很简单,遵循指示选择后回车即可。

项目配置

大致效果如下

浏览器演示画面

顺带一提,vite 插件也提供了在 vscode 中的实时预览

vscode中的预览

基础的函数组件和 jsx

jsx 是 react 最有趣的特性之一,通过 jsx ,可以很轻松的将视图和逻辑绑定到一起。换言之,就是不用再去单独处理 html 和 css 了,基本上所有的东西都可以扔到 jsx 里面来写。

根节点和实际的 dom 树

当我们通过 react 去创建组件时,实例化的组件必定会在 dom 树上有所体现。比如一个 button 组件可能就是一个套着 div 并且绑定了相关回调函数的 button (原生)组件。所以为了将我们写的组件渲染到浏览器中,react 必须将我们的组件通过某种方式挂载到入口的 html 文件中。

在这个项目里,react 则是创建了一个根组件,并将我们创建的其他组件在根组件中组合,最后将根组件挂载到 html 文件上。

这点从项目中 index.html 和 main.jsx 的文件内容也可以看出。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

main.jsx

1
2
3
4
5
6
7
8
9
10
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

显然,这里 react 是通过 getElementByID 这个方法获取了 index.html 中 id 为 root 的 dom 元素,并在其中渲染了 <App /> 这个组件。

App.jsx

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
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import "./App.css";

function App() {
const [count, setCount] = useState(0);

return (
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
);
}

export default App;

而在 App.jsx 中,则是通过 App 函数,返回了一段 jsx,这里面就是实际上描述页面的内容。

这里稍微岔开下话题,React 中通过首字母是否为大写来判断一个函数是组件还是普通函数,所以这里的 App() 函数首字母是大写的。

一切的开始,第一个组件 && Hello, World!

当然对于我们这样的萌新而言,脚手架里提供的代码也是有点难懂。。。为了学习起见,我们需要对项目中的代码做一些调整。

具体的调整如下:

  1. src 目录下仅保留 main.jsx 和 App.jsx ,其余文件全部删除。

  2. 删除 main.jsx 中的不需要的 import 语句,直至代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import React from "react";
    import ReactDOM from "react-dom/client";
    import App from "./App";

    ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
    <App />
    </React.StrictMode>
    );
  3. 删除 App.jsx 中的大部分语句,直至代码如下。

    1
    2
    3
    4
    5
    function App() {
    return <div className="App"></div>;
    }

    export default App;

之后我们所有的操作就只在 App.jsx 中进行,方便管理。当然实际的项目开发并不会像我们这样乱搞,大概率是会将不同的组件拆分到不同的文件中,不然想想看单文件几万行的屎山项目,对于开发者来说简直就是人间地狱。

OK,我们现在来写我们的第一个组件,我们希望这个组件应该能够渲染一段文本,其内容为 “Hello, World!”。

参考代码如下

1
2
3
function Hello() {
return <div>Hello, World!</div>;
}

然后我们在 App() 组件中将其实例化。

1
2
3
4
5
6
7
function App() {
return (
<div className="App">
<Hello></Hello>
</div>
);
}

渲染效果如图

简单组件

这里有个有趣的地方,当我们创建了一个组件并想要在其他地方的 jsx 里面使用的时候,既可以写成自闭合标签,也可以写成普通的标签。

也就是说,既可以写成 <Hello></Hello>, 也可以写成 <Hello/> 。

组件传参,顺带渲染点别的

一个只能渲染 "Hello, World!" 的组件是没有前途的。至少得让他渲染点别的,至少得让他根据我们传入的参数渲染点什么出来。

这里整个简单的例子,就让我们的组件渲染一段根据参数拼接的文字,就在传入的参数(字符串)尾部加上 “,我好喜欢你啊”,比如给我们的 <Love/> 组件传入一个参数 “嘉然” ,就会渲染成 “嘉然,我好喜欢你啊”。

参考完整代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
function App() {
return (
<div className="App">
<Love love={"嘉然"} />
</div>
);
}

function Love(props) {
return <div>{props.love + ",我好喜欢你啊"}</div>;
}

export default App;

表达式语法

这里又引入了几个新语法,比如这个大括号,看起来很奇怪还显得很多余,难道就不能把它省略掉吗?

事实上,这里的大括号表示的是表达式的意思,大括号框住的内容是一个需要计算的表达式,如果省略掉大括号,div 标签内的内容则会直接被转义成字符串。

也就是说如果我们写成下面的形式,渲染的内容就根本不会是我们想要的。

错误的渲染

...就像图片里演示的那样。

加上大括号后,结果就正常了,我们甚至还能多整几行。

正确的渲染

props

另一个奇怪的语法是这个 props ,很显然我们的参数传递都是通过 props 实现的。这个词是 properties 的缩写,意思就是属性,怎么感觉说了和没说的一样。。

在 jsx 中写标签时,我们往里面传入的参数则会被包装成一个 js 对象,也就是 props ,所以在组件函数中想要使用外界传入的参数时,只要使用 props 里的参数即可。

也就是说,如果要传递不可变的参数给组件,在 jsx 里面写标签的时候在标签里写上 " 参数名={参数值} " 就可以了,使用时用 " props.参数名 " 即可访问到所需要的参数。

至于为什么是不可变的参数,这是个大坑,之后到类组件和 hocks 的时候再细聊。

这种写法看起来挺酷炫的,但是捏,当组件需要的参数变的多起来且参数名称变的越来越长的时候就有点蛋疼了,因为憨憨的编辑器压根不提示!而且如果写了错误的参数名称,也不会报错,实际渲染时候那个 prop 的值则会变成 undefined 。这挺蠢的。。因为这种错误不必要非得在运行时才能发现。至于解决方式,后面我们在把 react 基础的一些玩意儿搞定后就会介绍一个屌炸天的新玩意儿,typescript,通过类型约束就可以很好的解决这种问题,甚至编辑器的提示也更有劲了,自动补全一口气能上三楼了。

解构赋值语法

可是现在学 ts 还是有点太远了,所以这里有一种简单的技巧也可以让编辑器提供更好的自动补全,即使用 es6 中新增加的解构赋值语法

1
2
3
4
5
6
7
8
9
10
11
12
13
function App() {
return (
<div className="App">
<ABC name={""} birthday={"我的生日"} />
</div>
);
}

function ABC({ name, birthday }) {
return <div>{name + "的生日是" + birthday}</div>;
}

export default App;
解构赋值

虽然是一段废话代码,但是这个技巧确实还不错。

条件渲染

在使用组件时经常会需要根据指定的条件来渲染特定组件,所以我们会希望在在渲染时增加一些判断逻辑。

包装组件和判断逻辑

由于 jsx 里面只能插入表达式,所以直接写判断是不行的,所以我们可以将判断语句和想要分别展示的组件放置到一个新组件中来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function App() {
return (
<div className="App">
<M1 choice={true} />
<M1 choice={false} />
</div>
);
}

function M1(props) {
if (props.choice === true) {
return <div>true</div>;
} else {
return <div>false</div>;
}
}

export default App;

三目运算符

包装成另一个组件在处理复杂逻辑时很有用的,但是绝大多数情况下我们可能只是需要一个二选一的判断, A 或 B,渲染或不渲染,这种情况下三目运算符就是一个很好的选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function App() {
const flag = true;

return (
<div className="App">
<div>{flag ? <What what={"???"} /> : null}</div>
<div>{flag ? null : <What what={"not empty"} />}</div>
<div>{!flag ? <What what={"false"} /> : <What what={"true"} />}</div>
</div>
);
}

function What(props) {
return <div>{props.what}</div>;
}

export default App;

就像代码里面演示的那样,当我们遇到渲染、不渲染二选一这种逻辑时,直接在不渲染时返回 null 即可。

处理列表

在页面的构建中,一种很常见的写法是将 js 中的列表或者其他可迭代的结构转换成对应的组件结构。比如说将后端返回的一组文章提要转换为 dom 树上的一个无序列表,接下来我们来处理这种情形。

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
37
function App() {
const data = [
{
name: "学生",
year: 24,
},
{
name: "李田所",
year: 19.19,
},
{
name: "野兽",
year: 8.1,
},
];

return (
<div className="App">
<ul>
{data.map((i) => (
<Item key={i.year.toString()} name={i.name} year={i.year}></Item>
))}
</ul>
</div>
);
}

function Item(props) {
return (
<li>
<h1>name: {props.name}</h1>
<h2>year: {props.year}</h2>
</li>
);
}

export default App;

虽然结果是差不多的,但是控制台却有个警告,

这里的警告说的是列表的每一个子元素都需要有一个 "key" 属性。

原因解释起来比较复杂,深入了解的话需要对 vdom 和 dif 算法有一定认识,简单概括就是无 key 时使用的 dif 算法比有 key 时的更加没有效率。

所以我们在 App() 里补充上 key,这里我们就将 key 设置为列表元素的 .year 属性。

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
function App() {
const data = [
{
name: "学生",
year: 24,
},
{
name: "李田所",
year: 19.19,
},
{
name: "野兽",
year: 8.1,
},
];

return (
<div className="App">
<ul>
{data.map((i) => (
<Item key={i.year.toString()} name={i.name} year={i.year}></Item>
))}
</ul>
</div>
);
}

另外一点值得注意的,这里的 key 属性只加在组件被调用时,也就是这里的 App() 组件,而不是直接加在 Item() 里面。

管理内部数据

在上个章节中我们使用的都是无内部状态的的组件,组件无法保存数据而是根据外部的数据源渲染,在这个章节中我们将会更进一步的研究讨论如何在组件中保存状态。

类组件和生命周期方法

在面向对象中,遇到这种需要内部数据的情形时,一般都是将数据结构和其他函数封装成一个类,使用时只需实例化对象即可。得益于 es6 引入的类语法,react 中也可以实现类似的操作。

这里作为示例,我们将创建一个简单的计时器,用于演示类组件的使用方法,以下为 App.jsx 的内容。

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
37
38
39
import React from "react";

function App() {
return <Clock />;
}

class Clock extends React.Component {
state = { date: null, timer: null };

constructor(props) {
super(props);
this.state = { date: new Date() };
}

componentDidMount() {
this.timer = setInterval(() => this.tick(), 1000);
}

componentWillUnmount() {
clearInterval(this.timer);
}

tick() {
this.setState({
date: new Date(),
});
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

export default App;

注意我们创建的类组件需要继承 React.Component ,所以需要预先 import 一下。

首先我们创建一个继承自 React.Component 的空组件。并添加 constructor 方法,这个方法会调用 super(props) 调用父类的构造函数。

然后为我们的类添加初始化的内部状态,即 state ,注意这一步并不是必需的,这样做的目的主要是为了方便阅读与增强编辑器的提示能力。

接下来创建一个 render 方法,这个函数会返回一段 jsx 表达式,也就是我们之前在函数式组件中写的返回值。

然后我们创建 tick 方法,这个方法会更新 state 中的字段。

这里就碰到了 react 中比较奇怪的一个特性,即不变性,也就是我们这里并不能直接修改 state 中的 date 属性,而是得使用 setState 方法重新给 state 赋值,setState 方法则只会基于我们传入的对象的字段去更新 state,state 的其余字段则会与原先一致。

由于我们希望组件在被创建后每秒都调用一次 tick 方法,所以我们需要一个生命周期方法,生命周期方法指的是在组件挂载、更新、销毁时会调用的方法。

所以我们在 componentDidMoun 方法中通过 setInterval 函数每隔一秒就调用一次 tick 方法,注意在组件销毁时清除掉 setInterval 返回的 interval ID ,所以我们在 componentWillUnmount 方法中调用 clearInterval 函数清除掉 interval ID。

除了我们刚才介绍的两个生命周期方法,其实还有一些其他的生命周期方法,关于完整的生命周期方法和生命周期函数的执行顺序,可以参考这篇文档。

听着貌似挺乱的。。。事实也确实如此,相较于函数组件的简洁形式,类组件明显要更复杂些,但是有没有一种更加优雅的方法呢?当然是是有的!在后几章我们会接触到如何在函数组件中使用 hocks 管理内部数据。

回调函数与 React 事件处理

不会响应用户交互的组件是没有前途的!在这个小节我们将会介绍在组件中如何使用回调函数更新 state ,并学习使用 react 中的事件处理函数。

回调函数和蛋疼的 this

回调函数,一种非常常见的设计模式,在使用 react 组件时,有时候我们会希望某些组件接受一个函数并在适时的时候调用,这时候就是回调操作大展身手的机会。接下来我们来看一个例子怎么使用回调创建一个计数器的框架。

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 from "react";

function App() {
return <Counter />;
}

class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}

add() {
this.setState({ count: this.state.count + 1 });
}

render() {
return (
<>
<h1>Count: {this.state.count}</h1>
<button onClick={() => this.add()}>+1</button>
</>
);
}
}

export default App;

好吧。这个例子可能有些剧透,比如 props 里面那个奇怪的 onClick ,总之先知道这个 onClick 会在组件被点击时调用传入的函数就行了。

我们来看代码里面的其他部分,和在上一节里面介绍的类组件一样,我们同样为其提供了 constructor 和 render 函数,然后定义一个 add 函数用于更新 state,然后传递一个奇怪的箭头函数给 onClick 做回调,当按钮被点击时即会触发。

这里的写法其实很奇怪,为什么不直接传函数名而是传递一个箭头函数呢?这里就是 js 屎山特性的一个蛋疼问题,臭名昭著的 this 绑定问题。具体内容可以参考 mdn 的这篇文章以及 react 文档中关于 jsx 编译为 js 代码的内容。

如果我们写成如下写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add() {
console.log("Clicked!");
console.log("this =");
console.log(this);
this.setState({ count: this.state.count + 1 });
}

render() {
return (
<>
<h1>Count: {this.state.count}</h1>
<button onClick={this.add}>+1</button>
</>
);
}

在点击按钮后

就会发现 add 函数的 this 绑定到了 undefined 上,也就不可能更新 state 内容了。

箭头函数并没有自己的 this,所以传递箭头函数给 onClick 的时候, add 的 this 则就处于正确的上下文环境中了。

1
2
3
4
5
6
7
8
render() {
return (
<>
<h1>Count: {this.state.count}</h1>
<button onClick={() => this.add()}>+1</button>
</>
);
}

茴字的四种写法

其实除了上述直接传递箭头函数的写法外,还有几种其他的写法。

1.公有类字段绑定箭头函数,传参提供字段名称(推荐)

具体细节可以参考这份文档,这也是推荐的一种写法,可以避免在调用时创建多余的箭头函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}

add = () => {
console.log("Clicked!");
console.log("this =");
console.log(this);
this.setState({ count: this.state.count + 1 });
};

render() {
return (
<>
<h1>Count: {this.state.count}</h1>
<button onClick={this.add}>+1</button>
</>
);
}
}
2.使用 bind 手动绑定 this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}

add() {
console.log("Clicked!");
console.log("this =");
console.log(this);
this.setState({ count: this.state.count + 1 });
}

render() {
return (
<>
<h1>Count: {this.state.count}</h1>
<button onClick={this.add.bind(this)}>+1</button>
</>
);
}
}

注意 bind 返回的是一个函数,所以这么写是可以的。

3.使用 call 手动绑定 this

真的会有人这么写吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}

add() {
console.log("Clicked!");
console.log("this =");
console.log(this);
this.setState({ count: this.state.count + 1 });
}

render() {
return (
<>
<h1>Count: {this.state.count}</h1>
<button onClick={() => this.add.call(this)}>+1</button>
</>
);
}
}

react 的事件处理函数

在刚才我们实现的计数器组件中已经用到了 react 中的事件处理函数,也就是我们传递给 button 的 onClick 属性,正如其名,这个事件处理函数会在组件被点击时触发。 此外常见的事件处理函数还有 onFocus, onKeyPress 等等。完整的列表可以参考 react 文档提供的部分。 另外就是在子组件处理事件时,有时候需要避免父组件(冒泡)或者自身的默认行为。 这里采用表单组件作为例子,当 submit 时会触发页面刷新,我们尝试在组件中阻止默认行为。

1
2
3
4
5
6
7
8
9
10
11
12
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log("You clicked submit.");
}

return (
<form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>   {" "}
</form>
);
}

这里 onSubmit 绑定到了一个接受合成事件 e 的函数,在 handleSubmit 中调用 e 的 preeventDefault 方法阻止其默认行为。 当然就像上一节中介绍的各种写法一样,也可以改写成类组件的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Form extends React.Component {
constructor(props) {
super(props);
}

handleSubmit = (e) => {
e.preventDefault();
console.log("You clicked submit.");
};
render() {
return (
<form onSubmit={this.handleSubmit}>
        <button type="submit">Submit</button>     {" "}
</form>
);
}
}

事件处理传递额外参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Hello extends React.Component {
render() {
return (
<div>
<button onClick={(e) => this.handleClick(e, "click button")}>
button
</button>
</div>
);
}

constructor(props) {
super(props);
}

handleClick = (e, msg) => {
console.log(msg);
console.log(e);
};
}

受控组件

在使用组件去创建保存数据并对输入数据进行渲染的应用时,一种很常见的的手法是使用受控组件,即在父组件中保存状态,子组件根据父组件的 state 进行渲染,当需要进行状态变更时,子组件调用父组件传入的函数来更改状态。

graph TD
A[父组件] -->|数据流| B[子组件]
B -.-> |控制流| A

我们接下来写一个表单的例子来演示一下。

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
37
38
import React from "react";

function App() {
return <PackagedForm />;
}

class PackagedForm extends React.Component {
constructor(props) {
super(props);
this.state = { input: "" };
}

handleChange = (e) => {
this.setState({ input: e.target.value });
};

handleSubmit = (e) => {
alert("提交的名字: " + this.state.input);
e.preventDefault();
};

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input
type="text"
value={this.state.input}
onChange={this.handleChange}
/>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
export default App;

在这里,表单本身并不会保存数据,所有的数据都是保存在我们自定义的组件的 state 中,就像刚才那张流程图描述的一样。

hocks

hocks 是 react 16.8 新引进的一种功能,可以不使用 class 写法也能保存 state 和管理组件的生命周期。

State Hook

state hock 提供了一个简单的方式来管理 state 接下来我们创建一个简单的计数器来使用 state hock 。

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

function App() {
return <Counter />;
}

function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>count: {count}</h1>
<button onClick={() => setCount(count + 1)}>click me</button>
</div>
);
}

export default App;

是不是很简单?useState 函数返回了一个数据,通过数组解构语法把其内容物绑定到 count 和 setCount 上,然后在组件中直接使用 count 即可,如果需要更新,使用 setCount 然后传入用于更新的表达式即可。

顺带一提,setCount 中间也可以传入一个函数用于更新 state 。 如果需要多个内部状态,重复多次使用 useState 即可。

useState 使用起来非常类似于其他语言中实例化对象的静态变量,在组件的生命周期中这些 state 只会被初始化一次,然后被更新和使用。

副作用和 Efect Hock

好吧,到这里我们不得不稍微提及一点函数式相关的内容。 关于纯函数和副作用,详细可以参考这篇 wiki

简单来说纯函数就是输出只和输入唯一绑定且不影响其他状态,如全局变量等的函数。

副作用就是非纯函数调用时会产生的影响,包括不限于 I/O,全局变量的写。

effect hock 就是一类用于执行副作用相关操作的 hock,数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。

事实上 effect hock 的用法相当多样,react 团队基本上讲一大堆原先的生命周期方法都塞进 effect hock 里面了。。。我觉得这样挺蠢的。接下来我们简单将各个用法分类介绍下。

对应组件挂载或者更新时

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";
import { useEffect } from "react";

function App() {
return <Counter />;
}

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`clicked ${count} times`);
});

return (
<div>
<h2>{count}</h2>
<button onClick={() => setCount((i) => i + 1)}>click me</button>
</div>
);
}

export default App;

注意,这里的 useEffect 会在开始时调用两次传入的函数,这并不是什么 bug ,在生产环境发布时并不会有上述行为,详细信息请自行使用搜索引擎,在此不再叙述过多。

对应组件卸载时(清除副作用)

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

function App() {
return <Box />;
}

function Box() {
const [flag, setFlag] = useState(true);
return (
<>
{flag ? <Counter /> : null}
<button onClick={() => setFlag(!flag)}>{flag ? "on" : "off"}</button>
</>
);
}

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`clicked ${count} times`);
return () => {
console.log("unmounted");
};
});

return (
<div>
<h2>{count}</h2>
<button onClick={() => setCount((i) => i + 1)}>click me</button>
</div>
);
}

export default App;

在这里,当组件 Counter 被卸载时,会调用我们传给 useEffect 的函数,也就是在控制台打印消息 "unmounted" 。

追踪依赖变化

有时候我们只希望在某些变量变化时才使用 useEffect,要实现这种操作其实很简单,只要在 useEffect 的第二个参数中传入需要追踪的参数的列表即可。

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
37
38
39
40
41
import React, { useState } from "react";
import { useEffect } from "react";

function App() {
return (
<>
<Counter />
</>
);
}

function Counter() {
const [button0, setButton0] = useState(0);
const [button1, setButton1] = useState(0);
const [button2, setButton2] = useState(0);

useEffect(() => console.log("you clicked button0"), [button0]);

useEffect(() => console.log("you clicked button1"), [button1]);

useEffect(() => console.log("you clicked button2"), [button2]);

return (
<>
<h1>
button0:{button0} <br /> button1:{button1} <br /> button2:{button2}
</h1>
<button onClick={() => setButton0(button0 + 1)} key={"button0"}>
button0: +1
</button>
<button onClick={() => setButton1(button1 + 1)} key={"button1"}>
button1: +1
</button>
<button onClick={() => setButton2(button2 + 1)} key={"button2"}>
button2: +1
</button>
</>
);
}

export default App;

注意,这里追踪的依赖项都是基本变量,假设你需要依赖项是一个引用类型,请不要这么写,有可能会出现不及时更新或者重复更新等 bug,解决方法在这里详细叙述的话可能不太合适,简单来说,使用第三方全局状态管理或者 useMemo 会是一个好主意。

仅在挂载和销毁时调用

这实际上是上一小节的小小拓展。。。只要把依赖数组填写为空数组即可,是不是很简单?

Custom Hock

todo...

isThereAnythingMore?

hocks 事实上还有很多可以聊下去的话题。。。但是深入展开的话就不符合这篇记录的主旨了!这只是一篇关于 react 学习的小作品,更多的内容就交给 google 和 github 吧 :)

这里推荐几个还不错的学习资源。

  1. React 官网的 Hocks API 索引

  2. dan_abramov 大佬的关于 effectHocks 的一篇长文

阶段性总结

在前几个章节里我们已经一起体验了 react 的核心概念,在后续的章节里我们将会更深入的步入基于 react 的前端开发,就像之前承诺的,学习 ts ,然后去使用一些第三方库。

相信我,这绝对会非常有趣的 ;)

TypeScript

在这个章节中我们将会介绍 ts 的基本语法和如何在 react 中使用 tsx 编写和使用组件以及如何成为一名类型体操运动员。 注意,我们不会详细的介绍所有概念,完整的介绍可以阅读官方文档

类型

基本类型

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
// 布尔值
let flag: boolean = false; // ts采用后置类型标注
let anoutherFlag = false; // ts也支持类型推断,所以绝大数情况下不需要手动标注类型

// number
let num = 1.1
const number = 10 // 你也可以使用 const 来声明一个不可变变量

// 字符串
const name = "ooj"
const favorite = "chocolate"
let s = `${name} 's favorite food is ${favorite}` //模板字符串

// void
function f(): void {
console.log("hi")
}
// 无返回值的函数的返回类型即为 void
const res: void = f()

// null 和 undefined
// 任何类型的子类型
let i:number = 1
i = null
i = undefined

// any
// 加上这个, ts 就变回了 js

联合类型(Union Types)

1
2
3
4
5
6
7
8
9
10
11
12
13
//声明联合类型
let n: string|numbrt = "one"
n = 1

//当上下文无法推断出具体类型时,访问属性或者方法时只能访问公共属性、方法
let a: Array<Number>|string = "hi";
let b: Array<Number>|string = [1, 2, 3];
const len = (i : Array<Number> | string): number => {
return i.length
}

console.log("a: " + len(a));
console.log("b: " + len(b));

类型别名

1
2
3
4
5
6
7
8
9
10
//上一小节的联合类型写了很多扽重复代码,通过类型别名可以更好的表述我们需要的类型
type MyType = Array<Number>|string;
let a: MyType = "hi";
let b: MyType = [1, 2, 3];
const len = (i: MyType): number => {
return i.length;
}

console.log("a: " + len(a));
console.log("b: " + len(b));

数组

1
2
3
4
//一般写法
let numbers: number[] = [1, 2, 3]
//泛型写法
let myNumbers: Array<number> = [1, 2, 3]

函数

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
37
38
39
40
41
42
43
44
45
46
47
//函数的类型
type F1 = () => void
const hi: F1 = () => {
console.log("hi")
}
type F2 = (a: number, b: number) => number;
const sub: F2 = (a: number, b: number) => {
return a - b;
}

//函数的类型标注
const add: (a: number, b: number) => number = (a: number, b: number) =>{
return a + b;
}

function mul(a: number, b: number): number {
  return a * b;
}

//可选参数
type f = (a: number, b?: number) => number

const add: f = (a: number, b?: number) => {
if (typeof b ==="undefined") {
return a;
}
return a + b;
}

//参数默认值
function add(a: number = 0, b: number = 0) {
//注意此时 add 的类型变成了 (a?: number, b?: number) => number
return a + b;
}

console.log(add());
console.log(add(1));
console.log(add(1, 2))

//剩余参数
function sum(a: number = 0, ...args: number[]) {
let s = a;
for (const i of args) {
s += i;
}
return s;
}

类型断言

类型断言可以欺骗编译器,让编译器认为某个变量的类型为另一个类型。 类型断言可以有两种写法 as "你想要的类型" 或者 <"你想要的类型">"变量" ,我们推荐前一种写法,否则在编写 tsx 时可能会产生语法歧义。 这里我举一个实际开发中会用到断言的地方。

1
2
3
4
5
6
7
8
9
10
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
getElementById 方法的返回值为 HTMLElement | null ,但是我们并不需要处理 null 的情况。。因为如果在根节点处获取的是 null , 很可能意味着发生了更大的问题,断言成 HTMLElement ,然后真出问题时让其在运行时崩溃可能是更好的一种选择。


react学习记录
https://ooj2003.github.io/2022/07/17/react学习记录/
作者
OOJ2003
发布于
2022年7月17日
更新于
2022年12月14日
许可协议