React 学习笔记

React 入门

引入

React 是什么

根据官网: 这是一个用于构建用户界面的 JS 库.

比如, 我希望在页面上展示学生的信息, 那么使用 JS, 就是操作各种 DOM, react 就是帮助我们操作 DOM 的一个框架. 另外, React 也有一些处理数据的作用, 可以将传入的数据进行处理后, 再进行渲染.

总结来说, React 就是一个将数据渲染为 HTML 视图的开源 JavaScript 库.

谁开发的

当然是知名的 Facebook 开发的! 不过一开始, 其实是 Facebook 的一个工程师实现的, 随后 Facebook 进行了升级部署, 最后宣布开源.

为什么要学习

因为原生的 JS 效率太低了, 并且太过于繁琐. 比如我要获取某个元素, 代码量会很大, 因为我们是根据 DOM 来操作 UI 的.

JS 会造成浏览器的大量重绘重排, 数据量大了会非常的可怕.

另外, JS 是没有组件化的编码方案的, 代码的复用率较低.

React 的特点

React 有组件化的模式, 以及声明式的编码, 提高了开发效率以及组件的复用率.

声明式, 就是自动识别的一种方法, 不需要我们一步步教编译器怎么做.

另外, React Native 也可以进行移动端的开发, 最重要的, React 存在虚拟 DOM 的概念, 不需要我们操作真实 DOM 了. 例如, 我原来页面上显示了两个人的列表, 现在我请求了另外的人, 如果使用最简单的 JS, 则原来的 DOM 直接被替换掉了, 这不是我们所希望的. 如果使用 React, 则不会替换掉原来的 DOM, 大大提升了运行效率.

原理就是先存在虚拟 DOM 中, 新的数据来了以后, 生成新的虚拟 DOM, 进行比较, 原来的一样的虚拟 DOM 对应的真实 DOM 是不会进行修改的, 但是新的 DOM 则会进行新的渲染.

JS 基础知识

至少需要具备如下基础知识:

  1. this 的指向的判断
  2. class 类的概念
  3. ES6 语法规范
  4. npm 包管理器
  5. 原型, 原型链
  6. 数组的常用方法
  7. 模块化

React 的基本使用

Hello React

为了方便引入, 我们可以先使用引入 js 的方式来学习 react. 直接引入需要的 js 文件即可:

注意, 这里的引入顺序不能变, 否则会报错的! 因为先有核心库, 才有后来的扩展库.

1
2
3
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/18.2.0/umd/react.production.min.js" ></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/babel-standalone/6.26.0/babel.min.js" ></script>

我们直接创建一个 html​ 文档, 即可开始学习之旅.

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello React</title>
</head>
<body>
<!-- 首先 需要准备好一个容器 -->
<div id="test"></div>
<!--引入React的核心库-->
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/18.2.0/umd/react.production.min.js"></script>
<!--引入React的DOM操作库-->
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<!--引入babel 将jsx转换为js-->
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/babel-standalone/6.26.0/babel.min.js"></script>

<!--写脚本-->
<!--这里需要注意 写的不是jsx 而是babel, 表示需要它进行帮忙翻译-->
<script type="text/babel">
// 首先, 我们需要先有数据
// 1. 创建虚拟DOM
const V_DOM = <h1>Hello, React</h1>; // 注意, 这里不需要写引号, 因为不是字符串
// 2. 渲染虚拟DOM到页面
ReactDOM.render(V_DOM, document.getElementById("test"))
</script>
</body>
</html>

image

打开浏览器, 成功渲染出来了 Hello React!

这里有如下点需要注意:

  1. 引入库是有顺序要求的
  2. script 的 type 是 babel, 否则直接报错
  3. React 中, JS 可以和 HTML 混写, 千万不要写双引号, 否则就是一个普通的字符串了.

这里需要注意: ReactDOM.render(V_DOM, document.getElementById("test"))​ 是一个覆盖的动作, 并不是追加, 所以如果写很多个, 则以最后一个为准. 那么怎么添加呢? 后面学到组件就知道了 √

创建虚拟 DOM 的两种方式

刚才写的, 其实就是通过 JSX 来创建虚拟 DOM 了, 写起来比较简单; 不过, 也可以使用最简单的 JS 来创建虚拟 DOM.

1
2
3
4
5
6
7
<script type="text/javascript">
// 创建虚拟DOM
// 传入的东西是: 标签名, 标签属性, 标签内容
const V_DOM = React.createElement('h1', {id: 'title'}, 'Hello React - 2')
// 渲染
ReactDOM.render(V_DOM, document.getElementById('test'))
</script>

image

所以, 不用 JSX 也是可以实现的; 但是呢, 我希望 <h1>​ 里面还有一个 span​ 标签, 那么这种纯 JS 的写法就麻烦了.

下面的这种 JSX 会更为简单:

1
2
3
4
5
6
7
8
9
<script type="text/babel">
// 创建虚拟DOM
const V_DOM = <ul>
<li>Hello</li>
<li>World</li>
</ul>;
// 渲染
ReactDOM.render(V_DOM, document.getElementById('test'));
</script>

|220

总结来说, JSX 就是为了更加简单的创建虚拟 DOM, 并且支持多级结构! 不过呢, 其实 JSX 就是 JS 写法的一种语法糖, 让写代码更加简单, 优雅.

虚拟 DOM 与真实 DOM

我们可以在控制台中输出看看, 这个虚拟 DOM 到底是个什么:

1
2
3
4
5
6
7
8
9
10
11
<script type="text/babel">
// 创建虚拟DOM
const V_DOM = <ul>
<li>Hello</li>
<li>World</li>
</ul>;
// 渲染
ReactDOM.render(V_DOM, document.getElementById('test'));
// 输出查看
console.log(V_DOM)
</script>

|250

由此, 虚拟 DOM 其实就是一个对象, 和真实 DOM 的区别是什么呢? 真实 DOM 其实在控制台中, 就是标签.

虚拟 DOM 比较的轻, 但是真实 DOM 的属性则多得多了. 不过呢, 虚拟 DOM 最终会被转换为真实 DOM, 呈现在页面中.

JSX

什么是 JSX

其实就是 JavaScript XML, 一种类似于 XML 的 JS 扩展语法.

本质是 React.createElement() ​ 的语法糖, 可以用来简化创建虚拟 DOM.

JSX 语法规则

  1. 定义虚拟 DOM 的时候, 不要写引号!
  2. 如果需要读取变量, 或者混入 JS 代码的时候, 需要使用花括号 {}​ 包裹起来.
  3. 如果想要使用某个样式, 不允许写 class=xxx​, 而是需要使用 className=xxx​ ​这种.
  4. 如果想要写行内的 style, 不能直接 color: white​ 这种. 只能通过 key-value 的形式来写, 应该使用 {{}}​ 来写需要的行内样式.
  5. JSX 要求, 只能有一个根标签. 只要两个根标签, 直接报错!
  6. 对于 input 标签, 一定需要闭合! 要么自结束, 要么写前后两个标签.
  7. 虽然可以写自定义标签, 不过不能乱写, 如果是组件, 则首字母大写, 否则不要乱写. 因为如果是小写字母开头, 则自动转换为一个 html 标签, 如果 html 中没有该标签对应的同名元素, 则报错. 另外, 如果改为大写, 有组件才不会报错, 否则直接报错: 组件未定义.

包含所有注意点的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type="text/babel">
// 定义变量
const myID = "Kaede";
const myData = "Hello React";
// 创建虚拟DOM
const V_DOM = <div>
<ul>
<li className="normal-background">{myID}</li>
<li style={{color: "red", fontSize: "30px"}}>{myData.toLowerCase()}</li>
</ul>
<input type="text" />
<good>自定义标签</good>
</div>;
// 渲染
ReactDOM.render(V_DOM, document.getElementById('test'));
</script>

image

JSX 小练习

综合上面学习的内容, 我们可以写一个简单的小练习: 渲染一个列表出来. 如果我们写死, 可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/babel">
// 创建虚拟DOM
const V_DOM = <div>
<h1>前端js框架列表</h1>
<ul>
<li>React</li>
<li>Vue</li>
<li>Angular</li>
</ul>
</div>;
// 渲染
ReactDOM.render(V_DOM, document.getElementById('test'));
</script>

可是显然, 我们不希望这样子, 我们希望它根据一个数组来动态的进行渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/babel">
// 定义数据
const data = ["Angular", "react", "vue"];
// 创建虚拟DOM
const V_DOM = <div>
<h1>前端js框架列表</h1>
<ul>
{data}
</ul>
</div>;
// 渲染
ReactDOM.render(V_DOM, document.getElementById('test'));
</script>

|330

这并不是我们期待的效果, 但是 React 确实自动进行遍历了! 这说明, React 会自动对数组进行遍历.

不过, 对象是不能自动遍历的, 会提示你使用数组来进行遍历

我现在希望, 这个数据编程有加工后的数据, 想要循环是不可以的! 因为: 标签中只能放入 JS 表达式. JS 表达式并不是 JS 代码, 表达式是有一个值的, 可以放在任何一个需要值的地方, 比如这些:

  • a 自动找到这个变量并且返回对应的值
  • a + b 计算后的值进行返回
  • demo(1) 函数调用的返回值
  • arr.map() 返回加工后的数组
  • function test () {} 定义一个函数也是有返回值的, 就是这个函数本身.

语句, 或者代码, 则是下面这些:

  • if () {}
  • for () {}
  • switch () {}

所以, 这里我需要将字符串加工成一个东西, 就可以使用 map 函数了. 进行加工, 可以得到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script type="text/babel">
// 定义数据
const data = ["Angular", "react", "vue"];
// 创建虚拟DOM
const V_DOM = <div>
<h1>前端js框架列表</h1>
<ul>
{
data.map((item) => {
return <li>{item}</li>;
})
}
</ul>
</div>;
// 渲染
ReactDOM.render(V_DOM, document.getElementById('test'));
</script>

|330

不过, 可能出现报错, 说明每个子元素都应该有一个唯一值 Key, 只要唯一就行.

模块(化)与组件(化)

模块, 就是向外提供特定功能的 JS 代码, 简单说就是一个单独的 JS 代码文件. 只要拆开, 就可以多次复用不同的 JS 代码, 进而提高开发的效率, 以及运行的效率.

比模块高一个等级, 就是组件. 组件, 就是用来实现局部功能的代码和资源的整合. 比如, 我把头部 Header, 拆分出来, 作为一个单独的组件, 方便进行直接的使用. 这样, 组件就可以直接进行复用了!

模块化, 自然就是模块的动词, 意思就是: 应用的 js 都以模块来进行编写, 这个引用就是一个模块化的应用.

组件化, 顾名思义, 就是这个引用是以多组件的形式来实现的, 那么这个应用就是一个组件化的应用.

React 面向组件编程

基本理解及使用

使用开发者工具进行调试

直接在浏览器的应用商店中, 搜索如下扩展即可进行安装:

image

只要安装了这个工具, 只要当前网页是 React 做的, 就可以在开发者工具找到如下内容:

image

定义组件

函数式组件

定义组件有两种方式, 一种是函数式组件, 一种是类式组件. 函数式比较简单, 根据想法我们可以写出如下代码:

1
2
3
4
5
6
7
8
9
<script type="text/babel">
// 创建函数式组件
function demo() {
return <h1>我是用函数定义的组件(适用于简单组件的定义)</h1>
}

// 渲染组件到页面
ReactDOM.render(demo, document.getElementById("test"));
</script>

打开网站, 没有看到任何东西. 这是因为, 函数是不能作为一个组件的, 但是函数的返回值可以!

同时, 组件的首字母必须大写, 所以需要改函数名为首字母大写, 随后按照标签进行传入即可:

1
2
3
4
5
6
7
8
9
<script type="text/babel">
// 创建函数式组件
function Demo() {
return <h1>我是用函数定义的组件(适用于简单组件的定义)</h1>
}

// 渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById("test"));
</script>

|425

这个时候, 在开发者工具中, 已经可以识别到这个组件了!

image

这里, 我们没有手动的调用函数, 而是 React 帮助我们调用了函数. 在这个函数中, this 指向的是 undefined. 因为 Babel 会在编译代码后, 自动开启严格模式, 严格模式下, 对于类调用的 JS, this 会指向 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
32
33
<!--类的基本使用-->
<script type="text/javascript">
// 创建Person类
class Person {
// 借助构造函数
constructor(name, age) {
// 构造器中的this是类的实例对象
this.name = name;
this.age = age;
}

// 一般方法 就是自定义方法
speak() {
/*
speak方法放在了类的原型对象上面 并不在自己身上
speak方法是给实例用的 谁调用就给谁
*/
console.log(`我叫${this.name}, 年龄${this.age}`)
}
}

// 创建示例对象
const p1 = new Person("yyt", 18);
const p2 = new Person("lc", 19);
// 输出实例对象 前面是类是什么, 后面才是类的内容
console.log(p1);
console.log(p2);
// 调用的时候, this指向自己这个实例
p1.speak();
p2.speak();
// 如果使用call调用 则this无效
p1.speak.call({a: 1, b: 2});
</script>

|330

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--类的继承-->
<script type="text/javascript">
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

speak() {
console.log(`我叫${this.name}, 年龄${this.age}`)
}
}

// 继承
class Student extends Person {
// 继承, 不用再写构造器了, 父类的构造器可以直接调用了
}

// 实例化
const s1 = new Student("yyt", 18);
s1.speak();
</script>

回到代码当中, 类组件, 自然就是定义一个类了. 简单说, 类名就是组件名.

不过, 如果要写组件, 则需要继承自 React.Component​ 类. 同时, 必须实现 render()​ 方法, 且这个方法必须有返回值.

1
2
3
4
5
6
7
8
9
10
11
<script type="text/babel">
// 创建类式组件
class MyComponent extends React.Component {
render() {
return <h2>我是类定义的组件(适用于复杂组件)</h2>
}
}

// 渲染组件到页面
ReactDOM.render(<MyComponent/>, document.getElementById("test"))
</script>

|330

至此, 组件已经成功显示.

注意点

  1. render 式放在类的原型对象上的
  2. render 是供实例使用的, 但是我们没有 new 过这个类的实例, 且页面上是有东西的.
  3. 这里需要注意, 我们直接使用了组件标签, 这个时候, react 会自动实例化一个组件出来, 我们不用手动的调用 new 方法, 同时 React 也会自动的调用 render 方法, 显示到页面当中.
  4. render 函数中, this 指向的是实例对象, 也就是组件的实例对象. (简称: 组件实例对象)

组件三大核心属性-state

解决 this 指向的问题

准确说, 是组件实例的 state 属性. 只有类定义的组件, 才有 state 属性, 所以我们后面就不再研究函数式组件了.

注意, 后面的 hooks 也可以, 同时也是函数式组件, 不过这里先不研究.

我们尝试做一个效果: 屏幕上面显示一句话, 通过点击可以来回进行切换.

image

经过思考, 其实就是两种状态, 所以这是一个布尔类型的值, 我们不妨想办法给 state 里面, 放一个 isHot 的布尔值. 这里因为需要初始化, 所以需要写一个构造器!

这里的构造器需要传入什么呢? 这就需要查阅官方文档了.

image

这里的 props 是后面的东西, 这里是必要的, 所以直接写上就好.

随后, 我希望给 state 里面加东西, 这个 state 是一个对象类型, 所以不能直接设置为一个布尔值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type="text/babel">
// 创建类式组件
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
isHot: true,
}
}
render() {
return <h2>今天天气很炎热</h2>
}
}
// 渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById("test"))
</script>

现在, 我们需要读取这个值. 这个值在组件的实例对象身上, 所以直接使用 this 就能获取. 下面返回, 三元表达式即可:

1
2
3
render() {
return <h2>今天天气很{this.state.isHot ? "炎热" : "寒冷"}</h2>
}

同时, 可以在开发者工具中看到这个组件上面的所有状态信息

image

接下来, 想办法实现点击后修改吧! 在 React 中, 之前的三种写法都是可以正常使用的, 不过还是更为推荐直接在标签中书写. 就是类似于 <button onclick="xxx"></button>​ 这种.

这里需要注意, React 中所有 on 开头的事件全部都被重写, 变成了 onClick, 小驼峰的写法. 同时需要注意, 传入的函数不能有小括号, 原生 JS 中, 点击的意思是点击后, 执行字符串中的代码; React 中, 则是返回值的形式. 目前可以写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script type="text/babel">
// 创建类式组件
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
isHot: true,
}
}

render() {
return <h2 onClick={changeWeather}>今天天气很{this.state.isHot ? "炎热" : "寒冷"}</h2>
}
}

// 渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById("test"))

function changeWeather() {
console.log("改变天气");
}
</script>

现在我们准备修改了, 可以先试试能不能读取到这个属性:

image

看起来是不可以的. 这里是因为外部的函数, 无法访问到组件的 this. 只把函数写在类里面是不够的, 还需要有 this 的指向.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script type="text/babel">
// 创建类式组件
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
isHot: true,
}
}

changeWeather() {
// 放在了类的原型对象上 提供实例使用
console.log(`Weather -> ${this.state.isHot}`)
}

render() {
return <h2 onClick={this.changeWeather}>今天天气很{this.state.isHot ? "炎热" : "寒冷"}</h2>
}
}

// 渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById("test"))
</script>

这里的 changeWeather 中, this 其实还是指向 undefined. 这是因为类并不是通过实例调用的. 这里我们简单复习一下类中方法的 this 指向.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script type="text/javascript">
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak(){
console.log(this)
}
}

const p1 = new Person('yyt', 18);
p1.speak(); // 通过实例调用speak方法

const x = p1.speak;
x(); // undefined 这是直接调用, 找不到this的.
/*
类的局部方法 默认开启了局部的严格模式!
所以这个时候的this就是undefined了.
*/
</script>

那么回到刚才的代码中, 其实问题就是, this.changeWeather​, 本质上就是顺着原型链, 找到了 changeWeather 这个东西, 并且进行回调, 并没有正确的进行调用, 而是一个直接调用.

那么如何解决的? 其实只要一行代码就解决了. 在构造方法中, 我们给这个函数绑定一下 this 就好:

1
this.changeWeather = this.changeWeather.bind(this)

.bind​ 方法会返回一个新的函数, 这个新函数是改好 this 的一个新的函数. 至此, 我们就成功解决了 this 的指向问题.

解决切换的问题 setState

我们如果直接切换原来的值, 会发现就算成功修改了, 前端显示也是没有任何变化的.

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
<script type="text/babel">
// 创建类式组件
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
isHot: true,
}
this.changeWeather = this.changeWeather.bind(this)
}

changeWeather() {
// 放在了类的原型对象上 提供实例使用
console.log(`Weather -> ${this.state.isHot}`)
this.state.isHot = !this.state.isHot;
}

render() {
return <h2 onClick={this.changeWeather}>今天天气很{this.state.isHot ? "炎热" : "寒冷"}</h2>
}
}

// 渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById("test"))
</script>

image

通过输出, 我们发现变量的数据已经进行了切换. 但是回顾一下, 状态的数据我们是不能直接进行更改的! 进入开发者控制台, 可以看到其实压根没有变化.

这里我们需要借助一个内部的 API 来进行更改: setState()​ 可以直接进行调用. (通过查看原型链就知道了)

1
2
3
4
5
6
7
changeWeather() {
// 放在了类的原型对象上 提供实例使用
console.log(`Weather -> ${this.state.isHot}`)
// 这是错误的写法
// this.state.isHot = !this.state.isHot;
this.setState({isHot: !this.state.isHot});
}

至此, 已经成功进行修改!

imageimage

这里的函数传入的是一个对象, 里面设置需要更改的值为更改后的值即可.

注意

setState 方法是一个合并的动作, 仅仅会更新传入的需要修改的值, 原来的其他值都是存在的!

另外, 构造器只会调用一次, 而 render 会调用 1+n 次. 1 是一开始显示到页面上, n 是状态更新的次数.

state 的简写方式

上面的代码还是过于复杂, 不适合写入大型项目当中. 类里面的方法, 其实大部分都是回调事件, 如果有很多的点击事件, 则要写一堆的 bind​ 方法, 这并不是我们想要的结果. 最后我们可以写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/babel">
class Weather extends React.Component {
// 给类追加属性 可以直接赋值 进而初始化 不需要写在构造器中
state = {isHot: true};

// 自定义方法
// 使用箭头函数 默认没有自己的this, 会自动寻找外层的this 作为自己的this!
// 这样就不需要绑定各种东西了
changeWeather = () => {
this.setState({isHot: !this.state.isHot});
}

render() {
return <h2 onClick={this.changeWeather}>今天天气很{this.state.isHot ? "炎热" : "寒冷"}</h2>
}
}

ReactDOM.render(<Weather/>, document.getElementById("test"))
</script>

这里我们可以总结一些规律:

  1. 给类初始化 state, 不需要在构造器中写
  2. 自定义方法, 统一写在类内, 并且使用函数名 = 箭头函数的形式.
  3. 构造器并不是必要的

虽然刚才的写法非常标准, 但是还是比较麻烦, 不方便.

state 总结

  1. state 已经是最复杂的一个部分了, 只要这个部分学习的没有问题, 后面学起来就会非常轻松了!
  2. state 是一个对象, 储存了各种变量的状态, 通过 setState 方法, 可以设置 state 里面的属性, 同时更新页面的显示.
  3. 自定义方法, 一定用箭头函数的形式, 不然会造成 this 指向出错.

组件三大核心属性-props

props 基本使用

我希望, 一个组件就是一个人, 这个人是一个列表, 多级结构. 可以写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script type="text/babel">
class Person extends React.Component {
state = {
name: "yyt",
gender: "女",
age: 18
}

render() {
return (
<ul>
<li>姓名: {this.state.name}</li>
<li>性别: {this.state.gender}</li>
<li>年龄: {this.state.age}</li>
</ul>
);
}
}

ReactDOM.render(<Person/>, document.getElementById("test"))
</script>

image

这里的信息都是写死的, 我希望我能够从外部传入一些信息, 作为组件的属性, 比如我要渲染多个标签:

1
2
ReactDOM.render(<Person/>, document.getElementById("test"))
ReactDOM.render(<Person2/>, document.getElementById("test2"))

如果每个单独的人都是一个独立的组件, 会非常麻烦, 甚至不太可能. 所以, 我们需要给组件进行传参即可.

作为 html 标签, 组件也是可以写的, 假如我传递一个 name 为 yyt:

1
ReactDOM.render(<Person name="yyt"/>, document.getElementById("test"))

image

至此, name 已经成功通过 props 进行传递了! 传入剩下的内容, 并且通过 this.props​ 进行访问即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="test"></div>
<div id="test2"></div>

<script type="text/babel">
class Person extends React.Component {

render() {
return (
<ul>
<li>姓名: {this.props.name}</li>
<li>性别: {this.props.gender}</li>
<li>年龄: {this.props.age}</li>
</ul>
);
}
}

ReactDOM.render(<Person name="yyt" age="18" gender="女"/>, document.getElementById("test"))
ReactDOM.render(<Person name="lc" age="19" gender="男"/>, document.getElementById("test2"))
</script>

image

批量传递 props

假如有很多的人, 或者人的信息是服务器传入的, 则肯定不是一个一个写的.

可以使用语法糖: ...p​ 来传递, 假设有对象 p, 属性名称一一对应, 则可以正常使用, 这就是展开运算. 当然, 这是 js 代码, 需要用 {}​ 括起来. 另外, 这种写法是 babel 独有的, 且只能用于标签传递, 别的地方不能这么干.

1
2
3
4
5
6
const persons = [
{name: "yyt", age: 18, gender: '女'},
{name: "lc", age: 19, gender: '男'}
]
ReactDOM.render(<Person {...persons[0]}/>, document.getElementById("test"))
ReactDOM.render(<Person {...persons[1]}/>, document.getElementById("test2"))

image

对 props 进行限制

假如说, 我希望所有人的 age 都 +1, 但是可能传入的 age 为: "18" ​, 一个字符串, 这个时候我如果要 +1, 则会出现显示年龄为 "181" ​ 的情况, 这不是我们所期望的.

又或者说, 现在性别没有传入的话, 我希望能提供一个默认值, 男或者女. 再或者说, 现在名字都没有传入, 直接报错, 也就是必须传入.

根据需求, 就可以设置 Types 了. 既然说了 types, 自然就是一个新的 js 库了: 直接引入即可:

1
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/prop-types/15.8.1/prop-types.min.js" type="application/javascript"></script>

引入后, 就可以对组件的标签属性进行限制了. 一旦引入, 全局就多了一个新的对象: PropTypes​, 方便进行限制.

同理, 默认值的设置也是类似的, 给对象添加一个 defaultProps 即可.

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
<script type="text/babel">
class Person extends React.Component {

render() {
// 通过解构赋值 获取三个属性
const {name, age, gender} = this.props;
return (
<ul>
<li>姓名: {name}</li>
<li>性别: {gender}</li>
<li>年龄: {age + 1}</li>
</ul>
);
}
}

// 添加标签属性的类型规则
Person.propTypes = {
name: PropTypes.string.isRequired,
gender: PropTypes.string
}

// 设置标签属性的默认值
Person.defaultProps = {
gender: "不知道~",
age: 18
}

ReactDOM.render(<Person age={18}/>, document.getElementById("test"))
ReactDOM.render(<Person name="yyt" gender="女"/>, document.getElementById("test2"))
</script>

特殊一些的, 如果我限制传入的只能是方法, 则不是 PropTypes.function​, 而是 PropTypes.func​, 避免冲突.

props 的简写方式

这里先注意一下, props 是只读的, 不允许修改! 如果修改就一定会报错

给类自身加东西, 我们不一定要写在类的外面的, 还可以直接写在类的里面, 直接写在类的里面就行, 这样子类的优越性就出现了: 所有和类相关的东西, 仅仅写在类的里面.

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
<script type="text/babel">
class Person extends React.Component {
render() {
// 通过解构赋值 获取三个属性
const {name, age, gender} = this.props;
return (
<ul>
<li>姓名: {name}</li>
<li>性别: {gender}</li>
<li>年龄: {age + 1}</li>
</ul>
);
}

// 设置标签类型
static propTypes = {
name: PropTypes.string.isRequired,
gender: PropTypes.string
}

// 设置标签默认值
static defaultProps = {
gender: "不知道~",
age: 18
}
}

ReactDOM.render(<Person age={18} name={"lc"}/>, document.getElementById("test"))
ReactDOM.render(<Person name="yyt" gender="女"/>, document.getElementById("test2"))
</script>

类式组件的构造器与 props

我们都知道, 类式组件是可以写构造器的, 其中会传入一个 props 参数. 更改上面的代码, 可以发现输出后的 props 内容如下:

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
<script type="text/babel">
class Person extends React.Component {
constructor(props) {
console.log(props);
super(props);
}

render() {
// 通过解构赋值 获取三个属性
const {name, age, gender} = this.props;
return (
<ul>
<li>姓名: {name}</li>
<li>性别: {gender}</li>
<li>年龄: {age + 1}</li>
</ul>
);
}

// 设置标签类型
static propTypes = {
name: PropTypes.string.isRequired,
gender: PropTypes.string
}

// 设置标签默认值
static defaultProps = {
gender: "不知道~",
age: 18
}
}

ReactDOM.render(<Person name={"yyt"} gender="女"/>, document.getElementById("test"))
</script>

这里的控制台输出如下:

image

虽然年龄参数没有进行传递, 它也存在一个默认值.

这里就算不传给 super 函数, 运行起来也是没有任何问题的. 所以为什么需要写构造器呢?

构造函数仅仅有两种用法:

  1. 通过给状态初始化
  2. 给事件处理器绑定实例, 比如 bind(this)​.

无论如何, 构造器只要写了, 必须要调用 super, 否则可能造成无法确认的错误.

总结就是: 构造器是否接收 props, 是否传递给 super, 取决于是否希望在构造器中通过 this 获取 props. (几乎用不到的)

函数式组件使用 props

对于函数式组件, 虽然没有 state, 但是是可以使用 props 的, 因为函数可以接收参数的.

对于函数来说, 会自动传入一个 props 对象, 通过解构赋值即可获取其中的内容.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script type="text/babel">
// 函数式组件
function Person2 (props) {
console.log(props);
// 解构赋值
const {name, age, gender} = props;
return (
<ul>
<li>姓名: {name}</li>
<li>年龄: {age}</li>
<li>性别: {gender}</li>
</ul>
);
}

ReactDOM.render(<Person2 name={"yyt"} gender="女" age={18}/>, document.getElementById("test"))
</script>

如果需要加上约束条件, 使用的时候是一样的, 是什么组件, 就给这个组件添加属性即可:

1
2
3
Person2.propTypes = {
name: PropTypes.string.isRequired,
}

总结 props

props 就是标签的属性, 传入的是 key-value 的形式, 获取的时候也是 key-value.

如果需要限制, 假如两个属性即可, 不过需要引入对应的库.

函数式组件也可以使用 props, 不过其他的就用不了了.

组件三大核心属性-refs 和事件处理

ref 可以获取到真实的 DOM!

字符串形式的 ref

假如我希望实现一个效果, 左边一个输入框, 点击按钮后, 弹出输入的内容; 右边的输入框, 失去焦点后, 弹出输入的内容.

先搭建好框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type="text/babel">
class Demo extends React.Component {
showData = () => {
// 展示左侧输入框的数据
console.log(this);
}
render() {
return (
<div>
<input type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示数据</button>
<input type="text" placeholder="失去焦点提示数据"/>
</div>
);
}
}
ReactDOM.render(<Demo/>, document.getElementById("app"));
</script>

这里, 我们想要获取到这个 input, 不需要写 document…之类的东西了, 可以使用 ref​ 对单独的内容进行标识. 只要标识了 input 框, 就可以看到, 对应的组件已经放入了类对象的 refs 中:

1
2
3
4
5
6
7
8
9
render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示数据</button>
<input type="text" placeholder="失去焦点提示数据"/>
</div>
);
}

image

注意, 这里获取的不是虚拟 DOM, 而是真实的 DOM, 所以该有的东西都有, 都可以进行获取, 甚至是修改.

这样, 就可以非常方便的获取到输入框中的内容了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script type="text/babel">
class Demo extends React.Component {
showData = () => {
// 通过结构 获取input输入框
const {input1} = this.refs;
alert(input1.value);
}

render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示数据</button>
<input type="text" placeholder="失去焦点提示数据"/>
</div>
);
}
}

ReactDOM.render(<Demo/>, document.getElementById("app"));
</script>

image

第二个输入框, 我们的需求是失去焦点, 根据之前的写法, onblur​, 这里推断为 onBlur​, 直接写, 至此完成案例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Demo {
showData2 = () => {
const {input2} = this.refs;
alert(input2.value);
}

render() {
// 失去焦点 对应的就是onBlur了
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示数据</button>
<input onBlur={this.showData2} ref="input2" type="text" placeholder="失去焦点提示数据"/>
</div>
);
}
}

image

综上, 其实 ref 就是原来的 ID 的改版, 让我们更加方便的获取到元素.

回调形式的 ref

其实字符串类型的 ref 是即将过时的内容, 因为它本质还是操作了真实的 DOM, 这可能导致一些未知的错误.

回调 ref, 顾名思义就是 ref 变成了一个回调函数. 我们不妨写一个回调函数输出传入的参数看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script type="text/babel">
class Demo extends React.Component {
render() {
// 失去焦点 对应的就是onBlur了
return (
<div>
<input ref={(s) => {
console.log(`Hello`, s);
}} type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
);
}
}

ReactDOM.render(<Demo/>, document.getElementById("app"));
</script>

image

可以看到, 默认传入的其实就是真实 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
<script type="text/babel">
class Demo extends React.Component {
showData = () => {
// input 也是可以解构赋值的
const {input1} = this;
// 直接从自身获取即可
alert(input1.value);
}

render() {
// 失去焦点 对应的就是onBlur了
return (
<div>
<input ref={(s) => {
this.input1 = s;
}} type="text" placeholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
);
}
}

ReactDOM.render(<Demo/>, document.getElementById("app"));
</script>

image

这样可以避免一些效率上的问题.

注意, ref 等于一个回调函数, react 会自动调用这个函数, 我们是不需要手动调用的.

ref 回调函数调用次数问题

可以阅读官网: 在更新这个组件的过程中, 其实 ref 中的内联函数会被调用两次的. 第一次传入的参数是 null, 第二次传入的参数才是 DOM 元素. 这是因为渲染的时候会出现一个新的函数实例, React 会清空原来的 ref 并且设置新的.

无论如何, 一般来说这是无关竟要的.

点击切换按钮, 导致页面重绘, 可以发现如下输出内容:

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
<script type="text/babel">
class Demo extends React.Component {
state = {isHot: false};

changeWeather = () => {
const {isHot} = this.state;
this.setState({isHot: !isHot});
}

render() {
const {isHot} = this.state;
return (
<div>
<h2>今天天气很 {isHot ? "炎热" : "寒冷"}</h2>
<input ref={(currentNode) => {
this.input1 = currentNode;
console.log("@", currentNode);
}} type="text"/>
<button onClick={() => {
alert(this.input1.value);
}}>点我提示数据
</button>
<button onClick={this.changeWeather}>点我切换添加</button>
</div>
);
}
}

ReactDOM.render(<Demo/>, document.getElementById("app"));
</script>

image

这里的 null 就是第一次渲染的时候传入的内容了. 不过, 原功能其实不会受到任何影响.

如果需要避免, 将内联函数转换为 class 的绑定函数即可只输出一次了.

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
<script type="text/babel">
class Demo extends React.Component {
state = {isHot: false};

changeWeather = () => {
const {isHot} = this.state;
this.setState({isHot: !isHot});
}

saveInput = (currentNode) => {
this.input1 = currentNode;
console.log("@", currentNode);
}

render() {
const {isHot} = this.state;
return (
<div>
<h2>今天天气很 {isHot ? "炎热" : "寒冷"}</h2>
<input ref={this.saveInput} type="text"/>
<button onClick={() => {
alert(this.input1.value);
}}>点我提示数据
</button>
<button onClick={this.changeWeather}>点我切换添加</button>
</div>
);
}
}

ReactDOM.render(<Demo/>, document.getElementById("app"));
</script>

运行后, 就算多次切换, 也没有任何的问题, 仅仅会输出一次.

image

createRef API

这是一个最新的 API, 如下代码运行, 控制台将会输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script type="text/babel">
class Demo extends React.Component {
/*
调用后 返回一个容器, 这个容器可以储存被ref标识的节点
*/
myRef = React.createRef()

render() {
return (
<div>
{/*这里, 发现你传入的是一个容器, 则会全自动的将该容器存入对应的容器当中*/}
<input ref={this.myRef} type="text"/>
<button onClick={() => {
{ /*输出看看容器是怎么存储的*/ }
console.log(this.myRef);
}}>点我提示数据
</button>
</div>
);
}
}

ReactDOM.render(<Demo/>, document.getElementById("app"));
</script>

image

可以看到, 我们可以使用 current 来获取这个真实 DOM, 随后获取对应的 value 了.

1
console.log(this.myRef.current.value);

不过这里需要注意, 这个容器只能储存一个真实 DOM, 不能够储存一堆的 DOM 进来. 所以繁琐的就是会创建许多的容器, 不过确实是目前 React 最为推荐的形式.

总结 ref

不推荐字符串类型的 ref, 推荐回调内联类型或者使用 API. 使用的时候, 根据想法使用即可.

实话说, 内联函数用的是最多的, 即便刷新可能出现问题, 但是这种问题是少之又少的, 可以忽略.

事件处理

其实, React 中的事件处理拥有一般规律:

  1. 通过 onXXX 来指定事件处理的函数, 一定注意大小写.
  2. 这是因为 React 中使用的是自定义的合成事件, 而不是原生的 DOM 事件.
  3. 同时, React 中的事件是通过事件委托的形式处理的
  4. 通过 event.target 得到发生事件的 DOM 元素对象.

不过, 这里结合前面的 ref, 提示: 不要过度依赖 ref! 推荐使用 evenv.target 来获取 DOM 元素对象

收集表单数据

非受控组件

假设我们制作一个简单的登陆界面出来, 既然是表单, 自然使用的是 <form>​ 标签了. 如果不指定请求方式, 默认的请求方式是 GET 请求, 同时点击按钮就会自动提交其中的内容.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script type="text/babel">
class Login extends React.Component {
render() {
return (
<form action="http://localhost:8080">
用户名: <input type="text" name="username"/>
<br />
密码: <input type="password" name="password"/>
<br />
<button>登陆</button>
</form>
);
}
}

ReactDOM.render(<Login/>, document.getElementById("app"));
</script>

点击按钮后, 跳转目录为: http://localhost:8080/?username=123&password=456​.

对于 input 框之类的东西, 可以使用 ref 获取, 但是我希望不要让页面进行跳转, 可以通过传入的 event 来配置阻止默认跳转

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
<script type="text/babel">
class Login extends React.Component {
handleSubmit = (event) => {
// 阻止表单提交
event.preventDefault()
const {username, password} = this;
// 输出用户名
alert(`用户名: ${username.value} | 密码: ${password.value}`);
}

render() {
return (
<form action="http://localhost:8080" onSubmit={this.handleSubmit}>
用户名: <input type="text" ref={(c) => {
this.username = c
}} name="username"/>
<br/>
密码: <input type="password" ref={(c) => {
this.password = c;
}} name="password"/>
<br/>
<button>登陆</button>
</form>
);
}
}

ReactDOM.render(<Login/>, document.getElementById("app"));
</script>

这样, 我们就可以正常获取内容, 并且阻止跳转了. 上面这些, 其实就是非受控组件了.

受控组件

如果我们有很多很多个表单, 那么这里的 ref 会非常繁琐. 受控组件, 就是不写 ref. 我们可以思考, 通过一个全局状态来维护输入的内容, 这样子会方便很多的!

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
<script type="text/babel">
class Login extends React.Component {
state = {
username: "",
password: ""
}

handleSubmit = (event) => {
event.preventDefault();
const {password, username} = this.state;
alert(`用户名: ${username}, 密码: ${password}`)
}

saveUsername = (event) => {
// 保存
this.setState({
username: event.target.value
})
}

savePassword = (event) => {
this.setState({
password: event.target.value
})
}

render() {
return (
<form action="http://localhost:8080" onSubmit={this.handleSubmit}>
用户名: <input type="text" onChange={this.saveUsername} name="username"/>
<br/>
密码: <input type="password" onChange={this.savePassword} name="password"/>
<br/>
<button>登陆</button>
</form>
);
}
}

ReactDOM.render(<Login/>, document.getElementById("app"));
</script>

为了简化代码, 可以进行 高阶函数 函数柯里化, 详见对应文档. 但是呢, 其实就算没有函数的柯里化, 也是没有什么大问题的. 不用柯里化实现, 就是直接接收两个参数. 根据第一个参数判断类型, 第二个参数传入需要的值即可:

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
saveFormData = (dataType, value) => {
// 不使用函数柯里化的写法
this.setState({
[dataType]: value
})
}

render() {
return (
<form action="" onSubmit={this.handleSubmit}>
用户名: <input type="text" onChange={
(event) => {
this.saveFormData("username", event.target.value)
}
}/>
<br/>
密码: <input type="password" onChange={
(event) => {
this.saveFormData("password", event.target.value)
}
}/>
<br/>
<button>登陆</button>
</form>
);
}

这样子也能够实现正常的功能.

★组件的生命周期 (旧)

引入生命周期

这里, 我们先生成这样一个静态的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script type="text/babel">
// 创建组件
class Life extends React.Component {
render() {
return (
<div>
<h2>React 学不会怎么办?</h2>
<button>不活啦</button>
</div>
);
}
}

// 渲染组件
ReactDOM.render(<Life/>, document.getElementById("app"));
</script>

我希望上面的文本从纯黑色逐渐透明, 最后看不到了, 然后直接回到一开始的样子; 点击按钮后, 直接删除整个组件.

我们先实现最简单的, 就是删除整个组件. 之前我们学习的, 就是把组件渲染到页面上, 其实有一个更加专业的说法, 就是把组件 挂载 到页面上. 既然存在挂载, 自然就存在卸载了.

我们添加一个回调函数, 卸载这个组件, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Life extends React.Component {
death = () => {
// 移除整个组件.
ReactDOM.unmountComponentAtNode(document.getElementById("app"));
}
render() {
return (
<div>
<h2>React 学不会怎么办?</h2>
<button onClick={this.death}>不活啦</button>
</div>
);
}
}

至此回到页面, 已经可以通过点击按钮来卸载组件了.


接下来, 我希望透明度不断地变化; 既然页面变了, 则肯定是状态中有内容变了! 所以我们需要维护一个状态. 这个状态需要用循环定时器来操作, 循环定时器肯定不能直接写在类里面, 所以可以写在 render 里面试试?

发现频率越来越快, 这就是因为引发了一个无限的递归. 只要渲染, 就会开启定时器, 定时器执行后, 再次触发渲染, 以此类推, 循环定时器会不断的扩展, 直到开启无数个定时器, 导致系统错误!

所以, 这样子是不合适的, 我们应该放在哪里呢? 我们希望的是, 组件挂载到页面上的时候, 就开启定时器!

这就需要用到生命周期了! 在组件挂载到页面上的时候, 会自动调用一个函数: componentDidMount()​ 这个函数就是一个默认的函数了, 意思就是当组件挂在后, 自动调用.

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
<script type="text/babel">
// 创建组件
class Life extends React.Component {
// 初始化状态
state = {opacity: 1}

death = () => {
// 移除整个组件.
ReactDOM.unmountComponentAtNode(document.getElementById("app"));
}

componentDidMount() {
setInterval(() => {
// 获取原来的状态
let {opacity} = this.state;
// 减小
opacity -= 0.1;
if (opacity < 0) opacity = 1;
// 更新状态
this.setState({
opacity: opacity
})
}, 200)
}

render() {
return (
<div>
<h2 style={{opacity: this.state.opacity}}>React 学不会怎么办?</h2>
<button onClick={this.death}>不活啦</button>
</div>
);
}
}

// 渲染组件
ReactDOM.render(<Life/>, document.getElementById("app"));
</script>

这里的 componentDidMount​ 是不需要按照用户自定义函数的格式书写的, 因为这是一个生命周期函数, 默认存在!

另外, 该函数的 this 就是对象实例, 所以不会出现 this 找不到的情况!

GIF 2025-5-12 19-29-57

那么, 真的结束了吗? 如果我们尝试删除这个组件, 其实定时器根本没有正常关闭. 我们可以在定义定时器的时候, 挂在对象身上, 随后在删除的时候, 停止定时器即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
death = () => {
// 清空定时器 防止报错
clearInterval(this.timer);
// 移除整个组件.
ReactDOM.unmountComponentAtNode(document.getElementById("app"));
}

componentDidMount() {
this.timer = setInterval(() => {
// 获取原来的状态
let {opacity} = this.state;
// 减小
opacity -= 0.1;
if (opacity < 0) opacity = 1;
// 更新状态
this.setState({
opacity: opacity
})
}, 200)
}

但是我不想手动调用, 有没有这样一个函数, 在组件卸载的时候, 也会自动调用呢?

当然是有的, 有一个生命周期函数, 可以进行收尾的工作: componentWillUnmount()​. 放在里面即可:

1
2
3
4
componentWillUnmount() {
// 清空定时器 防止报错
clearInterval(this.timer);
}

这就是组件的一生, React 会帮助我们, 在应该调用某个函数的时候, 自动调用对应的函数. 生命周期函数, 也叫做生命周期回调函数, 或者钩子函数.

组件挂载流程

生命周期函数流程

下面的左图是旧版的生命周期函数图, 后面的是新版的, 总体来说, 新版是删掉了几个生命周期函数, 缩减趋势.

image

生命周期函数写的前后关系, 和调用的前后关系没有任何关系. 只要写了, 就会自动进行调用.

为了方便, 使用最简单的计数器代码来演示:

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
<script type="text/babel">
class Count extends React.Component {
// 初始化状态
state = {
count: 0
}

// 按钮+1的回调函数
add = () => {
// 获取原状态
const {count} = this.state
// +1
this.setState({
count: count + 1
})
}

render() {
const {count} = this.state
return (
<div>
<h2>当前求和为: {count}</h2>
<button onClick={this.add}>Click +1</button>
</div>
);
}
}

ReactDOM.render(<Count/>, document.getElementById("app"));
</script>

一开始, 其实调用的是构造器, 干脆写上吧.

1
2
3
4
constructor(props) {
console.log("Count 构造器被调用")
super(props);
}

随后, 实现几个钩子函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 组件将要挂载
componentWillMount() {
console.log("Count 组件将要挂载 componentWillMount")
}
// 组件挂载完毕
componentDidMount(){
console.log("Count 组件挂载完毕 componentDidMount")
}

render() {
console.log("Count 渲染调用 render")
...
}

运行后, 程序输出如下:

image

目前, 完全符合上面的图例; 后面我们假如再加上一个用来卸载组件的按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
render() {
console.log("Count 渲染调用 render")
const {count} = this.state
return (
<div>
<h2>当前求和为: {count}</h2>
<button onClick={this.add}>Click +1</button>
<button onClick={() => {
ReactDOM.unmountComponentAtNode(document.getElementById('app'));
}}>卸载组件
</button>
</div>
);
}

随后, 加上卸载组件相关的钩子函数:

1
2
3
componentWillUnmount(){
console.log("Count 组件卸载 componentWillUnmount")
}

GIF 2025-5-12 19-59-03

这个钩子是组件将要卸载的钩子.


还有一个钩子函数: shouldComponentUpdate​, 这个钩子函数的作用是, 当你更改了页面元素, 就会问这个工具, 为真则会更新页面, 否则不会更新, 我们也可以手动的写一下这个钩子. 不过! 这个函数必须返回 true 或者 false!

1
2
3
4
shouldComponentUpdate(){
console.log("Count 是否需要更新 shouldComponentUpdate")
return true;
}

这是控制组件更新的阀门, 如果返回 false, 就算更新了数据, 也无法更新组件中的内容.

既然有是否应该更新, 自然也有组件将要更新的钩子了:

1
2
3
componentWillUpdate(){
console.log("Count 组件将要更新 componentWillUpdate")
}

GIF 2025-5-12 20-04-38


除了刚才的内容, 还有一个路线, 就是强制更新. 正常更新就是我没有更改状态的数据, 我也想要更新数据, 就是另外一个函数了: forceUpdate()​ 这个函数会直接绕过阀门, 直接进行更新.

1
2
3
4
5
<button onClick={() => {
this.forceUpdate();
}}>
强制更新
</button>

GIF 2025-5-12 20-07-56

父组件 render

这里创建另外一个组件 A, 有基本的渲染结构; 同理, 我创建另外一个组件 B, 我希望 A 组件中存在另外一个组件 B. 这样子 A 和 B 组件就出现了一个父子组件的关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义A组件
class A extends React.Component {
render() {
return (
<div>
<h1>A组件</h1>
{/*写一个B组件 作为内部的子组件*/}
<B/>
</div>
);
}
}
// 定义B组件
class B extends React.Component {
render() {
return (
<div>
<h2>B组件</h2>
</div>
);
}
}

假设接下来, 我的父组件想要给子组件传递参数, 那么子组件就可以接收到参数了.

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
class A extends React.Component {
state = {
counter: 0
}
render() {
return (
<div>
<h1>A组件</h1>
<button onClick={() => {
const {counter} = this.state;
this.setState({
counter: counter + 1
})
}}>+1
</button>
{/*写一个B组件 作为内部的子组件*/}
<B myCounter={this.state.counter}/>
</div>
);
}
}
// 定义B组件
class B extends React.Component {
render() {
return (
<div>
<h2>B组件</h2>
<h3>Counter is {this.props.myCounter}</h3>
</div>
);
}
}

这里, 子组件会触发一个生命周期钩子: componentWillReceiveProps​, 可以在子组件中调用试试.

1
2
3
4
5
6
7
8
9
10
11
12
13
class B extends React.Component {
componentWillReceiveProps() {
console.log("B -> componentWillReceiveProps")
}
render() {
return (
<div>
<h2>B组件</h2>
<h3>Counter is {this.props.myCounter}</h3>
</div>
);
}
}

运行后, 发现控制台没有直接输出!

image

这是因为第一次传入的, 也就是初始化是不算的. 点击按钮后, 可以正常触发函数调用.

image

另外, 这个方法也是会自动传入参数的, 可以试试输出 props 试试.

image

这个参数其实就是传递的参数的对象形式.


假如这里也有其他的几个钩子函数, 则会按照正常的顺序进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class B extends React.Component {
componentWillReceiveProps(props) {
console.log("B -> componentWillReceiveProps", props)
}
shouldComponentUpdate() {
console.log("B -> shouldComponentUpdate")
return true;
}
componentWillUpdate(){
console.log("B -> componentWillUpdate")
}
componentDidUpdate(){
console.log("B -> componentDidUpdate")
}
render() {
console.log("B -> render")
return (
<div>
<h2>B组件</h2>
<h3>Counter is {this.props.myCounter}</h3>
</div>
);
}
}

image

生命周期总结 (旧)

  1. 初始化阶段, 由 ReactDOM.render() 触发初次渲染
    1. constructor()
    2. componentWillMount()
    3. render()
    4. componentDidMount() 一般来说, 可以做一些初始化的事情
  2. 更新阶段, 由 this.setState() 或者父组件重写 render 触发
    1. shouldComponentUpdate()
    2. componentWillUpdate()
    3. render()
      1. 必要内容!
    4. componentDidUpdate()
    5. componentWillUnmount() 一般做一些收尾的事情, 比如关闭定时器之类的事情

总体来说, 以上就是组件的调用顺序了, 还是都挺见名知意的, 基本都能够根据名字知道对应的功能.

★组件的生命周期 (新)

新旧生命周期对比

image

首先需要明确, 新的 React 版本是支持老版本的钩子的, 完全没有任何问题, 不过会有一定的警报, 告诉我们不能这么用.

在新的钩子中, 如下三个钩子都应当在前面加上 UNSAFE_​ 前缀.

  1. componentWillReceiveProps
  2. componentWillMount
  3. componentDidMount

不过功能是一样的, 简单记忆, 除了最后的卸载组件, 其他的和 Will 相关的钩子, 都需要加上 UNSAFE_​ 前缀.

这个 unsafe 表示的是, 在以后的异步渲染中, 可能出现 BUG.

同时, 出现了两个新的钩子:

  1. getDerivedStateFromProps
  2. getSnapshotBeforeUpdate

不过, 这两个新的钩子其实用的情况极少, 基本用不到就是了.

1
2
3
4
5
6
7
8
9
10
// 新的钩子
// 必须是一个静态的方法
static getDerivedStateFromProps() {
console.log("getDerivedStateFromProps")
// 同时必须返回一个状态对象或者null
// 其实就是状态里面的东西
return {
count: 108
};
}

image

这里只要记住, state 取决于 props 就是了.

也就是, 如果 state 的值, 任何时候都取决于 props, 则可以使用这个钩子.


对于另外一个新的钩子, 使用起来差不多, 必须要返回 null 或者一个快照.

快照, 其实就是之前的那个状态, 也就是之前的情况.

1
2
3
4
5
getSnapshotBeforeUpdate() {
console.log("getSnapshotBeforeUpdate");
// 返回的快照 就是之前的情况
return null;
}

这个生命周期钩子还是有一些用处的, 不过这里我就不做更多的演示啦

生命周期总结 (新)

  1. 初始化阶段, 由 ReactDOM.render() 触发初次渲染
    1. constructor()
    2. getDerivedStateFromProps()
    3. render()
    4. componentDidMount()
  2. 更新阶段, 由 this.setState() 或者父组件重写 render 触发
    1. ​getDerivedStateFromProps()
    2. shouldComponentUpdate()
    3. render()
    4. getSnapshotBeforeUpdate() ​​
    5. componentDidUpdate()
  3. 卸载组件
    1. componentWillUnmount()

其实最常用的压根没动, 该怎么样就还是怎么样的.

React 应用

使用脚手架创建 react 应用

什么是脚手架

如果在现实中, 就是工人干活的东西, 给工人提供了更加安全的环境, 以及更为方便的各种工具.

在编程中, 脚手架就是让我们更快速的创建一个基于某个库的基本框架; 我们直接使用官方打造好的脚手架即可.

使用脚手架

官方推荐, 我们直接使用如下代码, 即可创建一个 React App 出来.

首先切换到想要创建项目的目录, 然后输入如下命令, 即可创建一个名称叫做 my-app​ 的项目.

1
npm create vite@latest my-app -- --template react

注意, 考虑到 create-react-app 逐步弃用, 这里开始我使用 vite 进行代替.

随后, 根据提示安装依赖即可: npm install​.

安装后, 即可启动项目, 在浏览器中看到这样的内容: npm run dev

image

目前为止, 已经通过脚手架创建了一个最简单的项目!

熟悉文件

image

查看文件树, 发现这些文件.

其中, 最重要的其实就是 src​ 目录, 也就是代码目录, 就是我们写代码的地方.

不妨看看 App.jsx ​, 改一改内容:

内容 显示
image image

直接保存, 发现已经可以成功渲染了!

上面目录的前两个文件夹就不再过多赘述, 从 public 开始进行介绍.

  • public 静态资源文件夹
    • favicon.icon 网站图标
    • … 一系列的静态资源文件
  • src
    • App.jsx App 组件, 使用默认暴露, 暴露出去方便调用 这里的 App 就是一切子组件的根组件
    • App.css App 组件对应的 css 样式文件
    • index.css 通用型的 css 文件, 大家通用
    • main.jsx 程序的入口默认使用了 React 的严格模式, 方便进行代码校验
  • index.html 主页面 这里需要注意, 页面只有一个! 我们写的是 SPA 应用, 没有那么多的 html 文件.

删除没什么用处的代码, 仅仅保留空项目, 文件如下:

image

Hello 组件

定义组件

一个组件就是一个单独的 jsx 文件, 文件名就是组件名, 这是命名规范. 这里创建一个 Hello.jsx ​组件, 用类的形式来写:

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

class Hello extends Component {
render() {
return (
<>
<h2>Hello World!</h2>
</>
);
}
}

// 默认暴露该组件
export default Hello

随后, 定义好了组件, 我们在 App 中引入并且按照组件的方式使用, 即可看到效果!

1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入自定义组件
import Hello from "./Hello.jsx";

function App() {
return (
<>
{/*调用自定义组件*/}
<Hello/>
</>
)
}

export default App

image

但是呢, 这样子所有的代码会混在一起, 不如把所有的组件放在一个单独的文件夹中.

image

设置 css

同理, 创建组件同名的 css​ 文件, 写入样式即可.

1
2
3
4
5
6
7
8
9
10
11
12
// 引入样式
import "./Hello.css";

class Hello extends Component {
render() {
return (
<>
<h2 className="title">Hello World!</h2>
</>
);
}
}

image

这里的引入, 直接使用 import 即可, 不是 link 之类的东西了.

不过, 这里还是需要注意, 虽然放在了组件目录下, 但是组件多了还是不好管理; 所以我们可以再次新建一个组件名称的目录用来存放组件的 css 和 jsx 代码文件, 例如下面这样子:

image

使用组件

对于多个组件, 可以直接按照相同的案例进行使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 引入自定义组件
import Hello from "./components/Hello/Hello.jsx";
import Welcome from "./components/Welcome/Welcome.jsx"

function App() {
return (
<>
{/*调用自定义组件*/}
<Hello/>
<Welcome/>
</>
)
}

export default App

image


另外, 如果引入的 jsx 文件是 index.jsx, 则直接引入到对应的目录即可正常访问, 可以理解为默认访问 index.

1
2
3
4
5
6
7
8
9
10
11
12
// 对于更简单的写法
import Student from "./components/Student";

function App() {
return (
<>
<Student/>
</>
)
}

export default App

image

遇到这种情况, 知道就好 不要两眼一抹黑就行.

样式的模块化

为什么要做样式的模块化

想一想, 刚才写了很多的 css 文件, 他们最终都会汇总到 App.jsx 中, 显然这样子样式会出现冲突的效果. 我们并不希望这种情况发生. 所以, 我们要做样式的模块化.

模块化 css

首先, 我们要修改 css 的文件名, 在 css 的前面加上 module​, 表示是模块化的 css, 例如: Test.module.css​.

随后代码中, 就可以使用 import xx from yy​ 的形式了, 例如 Hello 组件:

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

// 引入样式
import hello from "./Hello.module.css"

class Hello extends Component {
render() {
return (
<>
<h2 className={hello.title}>Hello World!</h2>
</>
);
}
}

// 默认暴露该组件
export default Hello

组件化编码流程

首先, 肯定得有 components 文件夹, 以及 App.jsx, 以及 index.jsx.

首先实现入口文件, 就是 index.jsx:

1
2
3
4
5
6
7
8
9
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

这些代码基本都是固定的, 直接写就好, 随后按照如下步骤:

  1. 拆分组件: 拆分界面, 抽取组件
  2. 实现静态组件: 使用组件实现静态页面效果
  3. 实现动态组件:
    1. 动态显示初始化数据
      1. 数据类型
      2. 数据名称
      3. 数据保存在哪个组件‍
    2. 绑定各种事件.

至此, 按照顺序来写代码, 就可以写好一个项目了.

[!info] 项目实战
可以前往后面的归档中, 查看这个项目: TODO List

消息订阅-发布机制

介绍与安装库

其实兄弟组件之间通信是不需要借助父组件的, 这样子会非常的麻烦. 这里我们就需要借助消息订阅与发布了. 有很多的机制都实现了这个, 我们使用一个比较主流的: pubsub.

这个库的使用其实非常的简单, 首先我们需要安装这个库:

1
npm i pubsub-js

安装后, 我们可以在组件中订阅一个消息, 并且设置这个消息的对应回调函数. 语法其实非常的简单, 就是在一个 B 组件中订阅消息, 只要有其他组件, A 组件发布了这个消息, 就会调用后面的回调函数.

[!info] 这里需要注意, 我们是在需要接收数据的组件中订阅消息, 在发送消息的组件中发布消息.

函数里面可以做任何事情!

使用案例

最简单的, 我希望 A 输入框中的数据可以直接同步到 B 输入框. 首先, 搭建框架如下:

1
2
3
4
5
6
7
8
9
10
11
/* App.jsx */
class App extends Component {
render() {
return (
<>
<A/>
<B/>
</>
)
}
}

|217

既然是 B 组件接收参数, 则应当在 B 组件中订阅消息! 肯定是一挂载就进行订阅, 不妨使用生命周期钩子函数: componentDidMount.

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
import React, {Component} from 'react';  

// 引入pubsub库
import PubSub from 'pubsub-js'

class B extends Component {
// 一挂载就订阅事件
componentDidMount() {
// 订阅的基本语法如下
PubSub.subscribe('get-my-input', (_, data) => {
console.log(data)
})
}

render() {
return (
<div>
<h1>这里是B组件</h1>
<input placeholder={"这是B组件的输入框"}/>
</div>
);
}
}

export default B;

随后, 我们在 A 组件中发布消息, 写在监听中即可:

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

import PubSub from "pubsub-js";

class A extends Component {
changeInput = (event) => {
console.log(event.target.value)
// 发布消息
PubSub.publish('get-my-input', {text: event.target.value})
}

render() {
return (
<div>
<h1>这里是A组件</h1>
<input onChange={this.changeInput} placeholder={"这里是A组件的输入框"}/>
</div>
);
}
}

export default A;

回到浏览器中查看效果.

|425

数据已经在兄弟组件中获取了! 那么不妨直接改成 react 的设置 state 试试.

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
// B.jsx
import React, {Component} from 'react';

// 引入pubsub库
import PubSub from "pubsub-js";

class B extends Component {
// 设置状态对象
state = {
inputValue: ""
}

// 一挂载就订阅事件
componentDidMount() {
// 订阅的基本语法如下
PubSub.subscribe('get-my-input', (_, data) => {
this.setState({
inputValue: data.text
})
})
}

render() {
return (
<div>
<h1>这里是B组件</h1>
<input readOnly={true} placeholder={"这是B组件的输入框"} value={this.state.inputValue}/>
</div>
);
}
}

export default B;

回到浏览器, 效果实现了!

|375

进一步扩展

其实, 这种消息订阅与发布的技术是可以扩展到全局的. 就算不是兄弟组件, 也是可以实现组件间通信的! 要是还可以传递函数, 就更加轻松了!

React 路由

相关理解

SPA 的理解

SPA, 其实就是指的单页 Web 应用. 整个应用只有一个完整的页面. 点击页面中的不同链接并不会刷新页面, 只会做页面的局部刷新.

对于多页面的网页来说, 可以发现是不同的页面之间进行切换的, 因为有两个页面. 最经典的就是浏览器搜索内容, 直接就是多页面跳来跳去的.

但页面中, 就是点击主页, 展示的是 Home 组件, 点击关于, 展示的是 About 组件, 页面是完全没有发生变化的.

另外, 数据都需要通过 ajax 来获取, 并且在前端异步显示. 因为组件中在挂载的时候才会加载数据, 所以我们必须通过 ajax 来获取对应的数据.

[!info] 总结
虽然单页面, 但是多组件

路由的理解

浏览器中的路由其实就是浏览器路径的问题, 拿到路径以后, 根据路径来判断需要展示哪个组件, 这就是一种一一对应的关系.

每一个路径都对应的是一个组件, 这里的 key 就是地址栏的 path (不包含前面的 https 以及域名之类的东西, 仅仅看后面的路径).

react-router-dom 的理解

react-router 是 react 的一个插件库. 其实有很多作用, 一个是给 Web 使用, 就是我们目前学习的东西; 另外是给 native 用的, 还有一个就是 any, 在哪里都可以使用.

我们只需要学习 web 部分的就好, 所以我们学习的其实叫做: react-router-dom. 另外, 我们只要用 react 写网页, 基本都会使用到这个库. 另外, 这是官方维护的路由库.

基本路由使用

安装库

虽然是官方维护的库, 但是也不是默认就安装好的. 我们需要手动的进行安装:

[!error] 警告
这里学习的是 router 5 版本, 所以需要指定版本号, 否则可能使用起来并不一样.

1
npm i react-router-dom@5

基础使用

我们来实现一个切换效果. 这里我们需要养成一个基本的习惯, 拆分组件! 对于一个页面, 我们需要确认几个部分: 导航区, 展示区. 我们的原理就是点击导航区, 更改路由, 然后被检测到了, 就修改展示的组件内容.

变化的内容, 其实就是一个个的组件, 一开始是 Home 组件, 另外一个是 About 组件. 我们直接创建两个组件即可.

[!info] 组件位置

考虑到这个组件可以是一个页面, 所以我们应当创建一个新的目录: pages 用来存储.

左侧的导航和上面的标题是直接展示的, 不需要修改, 我们只需要修改变化的地方为组件即可.

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
// App.jsx
import {Component} from "react";
import './App.css'

class App extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<a className="list-group-item active" href="./about.html">About</a>
<a className="list-group-item" href="./home.html">Home</a>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
<h3>我是About的内容</h3>
</div>
</div>
</div>
</div>
</div>
)
}
}

export default App

这是原本的静态页面, 我们要做的就是转换为组件, 路由的形式.


原生中, 使用 <a> 标签来实现路由的跳转, 我们在这里自然也要使用一个标签, 路由链接. 路由链接是一个组件: Link, 按照 a 标签的格式写就行.

直接写的话, 很遗憾报错了. 这是因为, 我们的路由链接是需要写在 Router 中的.

当然, Router 也会报错. 这是因为, 我们需要的其实是告诉他, 我需要用哪种路由. 这里有两种, 一种是 HashRouter, 路径中会有一个 #, 比如这样子:

|228

使用浏览器历史记录的时候, 则可以使用 BrowserRouter:

|197

通过点击, 路由已经可以更改浏览器的链接并且没有刷新了!

1
2
3
4
<BrowserRouter>  
<Link className="list-group-item" to="/home">Home</Link>
<Link className="list-group-item" to="/about">About</Link>
</BrowserRouter>

|350

接下来, 就是根据路由来修改其中的组件了. 我们肯定不能直接写在那里, 否则就是全部显示了; 我们需要引入一个新的标签: 注册路由, 其实就是编写路由链接, 组件名称为: Route.

使用的时候, 这个组件是一个自结束标签, 通过传递 props 的形式来告诉浏览器显示什么:

1
2
3
4
<div className="panel-body">  
<Route path={"/about"} component={About}/>
<Route path={"/home"} component={Home}/>
</div>

然而, 又报错了:

这个报错还是一样的, 需要我们在外部包裹一个标签.

1
2
3
4
<BrowserRouter>  
<Route path={"/about"} component={About}/>
<Route path={"/home"} component={Home}/>
</BrowserRouter>

成功了吗? 并没有.

|375

现在的路由确实发生了变化, 但是组件渲染并没有. 这是因为一个页面只能有一个路由, 那么我们尝试将这两个地方包裹在一个路由中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<BrowserRouter>
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<Link className="list-group-item" to="/home">Home</Link>
<Link className="list-group-item" to="/about">About</Link>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
<Route path={"/about"} component={About}/>
<Route path={"/home"} component={Home}/>
</div>
</div>
</div>
</BrowserRouter>

目前, 页面已经可以正常进行切换了. 但是这样并不合理, 因为我们可能会在后面添加更多的内容, 这样页面的复杂度会大大上升.

所以不妨想一想, 我们的整个程序其实都在使用这个 Router, 那么为什么不直接把整个 App 组件包裹起来呢? 所以我们删掉刚才的 Router 部分, 来到 main.jsx 中, 给 App 包裹上 Router.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import App from './App.jsx'

// 引入Router
import {BrowserRouter} from 'react-router-dom'

createRoot(document.getElementById('root')).render(
<StrictMode>
{/*直接包裹即可*/}
<BrowserRouter>
<App/>
</BrowserRouter>
</StrictMode>,
)

至此, 我们已经可以正常使用该路由了.

|400

路由组件与一般组件

总结来说其实就是三点:

  1. 写法不同, 一般组件直接 <Header/> 即可.
  2. 存放位置不同, 一般组件在 components 目录, 路由在 pages 目录
  3. 接收到的 props 不同, 一般组件穿啥收到啥, 但是路由组件可以收到三个固定的属性

第三点最为重要, 我们会用到一些特定的内容, 不过这里暂时不做介绍.

基本使用

参考之前的 Link 组件, 它是没有点击后的高亮效果的. 如果我们需要让它有一个高亮的效果, 需要使用一个升级版的 Link: NavLink.

1
2
3
4
<div className="list-group">
<NavLink className="list-group-item" to="/home">Home</NavLink>
<NavLink className="list-group-item" to="/about">About</NavLink>
</div>

这里其实改后就直接生效了!

|350

它会自动的添加 active 属性. 不过因为现在用的 css 是 Bootstrap 的样式, 碰巧了也是 active, 所以我们肯定不能这么干. NavLink 是可以传入一个 prop 的, 叫做 activeClassName, 那就很好了, 直接传入高亮样式的类名即可:

假如现在我有这样的样式:

1
2
3
4
5
6
<style>
.kaede-active {
background-color: #fdc000 !important;
color: white !important;
}
</style>

就可以通过传递类名的方式直接追加了:

1
<NavLink activeClassName={"kaede-active"} className="list-group-item" to="/home">Home</NavLink>

|350

假如有很多的这种自定义样式的话, 我们肯定不能写一堆的这种 className. 我们不妨进行一下封装, 定义一个自己的 NavLink, 然后通过参数的方式, 传入 to 的地址以及显示的内容即可.

[!note] MyNavLink 属于什么组件呢?

属于一般组件.

另外, NavLink 我们是无法直接实现的, 所以我们必须要使用 react-router-dom 中的内容.

所以我们可以设计自定义组件如下:

1
2
3
4
5
6
7
8
9
10
11
12
import React, {Component} from 'react';
import {NavLink} from "react-router-dom";

class MyNavLink extends Component {
render() {
return (
<NavLink className="list-group-item" to={this.props.to}>{this.props.title}</NavLink>
);
}
}

export default MyNavLink;

随后传入对应的参数, 即可看到正确的效果了:

1
2
3
// App.jsx
<MyNavLink to={"/about"} title={"About"}/>
<MyNavLink to={"/home"} title={"Home"}/>

|450

但是呢, 假如我的 MyNavLink 中需要传递七八标签属性呢? 这样子的效率就比较低了. 我们还是可以使用三点运算符来简化写法, 直接解构传入的 props 作为属性即可:

1
<NavLink className="list-group-item" {...this.props}>{this.props.title}</NavLink>

然而, 标题这种东西最舒服的其实是按照浏览器标签的形式直接使用, 而不是作为参数进行传递.

1
2
3
// App.jsx
<MyNavLink to={"/home"}>Home</MyNavLink>
<MyNavLink to={"/about"}>About</MyNavLink>

这种形式的传参应该如何进行接受呢? 这种传参是传入了一个标签体, 之前我们获取的都是标签属性. 不过标签体其实就是一个特殊的标签属性: children. 所以可以直接在 MyNavLink 中进行接收:

1
<NavLink className="list-group-item" to={this.props.to}>{this.props.children}</NavLink>

现在就已经可以正常显示效果了; 但是似乎还是有一些麻烦: 既然 children 是一个属性, 为什么不直接让 children 为传入的 props 呢? 那么为什么不直接使用三点运算符呢?

于是, 我们可以得到最简单的, 最好用的写法:

1
<NavLink className="list-group-item" {...this.props}></NavLink>

|375

Switch 的使用

我们在注册路由的时候, 如果注册的路由特别的多, 可能会导致一些效率上面的问题, 这是我们不想看到的. 最优方案就是匹配到应有的组件后, 停止匹配.

然而默认情况下, 假如有两个路由相同的组件, 代码和渲染效果如下:

1
2
3
<Route path={"/about"} component={About}/>
<Route path={"/home"} component={Home}/>
<Route path={"/home"} component={Home}/>

|375

显然, 它会一个一个路由的判断, 直到所有路由遍历结束. 这就已经说明了它效率上的问题.

为了解决这个问题, 这里需要引入另外一个组件: Switch 组件. 其实使用起来非常简单, 使用 Switch 包裹所有注册的路由即可:

1
2
3
4
5
<Switch>
<Route path={"/about"} component={About}/>
<Route path={"/home"} component={Home}/>
<Route path={"/home"} component={Home}/>
</Switch>

这样页面中, 只要匹配到了就不会继续往下了.

|375

解决样式丢失问题

[!error] 该问题无法复现, BUG 已修复
但是该问题的解决方案仍然具备参考价值:

  1. index.html 中, 引入样式的时候不要写 ./, 而是要写 / 或者 %PUBLIC_URL%
  2. 使用 HashRouter

模糊匹配与严格匹配

假如说, 我的路由设置为匹配 /home, 但是 NavLink 为 /home/a/b, 会怎么样呢?

|500

正常运行了! 这就是模糊匹配. 简单说, Route 中需要的, 一个都不能少, 但是如果给的多了就没事了, 可以正常运行. 另外, 要的不能少, 顺序也不能变. 不能说要的是 /home, 给的是 /a/home/b.

如何开启精准匹配呢? 就是给的东西必须和要的东西完全一样? 其实就是给一个属性的事.

1
<Route exact={true} path={"/about"} component={About}/>

这样就 OK 了, 只有精准匹配才会触发该路由.

[!error] 不要随便开启严格匹配

只有页面出现了诡异的状况, 才需要开启严格匹配, 否则可能导致诡异的状况.

Redirect 的使用

我们直接打开网页的时候, 实际上什么都没有选中, 就像下面这种:

|425

一般来说, 上来就会默认选择一个的, 比如主页. 这里我们需要使用到一个新的组件: Redirect, 也就是重定向组件. 我们默认的访问其实是 /, 什么都匹配不到, 所以我们需要在匹配的最后, 重定向到某个路由.

Redirect 可以写在路由的最下面, 也就是没匹配到的时候, 重定向到什么地方.

1
2
3
4
5
<Switch>
<Route exact={true} path={"/about"} component={About}/>
<Route path={"/home"} component={Home}/>
<Redirect to={"/home"}/>
</Switch>

|500

嵌套路由使用

假如我希望在一个页面中, 又存在了一个新的路由呢?

|500

这就是嵌套路由了. 首先明确结构, 这是 Home 组件里面的 News 组件, 所以我们直接在 Home 目录中创建新的组件即可:

|181

随后实现基础的代码框架, 这里不做展示. 回到 Home 组件中, 又出现了一个路由 Tab, 我们可以直接拿过去, 修改后得到如下 Home 组件的代码:

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, {Component} from 'react';

// 引入自定义组件
import Message from "./Message";
import News from "./News";

class Home extends Component {
render() {
return (
<div>
<h1>我是Home组件</h1>
<ul className="nav nav-tabs">
<li>
<a className="list-group-item" href="./home-news.html">News</a>
</li>
<li>
<a className="list-group-item active" href="./home-message.html">Message</a>
</li>
</ul>
<Message/>
<News/>
</div>
);
}
}

export default Home;

随后我们就要开始改代码了. 上面的 a 标签肯定不能要了, 应当使用 NavLink 标签.

[!info] 注意

这里我们设置的 to, 应当包含父路由, 也就是包含 /home 部分. 否则会出现无法跳转的情况

1
2
3
4
5
6
7
8
9
<h1>我是Home组件</h1>
<ul className="nav nav-tabs">
<li>
<MyNavLink to={"/home/news"}>News</MyNavLink>
</li>
<li>
<MyNavLink to={"/home/message"}>Message</MyNavLink>
</li>
</ul>

至此, 已经实现了基本的路径切换:

|550

随后, 需要进行路由配置, 使用 Route 进行配置即可, 不会和之前的路由发生冲突.

1
2
3
4
5
6
// App.jsx
{/*注册路由部分*/}
<Switch>
<Route path={"/home/news"} component={News}/>
<Route path={"/home/message"} component={Message}/>
</Switch>

这里也验证了, 如果开启了 home 的严格模式, 将无法显示对于的内容, 因为需要的是 /home, 但是提供的是 /home/news. 即不要随便开启严格匹配!

路由传递参数

引入

基于上面的例子, 我希望点击对应的 Message, 下面能够显示对应的新闻信息内容. 这其实就是三级路由了, 但是这个组件是通用的组件, 只是 ID 不一样而已. 这里的新闻可以是动态生成的, 使用 state 在 Message 组件中实现.

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
import React, {Component} from 'react';

class Message extends Component {
state = {
messageArr: [
{id: '01', title: "消息1"},
{id: '02', title: "消息2"},
{id: '03', title: "消息3"},
]
}

render() {
const {messageArr} = this.state;
return (
<div>
<ul>
{
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
<a href="">{msgObj.title}</a>&nbsp;&nbsp;
</li>
);
})
}
</ul>
</div>
);
}
}

export default Message;

我们给 Message 创建一个子组件, 就叫做 Detail 组件吧.

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

class Detail extends Component {
render() {
return (
<div>
{/*详情组件中展示三个信息 id title和内容*/}
<ul>
<li>ID: xxx</li>
<li>Title: xxx</li>
<li>Content: xxx</li>
</ul>
</div>
);
}
}

export default Detail;

肯定得在 Message 中引入这个信息组件进行使用.

1
2
3
{/*对应Message的部分*/}
<hr/>
<Detail/>

我们肯定不使用 a 标签, 使用 Link 即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render() {
const {messageArr} = this.state;
return (
<div>
<ul>
{
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
<Link to="/home/message/detail">{msgObj.title}</Link>
</li>
);
})
}
</ul>
{/*对应Message的部分*/}
<hr/>
<Route path={"/home/message/detail"} component={Detail}/>
</div>
);
}

现在已经实现了点击后展示组件的效果了.

|275

现在我希望能够传递我的三个参数, 这就是路由组件的传参了.

第一种 params 参数

params 其实就是直接写在路径里面的参数. 我们在 Message 组件中, 便利的时候就配置好它的请求参数:

1
2
3
4
5
6
7
messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
<Link to={`/home/message/detail/${msgObj.id}`}>{msgObj.title}</Link>
</li>
);
})

这样子东西就已经带过去了, 可是我们还没有收到数据! 到下面的 Route 部分, 通过 Express 的形式进行参数的传递即可:

1
<Route path={"/home/message/detail/:id"} component={Detail}/>

随后在 Detail 组件中接收即可, 肯定是 props 中的信息. 但是具体在哪里呢? 在 Details 中, 写一个挂载的时候输出 props 看看.

|350

在 match 中, 找到了传递的 params 参数, 那么直接在 Detail 组件中使用即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Detail extends Component {
render() {
// 获取
const {id} = this.props.match.params
return (
<div>
{/*详情组件中展示三个信息 id title和内容*/}
<ul>
<li>ID: {id}</li>
<li>Title: xxx</li>
<li>Content: xxx</li>
</ul>
</div>
);
}
}

|275

剩下的属性按照同样的方式即可完成传递. 总结来说, 其实就是三个步骤:

  1. 声明参数 <Link to="/home/01">详情</Link>
  2. 接收参数 <Rount path="/home/:id" component={Test}/>
  3. 使用参数 const {id} = this.props.match.params

第二种 search 参数

其实就是查询参数, 类似于访问了一个页面: https://xxx.com/?name=yyy 这种. 基于刚才写的 Link, 修改一下:

1
2
3
<Link
to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}
</Link>

如何声明接收呢? 这里就不一样了: 查询参数无需声明接收. 所以下面路由注册直接正常注册就好.

1
<Route path={"/home/message/detail/"} component={Detail}/>

我们直接去组件中接收参数; 不论如何, 我们的参数都是在 props 中的, 不妨还是输出看一看.

|475

传递的参数在 location.search 中, 不过形式是 ?id=xxx 这种形式, 并不是已经准备好的对象形式. 这里我们需要使用另外一个库来解析这个内容: qs, 默认就安装好了.

1
2
// 引入qs
import qs from 'qs'

原本的这种用 & 分开, 用等号说明 key-value 内容的形式其实叫做 urlencoded, 反过来我们只需要使用解码的方法即可.

[!info] 注意

这里的默认是带一个开始的问号的, 所以使用 slice(1) 来删掉第一个字符.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Detail extends Component {

render() {
// 接收查询参数
const {search} = this.props.location;
// 拿到对象 进行解构赋值
// 需要把问号去掉
const {id, title} = qs.parse(search.slice(1));
// 解析并且使用解构赋值
return (
<div>
{/*详情组件中展示三个信息 id title和内容*/}
<ul>
<li>ID: {id}</li>
<li>Title: {title}</li>
<li>Content: xxx</li>
</ul>
</div>
);
}
}

至此, 参数已经成功传递.

|300

总结来说:

  1. 路由链接中直接通过查询参数的方式传递参数
  2. 注册路由啥都不需要, 正常就好
  3. 接收参数需要从 this.props.location 中进行解构
  4. 需要使用 qs 库将字符串转换为对象.

第三种 state 参数

注意, 这里的 state 是路由属性独有的 state. 这个东西是不能直接看到的, 看不出来区别.

state 传递, 其实就是修改了一下 to 的内容, 传入的不再是一个字符串了, 而是一个对象. 首先需要传入 pathname, 随后就是其他的想要传递的各种数据了.

1
2
3
4
5
6
7
8
9
{/*向路由组件传递state参数*/}
<Link
to={{
pathname: '/home/message/detail', state: {
id: msgObj.id,
title: msgObj.title
}
}}>{msgObj.title}
</Link>

state 是不需要声明接收的, 直接正常注册路由即可:

1
<Route path={"/home/message/detail/"} component={Detail}/>

但是无论如何, 其实传递的东西都是在 props 中的. 不妨还是输出看看 props 中的内容:

|575

这里直接就看到了, 就是在 location 中的. 那么直接解构赋值获取就好.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Detail extends Component {
render() {
// 获取数据
if (this.props.location.state !== undefined) {
const {id, title} = this.props.location.state;
return (
<div>
<ul>
<li>ID: {id}</li>
<li>Title: {title}</li>
<li>Content: xxx</li>
</ul>
</div>
);
}
return (
<></>
)
}
}

[!error] 警告

这里我加了一层判断, 主要是如果直接跑的话, 可能会出现未知的报错, 干脆做一层判断.

[!info] 解决

但是如果你使用的是 BrowserRouter, 则不会遇到这个问题, 因为它在帮你记录访问的记录, history 中是有 state 这个东西的! 这个情况下就算刷新也是会保留内容的!

多种路由跳转方式

push 与 replace

浏览器的历史记录, 本质上就是入栈和出栈的过程, 点击一个链接, 就到栈底了, 随后点击就再次入栈; 同理, 如果返回就是出栈, 出栈后, 如果有新的东西入栈, 则找不回来刚才访问的内容了.

默认的, 如果我们给组件开启了 replace 模式, 则无法回退到上面的内容了.

编程式路由导航

比如说, 我有一个需求, 点进某个页面后, 几秒后自动跳转到另外的一个路由中. 我们自然是没有手动的点击某个路由的, 这种由编程代码导致的路由变化, 就叫做编程式路由.

我们如果想用代码实现路由的切换, 实际上非常的简单, 就是修改历史记录. 只需要获取当前的历史记录进行修改即可~

在 Detail 中, 可以添加如下判断语句, 用来测试跳转的效果:

1
2
3
4
5
6
7
8
9
10
11
componentDidMount() {
if (this.props.location.state !== undefined) {
const {id, title} = this.props.location.state;
if (id === '01') {
// 几秒后跳转
setTimeout(()=>{
this.props.history.push("/about")
}, 3000)
}
}
}

效果正常实现!

|475

这里其实也有其他的方法, 上面介绍了 push 的方法, 也可以 replace, 也可以 go 多少, 直接前进或者回退多少步. 总结来说, 玩的其实就是 history 身上的一些 API.

withRouter 的使用

首先我们需要知道, 只有路由组件是可以获取路由的一些方法的. 我现在假设想要在 Header 实现一些路由的操作, 但是 Header 不是路由组件, 我就可以使用这个 withRouter 函数了.

withRouter 可以接收一个组件参数, 返回加工后的组件. 代码可以写成下面这样子:

1
2
3
4
5
6
7
8
9
10
import React, {Component} from 'react';
import {withRouter} from "react-router-dom";

class Header extends Component {
render() {
{/*省略*/}
}
}

export default withRouter(About);

这样子, 这个组件就可以使用路由相关的功能了, 这就是 withRouter 的作用.

HashRouter 和 BrowserRouter

这两个在我们眼中, 其实就是一个的访问会在路径前面加上一个 #, 一个啥都没有而已. 但是其实两个东西的原理是不一样的!

BrowserRouter 使用的是 H5 的 history API, 不兼容 IE 9 以下的版本; 但是 HashRouter 使用的是 URL 的哈希值. 另外, HashRouter 中, state 一旦刷新就没了, 但是 BrowserRouter 的还会存在.

[!note] 备注

HashRouter 可以用来解决一些路径错误相关的问题

React Router 6

概述

这是 react router 的最新版本, 也是一个默认版本. 直接使用 npm 安装则会默认安装这个版本. 我们还是需要注意, 我们使用的其实还是 react-router-dom.

比较特殊的就是, 新版移除了 <Switch/>, 新增了 <Routes/> 等等; 另外, 语法发生了一些变化需要注意; 其次, 新增了很多个 hook 方便使用.

[!ERROR] 最重要的

官方明确推荐使用函数式组件了! 所以这个部分的内容都会使用函数式组件实现!

一级路由

准备环境

这里回顾一下当时学习路由的情况, 我们要实现的是这样的一个路由切换效果:

|350

首先, 我们还是需要创建一个脚手架, 这里不过多赘述. 随后我们直接使用 WebStorm 打开, 对内容做一些精简.

|229

考虑到当时使用的是 Bootstrap 的样式, 这里直接进行引入, 然后写入主页的样式即可:

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
function App() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<a className="list-group-item" href="./about.html">About</a>
<a className="list-group-item active" href="./home.html">Home</a>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
<h3>我是Home的内容</h3>
</div>
</div>
</div>
</div>
</div>
)
}

export default App

实现路由

我们不同的展示区, 需要抽象为不同的组件. 简单一些, 直接抽象为单个文件组件, 就不建立文件夹了. 对应组件的内容如下, 另一个就是改了一下文本:

1
2
3
4
5
6
7
function Home() {
return (
<h3>我是Home的内容</h3>
)
}

export default Home

安装并使用 router

这里的安装, 直接安装最新版即可, 不需要加上版本号!

1
npm i react-router-dom

随后, 还是直接在 main 中引入并且使用 router.

1
2
3
4
5
6
7
8
9
10
11
12
import {createRoot} from 'react-dom/client'
// 引入路由
import {BrowserRouter} from 'react-router-dom'
import App from './App.jsx'
import './App.css'

createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App/>
</BrowserRouter>
)

编写路由链接 & 注册路由

有两种路由链接的写法, 一种是 Link, 一种是 NavLink. 我们需要高亮效果, 使用后者.

1
2
<NavLink className="list-group-item" to={'/home'}>Home</NavLink>
<NavLink className="list-group-item" to={'/about'}>About</NavLink>

接下来, 我们需要在呈现路由组件的位置注册路由.

至此基本的效果已经实现了.

|375

回顾

变化总结如下:

  1. Switch 没了, 变成 Routes
  2. Route 中不要写 component 属性, 而是 element, 同时传入的是组件标签
  3. Routes 是必选项, 必须要包裹, 否则报错, 但是规则和 Switch 一样.

重定向

自动重定向

如果我一开始直接访问, 会在控制台报一个警告的:

|325

这就是因为一开始的 / 路径中是没有规则的, 相当于啥都没有. 我们当时是使用 Redirect 进行重定向的, 不过现在这个组件已经没了, 被删除.

我们现在需要使用的是一个新组件: Navigate. 使用的规则也出现了一些变化. 使用的时候, Route 正常写, 但是 element 中传入这个新组件, 并且这个组件存在一个属性 to, 必须给.

1
<Route path={"/"} element={<Navigate to={'/home'}/>}/>

这样已经可以正常进行重定向了.

手动重定向

假如我的 Home 组件中存在一个 sum 计数器, 这个计数器到达 5 的时候, 自动重定向到 About 页面, 应该怎么做呢? 其实也是用到这个 Navigate 组件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {useState} from "react";
import {Navigate} from 'react-router-dom'

function Home() {
const [mySum, setSum] = useState(0)
return (
<>
<h3>我是Home的内容</h3>
{/*在渲染前判断即可*/}
{mySum === 5 ? <Navigate to={'/about'}/> : <></>}
<h4>当前Sum为 {mySum}</h4>
<button onClick={() => {
setSum(mySum + 1)
}}>点我++
</button>
</>
)
}

export default Home

这里使用了一个三元表达式进行跳转效果. 因为这个组件只要渲染, 就会引起视图的切换.

|425

replace 属性

我们都知道, 刚才的页面是可以退回去的, 但是我们也可以不让他退回去, 就是这是 replace 属性为 true. 如下面的代码, 浏览器左上角的按钮就按不了了

1
{mySum === 5 ? <Navigate replace={true} to={'/about'}/> : <></>}

我们在 router 5 中都知道, Navlink 在点击的时候, 可以自动添加一个 active 属性, 进而实现高亮效果. 但是如果默认的高亮效果不是 active, 则无法正常触发效果.

在版本 5 的时候, 我们可以直接使用 activeClassName 来选择我们想要的效果, 但是这里就不可以了, 会直接报错. 我们必须要将 className 的值转换为一个函数, 进而设置自定义样式.

并且这个函数是存在一个参数的! 默认传入的对象为一个参数, 我们根据这个参数来判断是否点击就好, 这个参数中存在一个 isActive 的布尔值代表是否点击, 可以参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
<NavLink className={  
// 注意 这里使用了解构赋值
({isActive}) => {
return (isActive ? "list-group-item active" : "list-group-item")
}
} to={'/home'}>Home</NavLink>
<NavLink className={
({isActive}) => {
return (isActive ? "list-group-item active" : "list-group-item")
}
} to={'/about'}>About</NavLink>

这样子非常的麻烦, 但是自定义确实高了不少. 为了方便, 我们可以将其中的内容抽象为方法:

1
2
3
4
// 定义一个方法 用来判断样式
const computedClassName = ({isActive}) => {
return (isActive ? "list-group-item active" : "list-group-item")
}

★ useRoutes 路由表

基本使用

下面的那么多路由, 其实不一样的就是目标 url 以及一个 element. 既然如此, 可以进行一定的精简.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义路由表
// 路由表, 储存的是一个数组, 每个元素都是一个对象
const element = useRoutes([
{
path: '/home',
element: <Home/>
},
{
path: '/about',
element: <About/>
},
// 重定向部分
{
path: '/',
element: <Navigate to={'/home'}/>
}
])

随后, 下面的 Routes 组件可以直接删掉了, 使用这个路由表即可.

1
2
3
4
{/*注意 这里原本的Routes直接没了, 替换为了下面的花括号的内容!*/}
<div className="panel-body">
{element}
</div>

这就比 React Router 5 的简单多了!

路由表

这样并不是最优的, 因为我们写在了一个组件里面. 我们一般会定义一个新的文件夹: routes, 里面定义这些路由规则进行使用.

我们拿走的只是规则, 也就是里面的数组, 别的不需要拿走.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// routes/index.jsx
import Home from "../pages/Home.jsx";
import About from "../pages/About.jsx";
import {Navigate} from "react-router-dom";

export default [
{
path: '/home',
element: <Home/>
},
{
path: '/about',
element: <About/>
},
// 重定向部分
{
path: '/',
element: <Navigate to={'/home'}/>
}
]

随后回来引入并且使用即可:

1
2
3
4
5
6
7
// 引入路由表  
import routes from "./routes/index.js";

...

// 获取内容
const element = useRoutes(routes)

嵌套路由

简单说, 就是一个页面中还有页面. 这里我们需要注意, 使用的是路由表. 首先还是引入对应的组件, 不过这里就简单很多了:

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
import Home from "../pages/Home.jsx";
import About from "../pages/About.jsx";
import News from "../pages/News.jsx";
import Message from "../pages/Message.jsx";
import {Navigate} from "react-router-dom";

export default [
{
path: '/home',
element: <Home/>,
// 嵌套路由 还是children的写法
children: [
// 这里就简单一些了, 直接写子集路径的名字就行 斜杠都不需要了
{
path: 'news',
element: <News/>
},
{
path: 'message',
element: <Message/>
}
]
},
{
path: '/about',
element: <About/>,
},
// 重定向部分
{
path: '/',
element: <Navigate to={'/home'}/>
}
]

回到 Home 组件, 改写一下原来的 a 标签:

1
2
3
4
5
6
7
8
<ul className="nav nav-tabs">  
<li>
<NavLink className="list-group-item" to="/home/news">News</NavLink>
</li>
<li>
<NavLink className="list-group-item" to="/home/message">Message</NavLink>
</li>
</ul>

这里遇到了一个问题, 我们现在的东西呈现在哪里呢? 如果匹配上了, 写在哪里呢? 这里我们需要使用一个全新的东西了: Outlet. 相当于一个槽位, 只要匹配到了就在这个位置进行呈现.

[!info] 提示

这里甚至不需要写 /home/news 这种路径, 直接写 news 就好, 非常的方便.

不过, 不能写 /news, 否则路由不匹配了.

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

function Home() {
return (
<>
<h3>我是Home的内容</h3>
<div>
<ul className="nav nav-tabs">
<li>
<NavLink className="list-group-item" to="news">News</NavLink>
</li>
<li>
<NavLink className="list-group-item" to="message">Message</NavLink>
</li>
</ul>
{/*指定路由组件呈现的位置*/}
<Outlet/>
</div>
</>
)
}

export default Home

现在页面中已经可以正确的显示结果了

|425

params 参数

基本结构

这里先改一下数据结构, 我希望我的列表是动态进行渲染的, 就使用一个 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
29
import React, {useState} from 'react';

const Message = () => {
// 定义可改变的数据
const [messages] = useState([
{id: '001', title: '消息1', content: 'yyt最可爱啦'},
{id: '002', title: "消息2", content: "lc也很可爱"},
{id: '003', title: "消息3", content: "yyt和lc要一直一起"},
{id: '004', title: "消息4", content: "lc也要!"},
]);

return (
<div>
<ul>
{
messages.map((messageObj) => {
return (
<li key={messageObj.id}>
<a href="/message1">{messageObj.title}</a>&nbsp;&nbsp;
</li>
)
})
}
</ul>
</div>
);
};

export default Message;

接下来写一个 Detail 组件, 用来接收传入的一些参数. 另外考虑到这个 Detail 是 Message 的子路由, 所以再次写一个 children 出来.

1
2
3
4
5
6
7
8
9
10
{
path: 'message',
element: <Message/>,
children: [
{
path: 'detail',
element: <Detail/>
}
]
}

接下来修改一下上面配置的 a 标签为一个 Link 标签, 并且 to 指向我们配置好的路由:

1
2
3
4
5
6
7
messages.map((messageObj) => {
return (
<li key={messageObj.id}>
<Link to={'detail'}>{messageObj.title}</Link>&nbsp;&nbsp;
</li>
)
})

最后, 引入 Outlet, 进而将子路由的组件放入页面当中:

1
2
3
<hr/>
{/*定义一个用来展示子路由组件的地方*/}
<Outlet/>

传递参数

之前我们学习过的, 这种路由有三种参数的传递形式: params, search 和 location.state. 在这里就有一些不太一样了, 首先携带参数还是一样的, 直接模板字符串即可:

1
2
3
4
5
6
7
<li key={messageObj.id}>  
<Link
to={`detail/${messageObj.id}/${messageObj.title}/${messageObj.content}`}
>
{messageObj.title}
</Link>&nbsp;&nbsp;
</li>

但是对应的路由字符串也需要有所变化. 回到路由定义的地方, 修改一下路由:

1
2
3
4
5
6
7
8
9
10
{
path: 'message',
element: <Message/>,
children: [
{
path: 'detail/:id/:title/:content',
element: <Detail/>
}
]
}

这样就可以获取了, 不妨看一看.

|425

接收参数

之前我们可以通过 this 来获取数据, 但是现在是函数式组件, 所以只能借助 hooks 了. 这个 hook 叫做 useParams, 非常的直白, 使用起来也非常的简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import {useParams} from "react-router-dom";

const Detail = () => {
// 接收参数
const {id, title, content} = useParams()
return (
<ul>
<li>{id}</li>
<li>{title}</li>
<li>{content}</li>
</ul>
);
};

export default Detail;

现在直接就能够看到正常的效果了!

|275

search 参数

基本结构

这里删掉之前的参数, 我们换一种形式. 查询参数就是类似于 ?name=xxx 这种的格式, 所以我们基于刚才的进行改变:

1
2
3
4
5
6
<li key={messageObj.id}>
<Link
to={`detail?id=${messageObj.id}&title=${messageObj.title}&content=${messageObj.content}`}>
{messageObj.title}
</Link>&nbsp;&nbsp;
</li>

获取参数

那么对应的, 其实使用的钩子大差不差, 就是 useSearchParams, 相比之前的就简单太多了. 那么使用的时候, 还是和刚才的一样吗? 然而并不是的. 不妨获取看看情况:

1
2
const a = useSearchParams()  
console.log(a)

|450

获取到的居然是一个数组! 这里我们需要按照解构一个数组的形式来获取需要的东西. 分别是 search 和 setSearch. 另外, 这里的 search 并不能直接使用, 而是需要使用 .get 方法来获取需要的内容. 例如下面, 就实现了获取对应的查询参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import {useSearchParams} from "react-router-dom";

const Detail = () => {
const [search, setSearch] = useSearchParams();
return (
<ul>
<li>{search.get('id')}</li>
<li>{search.get('title')}</li>
<li>{search.get("content")}</li>
</ul>
);
};

export default Detail;

效果完全正常的显示了.

更新参数

setSearch 函数, 主要是用来更新传入的 search 参数用的. 比如我写一个按钮, 更新对应的查询参数, 就可以按照下面这样的写法:

1
2
3
<button onClick={()=>setSearch('id=000&title=kaede&content=yyt')}>
点我更新search参数
</button>

就可以实现更新当前的 search 参数啦~

|241

state 参数

传递参数

state 参数和 search 参数都是一样不需要占位的, 所以代码不需要改动太多. 但是传参的时候, 传递的时候, to 还是正常写, 加上一个 state 属性即可.

1
2
3
4
5
6
7
8
9
<Link to='detail' state={{  
pathname: 'detail',
state: {
id: messageObj.id,
title: messageObj.title,
content: messageObj.content
}
}}>{messageObj.title}
</Link>

接收参数

这次接收的是 state 参数, 但是 useState 有自己的作用, 所以我们不能这么写. 这里我们需要用到的是 useLocation. 直接从 useLocation 中的 state 进行解构即可得到需要的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import {useLocation} from "react-router-dom";

const Detail = () => {
// 获取state参数
const {id, title, content} = useLocation().state.state
return (
<>
<ul>
<li>{id}</li>
<li>{title}</li>
<li>{content}</li>
</ul>
</>
);
};

export default Detail;

[!ERROR] 警告

这里存在未知的问题, 不过还是可以获取到值的.

编程式路由导航

基本使用

现在我有一个需求, 我不希望直接就显示其中的内容了. 我希望通过点击按钮来进行路由的跳转. 比如我现在有一个按钮, 我希望点击后跳转到某一个路由上面去, 就可以使用编程式路由导航了.

1
2
{/*这里提供一个关于的按钮*/}  
<button onClick={handleClick}>点我前往关于页面</button>

这里需要进行跳转, 使用一个钩子函数就好: useNavigate. 这里首先需要调用这个函数, 返回一个路由跳转的东西, 随后使用它就可以直接进行跳转了.

1
2
3
4
5
6
7
// 使用钩子  
const navigate = useNavigate()

// 跳转到about页面
const handleClick = () => {
navigate('/about');
}

这样就可以进行路由的可控跳转了.

|525

传递参数

假如我希望跳转路由的时候携带一些参数, 直接写在函数的第二个参数就好, 比如这里跳转到 detail 路由上面, 并且使用一些参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用钩子
const navigate = useNavigate()

// 跳转到about页面
const handleClick = () => {
navigate('/about', {
replace: false,
state: {
id: "2333",
title: "天气不错",
content: "路由跳转成功"
}
});
}

|230

点击按钮后, 就可以成功进行路由的跳转了.

React UI 组件库

流行的开源 React UI 组件库

引言

我们如果所有的样式都自己写, 会非常的麻烦, 这是我们显然不想要的. 所以, 我们可以使用一些别人已经写好的组件库.

比如我们自己写的 Header, 我们自己的样式可能无法做到非常的好看, 那么有没有能够直接使用的 Header 库呢? 有. 前端有很多的 UI 组件库, 任何语言几乎都是有的.

在国外, 可以使用 material-ui, 很多的组件都是封装好的, 开箱即用; 在国内则可以使用 ant-design 组件库, 来源于蚂蚁金服, 用的还是非常多的. 我们就主要研究国内的这个.

1
2
3
4
5
url: https://ant-design.antgroup.com/
title: "Ant Design - The world's second most popular React UI framework"
description: "An enterprise-class UI design language and React UI library with a set of high-quality React components, one of best React UI library for enterprises"
host: ant-design.antgroup.com
image: https://gw.alipayobjects.com/zos/rmsportal/rlpTLlbMzTNYuZGGCVYM.png

antd 的基本使用

默认的按钮样式长这个样子:

|182

虽然不好看, 但至少能用. 我们不妨看看 antd 的这个组件库中的按钮; 首先安装:

1
npm install antd --save

我们想要添加按钮, 就在官方找到想要的样子, 并且点击按钮查看代码:

这里就已经可以知道这个组件怎么使用了, 不妨直接拿过来看看.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入antd
import {Button} from 'antd'

function App() {
return (
<>
<button>这是一个普通的按钮</button>
<Button type={"primary"}>这是高级一些的按钮</Button>
</>
)
}

export default App

|350

效果已经完全不一样了!


无论如何, 只需要阅读官方文档后再次使用即可.

Redux

redux 理解

redux 是什么

redux 是一个专门用来做状态管理的 JS 库, 并不是 react 的一个插件库, 而是所有 JS 通用的一个库. 它可以用在 react, angular, vue 之类的项目当中, 不过一般和 react 配合使用.

简单说, 它的作用就是 集中式的管理 react 中的多个组件共享的状态 .

[!info] 情景
假设有这样一个嵌套组件: A->B->C->D, E, F->G

如果我们希望能够让 D 组件中的信息, 其他的所有组件都可以进行访问, 之前的回调函数就太过于麻烦了.

什么情况下使用 redux

我们会在如下情况中使用 redux

  1. 某个组件的状态, 需要让其他的组件都可以随时拿到
  2. 一个组件需要改变另外一个组件的状态
  3. 总体原则: 能不用就不用, 如果不用比较吃力, 再用

redux 工作流程

大体可以参考如下原理图.

我们可以假设, 现在有一个 Count 组件, 也就是下面的蓝色的组件. 为了告诉 redux 我们需要让数据变动, 走的就是左边的这条线了, 出现了一个 action creators. 其实就是一个创建 action 的东西. (这个 action 就是动作对象, 包含了动作的类型以及动作的数据.)

随后, 调用了一个 dispatch 的函数, 传入了 action. 这里的 dispatch 就是一个分发用的函数, 将动作对象进一步进行传递, 进而修改.

store, 其实就是一个商店, 或者说叫做调度者, 用来掌控全局的状态. 通过 dispatch 将 action 给了 store, store 会将动作交给正确的人来做事情, 在这里的人就是后面的 reducers.

reducers 会通过 action 和之前的初始状态, previousState 来返回新的状态, 随后返回修改好的新状态返还给 state, 随后通过新的方法 getState 就可以获取到更新后的状态了.

求和案例

纯 react 版

这里直接先写好 Count 组件:

[!info] 这是什么代码

这里我用的是函数式组件, 使用起来更加简单.

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
48
49
import React, {useRef, useState} from 'react';

function Count() {
const selectRef = useRef();
const [counter, setCounter] = useState(0);

const increment = () => {
// 先获取用户输入
// 这里需要*1, 因为默认是字符串, 只能强制类型转换了
setCounter(counter + 1 * selectRef.current.value);
}

const decrement = () => {
setCounter(counter - 1 * selectRef.current.value)
}

const incrementIfOdd = () => {
if (counter % 2 !== 0) {
setCounter(counter + 1 * selectRef.current.value)
}
}

const incrementAsync = () => {
// 等待几秒后加法
setTimeout(() => {
setCounter(counter + 1 * selectRef.current.value)
}, 1000)
}

return (
<div>
<h2>当前求和为: {counter}</h2>&nbsp;
<select ref={selectRef}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
</select>&nbsp;
<button onClick={increment}>+</button>
&nbsp;
<button onClick={decrement}>-</button>
&nbsp;
<button onClick={incrementIfOdd}>当前求和为奇数再加</button>
&nbsp;
<button onClick={incrementAsync}>异步加</button>
</div>
);
}

export default Count;

目标效果如下:

|325

redux mini 版

其实我们用不到所有的 API, 这里只需要使用到两个 API, 就可以让 redux 开始工作. 上图中的 action creator 可以不用的, 自己写一个对象也可以; 但是 store 是最关键的, 必须得有; reducers 的话, 也得有.

我们首先安装 redux: npm i redux, 然后创建 redux 目录以及其中的文件:

|177

我们先实现最核心的内容, 也就是 store. 来到 store.js 文件, 可以写出如下代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
该文件专门用来暴露一个store对象
整个应用只有一个store对象
*/

// 引入createStore 专门用来创建store对象
import {createStore} from 'redux'

// 引入为count服务的reducer
import countReducer from './count_reducer.js';

// reducer为store服务, 所以需要传入的就是reducers了
const store = createStore(countReducer)

// 暴露出来
export default store

上面代码中的 reducer 还没有写, 但是可以写一个默认暴露, 因为一个 reducer 就是一个 reducer, 不会有其他的过多作用. 来到 reducer 文件, 可以编写如下代码:

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
/*
reducer可以接收到东西, 然后根据之前的状态, 处理状态并且返回新的状态
既然如此, reducer肯定是一个函数了

该文件是为了 创建一个为Count服务的reducer
*/

// reducer函数会收到两个参数 分别为之前的状态和动作对象
function countReducer(pre, action) {
// 初始化的时候, 程序会出现这样的情况
// store传入了一个空的action
// 所以我们可以做一层判断来解决问题
if (pre === undefined) {
pre = 0;
}

// 这里的action是一个对象, 包含了type和data数据
// type就是要干什么, 字符串; data就是传入的数据
// 所以, 从action对象获取type和data
const {type, data} = action;

// 对于count组件, 我们有需要加法的运算, 所以判断是否为加, 也就是increment
// 根据type 决定如何加工数据
switch (type) {
case 'increment':
// 加法 就是之前的数据加上data
return pre + data;
case 'decrement':
return pre - data;
default:
// 默认返回pre即可, 即解决了初始化, 又解决了意外输入
return pre;
}
}

// 最后导出 进而使用
export default countReducer

[!info] 注意

reducer 不管太多的东西, 奇数偶数什么的不在这里进行判断!

reducer 是一个纯函数


至此, 两个核心文件已经准备完毕, 回到 Count 组件中, 修改一下代码. 这里我们可以用到一个新的 API: store.getState() 获取状态变量!

1
2
3
4
5
// 引入store 用于获取redux中保存的状态  
import store from "../../redux/store.js";

// 使用API 获取存储的值
<h2>当前求和为: {store.getState()}</h2>&nbsp;

|375

页面已成功渲染! 那么我现在要修改加法, 其实就是通知 redux 加 value. 这里需要用到第二个 API 了: store.dispatch(), 传入 action 即可.

这里的 action 是一个对象, 需要 type 和 data 的数据, 传入即可:

1
2
3
const increment = () => {
store.dispatch({type: "increment", data: selectRef.current.value})
}

然而来到页面上, 并没有效果. 这是因为 redux 仅仅是维护了状态, 并没有调用 render. 这里需要某个办法, 检测到变化后, 调用 render.

我们需要在组件挂载的时候, 调用 store.subscribe 进行订阅, 只要 redux 中的值发生变化就会调用传入的回调函数. 这里调用 this.setState({}) 就会直接触发 render 的触发.

[!error] 注意

这里修改为了类式组件, 方便调用对应的生命周期函数

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 引入store 用于获取redux中保存的状态
import store from "../../redux/store.js";
import React, {Component, createRef} from "react";

class Count extends Component {
selectRef = createRef()

componentDidMount() {
store.subscribe(() => {
this.setState({})
})
}

increment = () => {
store.dispatch({
type: "increment",
data: this.selectRef.current.value * 1,
})
}

decrement = () => {
store.dispatch({
type: "decrement",
data: this.selectRef.current.value * 1,
})
}

incrementIfOdd = () => {
if (store.getState() % 2 !== 0) {
this.increment();
}
}

incrementAsync = () => {
setTimeout(() => {
this.increment();
}, 1000)
}

render() {
return (
<div>
<h2>当前求和为: {store.getState()}</h2>&nbsp;
<select ref={this.selectRef}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
</select>&nbsp;
<button onClick={this.increment}>+</button>
&nbsp;
<button onClick={this.decrement}>-</button>
&nbsp;
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
&nbsp;
<button onClick={this.incrementAsync}>异步加</button>
</div>
);
}
}

export default Count;

这样就使用 store 实现了一个 Counter:

|350

redux 完整版

添加 action creators

其实就是多了两个文件而已. 刚才我们省略了 actionCreator, 这里我们借助他们已经创建好的 action creator 试试. 在 redux 目录下, 创建一个新的文件: count_action.js.

|156

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
该文件专门为Count组件生成action对象
*/

// function createIncrementAction(data) {
// return {
// type: 'increment',
// data: data
// }
// }

/*
考虑到直接就是一个返回对象, 可以直接使用箭头函数
这里需要返回的式一个对象, 使用括号进行包裹, 否则代表的是函数体.
另外, 同名的属性可以简写, 直接写一个data即可

最后, 直接暴露方便使用即可
*/
export const createIncrementAction = data => ({type: 'increment', data: data})
export const createDecrementAction = data => ({type: 'decrement', data})

随后, 我们回到 Count 组件中, 改一下代码. 不要使用我们手写的 action 了

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 引入store 用于获取redux中保存的状态
import store from "../../redux/store.js";
// 引入actionCreator 专门用于创建action对象
import {createIncrementAction, createDecrementAction} from '../../redux/count_action.js'
import React, {Component, createRef} from "react";

class Count extends Component {
selectRef = createRef()

componentDidMount() {
store.subscribe(() => {
this.setState({})
})
}

increment = () => {
const {value} = this.selectRef.current;
store.dispatch(createIncrementAction(value * 1))
}

decrement = () => {
const {value} = this.selectRef.current;
store.dispatch(createDecrementAction(value * 1))
}

incrementIfOdd = () => {
if (store.getState() % 2 !== 0) {
const {value} = this.selectRef.current;
store.dispatch(createIncrementAction(value * 1))
}
}

incrementAsync = () => {
setTimeout(() => {
const {value} = this.selectRef.current;
store.dispatch(createIncrementAction(value * 1))
}, 1000)
}

render() {
return (
<div>
<h2>当前求和为: {store.getState()}</h2>&nbsp;
<select ref={this.selectRef}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
</select>&nbsp;
<button onClick={this.increment}>+</button>
&nbsp;
<button onClick={this.decrement}>-</button>
&nbsp;
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
&nbsp;
<button onClick={this.incrementAsync}>异步加</button>
</div>
);
}
}

export default Count;

添加常量模块

因为我们如果直接写单词的话, 容易拼写错误, 所以我们一般还会定义常量模块. 我们直接创建另外一个文件: constant.js, 内容注释如下:

1
2
3
4
5
6
/*
该文件用来定义action中type类型的常量值
*/

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'

随后, 我们就可以修改刚才写的所有文件了, 比如在 reducer 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引入常量
import {INCREMENT, DECREMENT} from "./constant.js";

function countReducer(pre, action) {
if (pre === undefined) {
pre = 0;
}

const {type, data} = action;
switch (type) {
case INCREMENT:
// 加法 就是之前的数据加上data
return pre + data;
case DECREMENT:
return pre - data;
default:
return pre;
}
}

export default countReducer

在 action 中, 可以这样写:

1
2
3
4
5
// 引入常量模块
import {INCREMENT, DECREMENT} from './constant.js'

export const createIncrementAction = data => ({type: INCREMENT, data: data})
export const createDecrementAction = data => ({type: DECREMENT, data})

至此, 我们就不怕单词写错了!

异步 action

简单说, 如果 action 的值是一个对象, 那么就是同步的 action; 否则如果 action 是一个函数, 则为一个异步的 action. 其实就是一个概念上的东西.

我有一个需求, 刚才存在一个异步加法, 点击后就会等待几秒后再更新. 我现在不想在组件中调用这个定时器了, 应该怎么做呢? 或者说, 我希望服务员能够几分钟后上菜, 这个等待几分钟不是我们客人做的事情, 而是服务员做的事. 这里就需要用到异步 action 了.

这里先改写一下代码, 引入, 然后再去实现这个异步 action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引入
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction
} from '../../redux/count_action.js'

...

// 调用异步action
incrementAsync = () => {
const {value} = this.selectRef.current;
store.dispatch(createIncrementAsyncAction(value * 1, 500))
}

来到 action 文件, 可以写出来这样的异步 action 代码:

[!note] 所谓的异步action 就是说action的值是一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 引入常量模块
import {INCREMENT, DECREMENT} from './constant.js'
import store from "./store.js";

export const createIncrementAction = data => ({type: INCREMENT, data: data})
export const createDecrementAction = data => ({type: DECREMENT, data})

// 异步action
/*
所谓的异步action 就是说action的值是一个回调函数
*/
export const createIncrementAsyncAction = (data, time) => {
return () => {
setTimeout(() => {
// 接下来 通知redux加上data即可
// store.dispatch({type: INCREMENT, data})
store.dispatch(createIncrementAction(data))
}, time)
}
}

这样就可以了吗? 运行看看:

看来还需要一个中间件, 否则 redux 不认, 那么就按照他说的, 直接安装一下:

1
npm i redux-thunk

随后, 我们在 store 中进行引入.

1
2
3
4
5
6
7
8
9
10
11
12
// 额外引入一个中间件调用的函数, 用来引入中间件.
import {createStore, applyMiddleware} from 'redux'
import countReducer from './count_reducer.js';

// 引入中间件
import {thunk} from 'redux-thunk'

// 这里需要传入中间件
const store = createStore(countReducer, applyMiddleware(thunk))

// 暴露出来
export default store

扩展

刚才的 action 文件中, 其实这个异步方法在调用的时候会自动传入一个 dispatch, 所以不需要引入 store 再调用, 直接通过传入的方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
import {INCREMENT, DECREMENT} from './constant.js'

export const createIncrementAction = data => ({type: INCREMENT, data: data})
export const createDecrementAction = data => ({type: DECREMENT, data})

export const createIncrementAsyncAction = (data, time) => {
// 默认会传入这样的一个东西
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
}, time)
}
}

[!warning] 备注

异步 action 不是必须要用的, 实现的方法其实完全可以在组件中实现.

Redux Toolkit

介绍

毕竟现在的 React, 大部分使用的都是 Hooks 的语法. 所以 Redux 也有对应的 Hooks 语法. 同时为了提升开发效率, 我们一般不会使用普通的 redux, 而是结合 redux-toolkit 进行开发.

直接进行安装即可 (需要安装 react-redux)

1
npm i react-redux @reduxjs/toolkit

下面标题的顺序就是基本的使用流程了.

创建 slice

一个状态, 比如一个计数器, 或者学生列表, 其实都是一个一个的切片. 我们只需要创建切片, 最后将切片进行引入, 就可以快速的实现全局状态的获取, 修改了.

例如下面的代码:

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
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
// 切片名称
name: "counter-slice",
// 初始值
initialState: {
counter: 0,
},
// 操作数据的方法
reducers: {
increment(state) {
state.counter += 1;
},
decrement(state) {
state.counter -= 1;
},
setCounter(state, action) {
state.counter = action.payload;
},
},
});

export const { increment, decrement, setCounter } = counterSlice.actions;

export default counterSlice;

这里除了暴露基本的切片, 也暴露了一些 actions. 这是因为如果我们要手写 dispatch 的 action, 则需要用到切片的名称. 但是这个名称可能后期会变, 如果我们直接写死, 很容易造成难以维护的问题.

所以, 干脆直接导出 actions, 使用的时候使用即可.

创建 store

有了切片还不够, 我们还需要将需要的东西进行聚合. 所以我们创建 index.js, 作为 store. 里面可以写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./counter";

// 合并两个切片
const store = configureStore({
// 只需要写一个reducer 没有s
reducer: {
// 还是需要加以区分的 按照对象的形式来写就行
counter: counterSlice.reducer,
},
});

// 导出
export default store;

导入 store

定义好了, 还需要在全局进行导入使用. 在 main.js, 或者其他入口文件中, 使用 Provider 导入 store 即可.

1
2
3
4
5
6
7
8
9
10
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import { Provider } from 'react-redux'
import store from './store/index.js'

createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)

使用 store

我们使用钩子语法. 在需要的组件中, 使用 useSelector 来使用某个切片, 并且使用 useDispatch 来调用切片中的 action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useDispatch, useSelector } from "react-redux"
import { setCounter } from "./store/counter"

function App() {
// 引入切片
const counter = useSelector(state => state.counter)
// 引入dispatch
const dispatch = useDispatch()
return (
<>
<h1>
{/* 直接使用某个内容 */}
根组件 {counter.counter}
</h1>
<button onClick={() => {
// 通过dispatch + action来修改全局状态
dispatch(setCounter(counter.counter + 1))
}}> +1 </button >
</>
)
}

export default App

这么一看, redux 的使用就变得简单很多了.

|143

React Hooks

什么是 Hooks

Hook 是 React 16.8.0 中新增加的特性. 可以帮助在函数式组件中使用不同的 React 功能.

Hooks 使用规则

  1. 只能在组件或者其他自定义 Hook 中调用
  2. 只能在组件的顶层使用, 不能嵌套 if, for 或者其他函数中

useState

useState 其实就是一个函数, 用来给组件添加一个状态变量. 之前我们是直接使用的一个大 state, 这里我们就可以单独的创建一个变量, 脱离原本的 state 了.

该函数的返回值为一个数组, 第一个参数是状态变量, 第二个参数是 set 函数, 用来修改创建的状态变量. 其中, useState 的参数将会作为状态变量的初始值.

我们创建函数式的组件, 可以写出基本语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// App.jsx ///

import {useState} from "react";

function App() {
// 定义一个状态变量
const [count, setCount] = useState(0);
return (
<>
<h1>Counter is {count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
}

export default App

这样就不需要写那么多的 setState 之类的东西了, 一下子简化了不少.

不过这里还是需要注意, 就算 count 是一个变量, 我们也不能直接 count++ ​, 因为它毕竟还是一个状态, 不会引发更新.

简单说, 我们应当始终覆盖原来的值, 而不是尝试直接修改原来的值.


对于类来说, 其实差不多, 不过我们可以使用三点运算符来简化我们的操作:

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

function App() {
// 定义一个状态变量
const [person, setPerson] = useState({
name: "yyt",
age: 18,
counter: 0
});

const addOne = () => {
setPerson({
...person,
counter: person.counter + 1
})
}

return (
<>
<h1>name: {person.name}</h1>
<h2>age: {person.age}</h2>
<h3>counter: {person.counter}</h3>
<button onClick={addOne}>Counter + 1</button>
</>
);
}

export default App

GIF 2025-5-13 18-44-16|155

useReducer

用来管理比 useState 更加复杂的状态的时候会更加方便, 可以通过一个函数来管理状态.

该钩子的返回值差不多, 返回一个状态以及一个 action 回调函数, 参考代码如下:

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

function App() {
// 首先需要准备一个回调函数 reducer
const countReducer = (state, action) => {
switch (action) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
return state;
}
}
// 使用useReduce钩子
const [count, countDispatch] = useReducer(countReducer, 0);
return (
<>
<h1>Not the count is: {count}</h1>
<button onClick={() => {
countDispatch('increment')
}}>Count + 1
</button>
<button onClick={() => {
countDispatch("decrement")
}}>Count - 1
</button>
</>
);
}

export default App

这里的 dispatch 一旦调用, 就会自动调用 reducer 中的方法, 并且根据 action 有条件的初始化状态; reducer 中则需要返回新的状态, 而不是直接进行更改.

[!warning] 注意
这里虽然报了参数不存在, 但是一定要写! 否则无效

|275

useEffect

这个钩子会在对应组件发生变化的时候自动调用传入的回调函数. 比如我希望网页的标题栏不断显示当前的 counter 是多少, 就可以使用这个副作用钩子.

这里传入两个参数即可, 第一个参数是回调函数, 也就是渲染后执行的代码; 第二个参数是监听的状态变量, 只要变了就会产生副作用.

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

function App() {
const [counter, setCounter] = useState(0);

// 添加副作用钩子
useEffect(() => {
document.title = `Counter: ${counter}`
}, [counter]);

// 添加副作用钩子
return (
<>
<h1>Not the count is: {counter}</h1>
<button onClick={() => {
setCounter(counter - 1)
}}>counter - 1
</button>
<button onClick={() => {
setCounter(counter + 1)
}}>counter + 1
</button>
</>
);
}

export default App

这样标题栏就会跟随我们的点击而变化啦:

|275

useRef

假如说, 我希望能够获取页面上一个输入框中的内容. 之前我们可以给一个标签添加 ref 属性, 但是在函数式组件中, 我们直接定义一个 ref 出来.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function App() {
// 定义ref容器
const inputRef = useRef()

// 定义展示内容的函数
const showInfo = () => {
console.log(inputRef);
alert(inputRef.current.value);
}

return (
<>
<h1>Hello World</h1>
<input ref={inputRef} placeholder={"输入一些内容"}/>
<button onClick={showInfo}>Show Info</button>
</>
)
}

使用方法是一摸一样的, 直接进行绑定即可. 效果呈现也是正确的.

|500

useMemo

如果我们有一个需求, 进入页面后, 根据数据渲染一个数组出来, 并且渲染到页面上. 如果这个数组一般是不会变化的, 但是数组很大, 那么之前的 useState 就会在每次加载都调用, 很不友好.

所以我们可以使用 useMemo 钩子函数, 仅仅在依赖更改的时候才会重新渲染. 例如下面是课表代码中, 用来获取开始时间列表的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const startCourseList = useMemo(() => {
// 获取当前一天有多少节课
let counter = 0;
COURSE_CONFIG_LIST.forEach((item) => {
counter += item.jieshu;
});
// 获取节数列表
const lis: Record<string, string | number>[] = [];
for (let i = 0; i < counter; i++) {
lis.push({
label: `第${i + 1}节`,
value: i,
});
}
return lis;
}, []);

可以看到, 每次更新都在进行 for 循环来更新数据, 如果使用 useState, 则会造成不必要的性能开销. 所以这里使用 useMemo, 直接固定其中的数据即可.

React + TS

项目创建

这里我们可以直接使用 vite 创建项目. 不过需要指定使用 ts 类型.

1
npx create-vite

|375

安装依赖后, 开启项目即可.

React 引入

目录结构

观察一下, 这里的目录其实存在一些不一样. 多了一个 tsconfig.json, 这个就是 TS 的配置文件; 随后还有 tslint 的代码检查文件, 后续可能修改一下, 以避免某些意外错误.

|232

React 的引入

之前我们的写法是类似于这样的:

1
import React from "react";

但是现在是 TS 了, 我们的写法变成了这样子:

1
import * as React from "react";

组件创建

组件创建也是在 components 文件中的, 不过组件的文件名后缀为 tsx. 文件中, 如果需要使用 React 对象, 则需要修改引入方式, 同时类式组件中, render 前面是有一个 public 的:

1
2
3
4
5
6
7
8
9
10
11
import * as React from "react";

export default class Hello extends React.Component {
public render() {
return (
<div>
Hello Component
</div>
);
}
}

使用的时候和之前一样的, 没有区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 引入组件
import Hello from "./components/Hello.tsx";

function App() {
return (
<>
<div>
Hello react TS
</div>
<Hello/>
</>
)
}

export default App

数据传递 props

基础使用

假如我现在希望给 Hello 组件传递一个参数, 之前我们就是直接进行传递的, 现在需要进行类型声明. 在 Hello 组件, 我们需要声明一个接口, 接口开头必须是大写字母 I, 否则报错.

另外, 这里的 props 类型接口需要作为泛型传递给 Component, 这样才会检测到.

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

// 声明接口
interface IProps {
title: string
}

export default class Hello extends React.Component<IProps> {
public render() {
return (
<>
<div>
Hello Component
</div>
<h5>Title is {this.props.title}</h5>
</>
);
}
}

在 App 组件中, 参数正常传递即可, 和之前一样 (不过需要符合类型)

1
<Hello title={"YYt and Lc"}/>

设置可选

刚才的 props 都是必要参数, 一定要传入否则报错, 我们可以在声明接口的时候加上一个 ? 表示该参数为可选参数.

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

// 声明接口
interface IProps {
title: string,
age?: number
}

export default class Hello extends React.Component<IProps> {
public render() {
// 解构赋值
const {title, age} = this.props;
return (
<>
<div>
Hello Component
</div>
<h5>Title is {title}</h5>
<h5>Age is {age === undefined ? "Not Set" : age}</h5>
</>
);
}
}

现在传入了会正常显示, 不传入则不会.

|177

函数式组件

函数式组件有一些不一样, 可以参考如下代码:

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

interface IProps {
name: string,
age: number
}

const Hello: React.FC<IProps> = ({name, age}) => {
return (
<>
<h1>Name is {name}</h1>
<h2>Age is {age}</h2>
</>
)
}

export default Hello

状态管理 state

该有什么状态, 也是通过接口进行声明的. 声明后, 还是通过泛型传递给 Component 就可以使用了.

1
2
3
4
5
6
7
// 声明状态接口
interface IState {
count: number
}

// 使用接口
export default class Hello extends React.Component<IProps, IState> {...}

随后我们需要实现这个接口 (这里声明为只读, 防止随便进行修改):

1
2
3
4
5
6
{
// 实现state
public state: Readonly<IState> = {
count: 1
}
}

使用还是和之前一样, 直接通过 state 进行使用即可.

1
<h3>Count is {this.state.count}</h3>

如果需要修改, 由于设置了只读, 所以不能直接 this.state.count = 999 这种格式来修改值了 (本身也是不可以的). 直接使用 setState 就可以, 完整代码如下.

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
import * as React from "react";

// 声明Props接口
interface IProps {
title: string,
age?: number
}

// 声明状态接口
interface IState {
count: number
}

export default class Hello extends React.Component<IProps, IState> {
// 实现state
public state: Readonly<IState> = {
count: 1
}

// 设置一个更新的回调函数
clickHandler = () => {
console.log(this)
const {count} = this.state;
this.setState({
count: count + 1
})
}

public render() {
// 解构赋值
const {title, age} = this.props;
return (
<>
<div>
Hello Component
</div>
<h5>Title is {title}</h5>
<h5>Age is {age === undefined ? "Not Set" : age}</h5>
<h3>Count is {this.state.count}</h3>
<button onClick={this.clickHandler}>Change</button>
</>
);
}
}

事件处理

我希望能够实现一个点击子组件, 给父组件调用事件传递数据的情况. 还是一样, 通过 props 进行传递, 不过这里还是一样, 需要声明传递的类型. 使用方法如下, 下面是完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// App
// 引入组件
import Hello from "./components/Hello.tsx";

function App() {
// 实现一下事件
const myClickHandler = (data: number) => {
console.log("Clicked!")
console.log(`The Number is ${data}`)
}
return (
<>
<div>
Hello react TS
</div>
<Hello title={"Lc Yyt"} age={19} onMyClick={myClickHandler}/>
</>
)
}

export default App
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
import * as React from "react";

// 声明Props接口
interface IProps {
title: string,
age?: number,
onMyClick: (data: number) => void
}

// 声明状态接口
interface IState {
count: number
}

export default class Hello extends React.Component<IProps, IState> {
// 实现state
public state: Readonly<IState> = {
count: 1
}

// 设置一个更新的回调函数
clickHandler = () => {
const {count} = this.state;
this.setState({
count: count + 1
})
this.props.onMyClick(count);
}

public render() {
// 解构赋值
const {title, age} = this.props;
return (
<>
<div>
Hello Component
</div>
<h5>Title is {title}</h5>
<h5>Age is {age === undefined ? "Not Set" : age}</h5>
<h3>Count is {this.state.count}</h3>
<button onClick={this.clickHandler}>Change</button>
</>
);
}
}

其实最重要的就是多了点类型配置.

条件与列表

例如我需要遍历一个传入的数据列表.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 引入组件
import Hello from "./components/Hello.tsx";

// 定义一个数据列表
const data = [
{name: "yyt", age: 18},
{name: 'lc', age: 19}
];

function App() {
return (
<>
<Hello data={data}/>
</>
)
}

export default App

那么接收的 Hello 组件中, 也应当有对应类型的限制, 否则直接报错

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

// 定义传入的data类型为一个数组
interface IProps {
data: {
name: string,
age: number
}[]
}

class Hello extends Component<IProps> {
public render() {
return (
<>
<h1>Hello World</h1>
{
this.props.data.map((item) => {
...
})
}
</>
)
}
}

export default Hello

|334

[!success] 总结
不管如何, 反正一定需要有类型的说明即可

拓展

setState

第一种写法

之前写的 setState, 我们基本都是使用下面这种格式:

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

class App extends Component {
state = {
counter: 0
}

addOne = () => {
const {counter} = this.state;
this.setState({
counter: counter + 1
})
}

render() {
return (
<>
<h1>Hello World: {this.state.counter}</h1>
<button onClick={this.addOne}>Add One</button>
</>
)
}
}

export default App

上面是最基本的, 之前的写法. 不过 setState 还有一种写法, 在后面传入第二个参数: 一个函数. 假如我现在有这样的一个情况, 我希望在 setState 之后, 在控制台中输出现在的 state:

|475

没想到, 这里会输出 0, 状态并没有进行准确的更新; 为了获取准确的值, 我们就可以使用 setState 的第二个参数, 回调函数了.

这个回调函数会在状态更新完毕, 并且页面也更新完毕的时候再次调用. 这个时候的数据绝对是准确的, 就可以获取准确的值了.

1
2
3
4
5
6
7
8
addOne = () => {
const {counter} = this.state;
this.setState({
counter: counter + 1
}, () => {
console.log("setState之后", this.state.counter)
})
}

|425

第二种写法

其实刚才的也是第一种, 还有一种, 就是函数式的 setState, 直接传入一个回调函数就好, 并且这个回调函数可以获取 state 和 props 参数.

1
2
3
4
5
6
7
8
addOne = () => {
// 函数式
this.setState((state, _) => {
return {
counter: state.counter + 1
}
})
}

这个的好处就是可以获取到 state 和 props, 同时也不需要再获取原来的值了, 可以直接进行调用.

总结

  1. 对象式的 setState 是函数式 setState 的一种简写方式, 也就是语法糖.
  2. 如果对象不依赖原状态: 使用对象方式
  3. 如果对象依赖原状态: 使用函数方式
  4. 如果需要在 setState 之后获取最新的状态数据, 在第二个参数中获取.

Fragment

翻译过来, 就是: 碎片. 之前的代码中, 我们可能会使用 <div> 来包裹一个多级的解构. 但是我们有的时候不想要这个 div. 这个时候就可以使用这种虚拟 DOM 来实现了.

代码中, 其实就是直接使用这个组件, 或者直接使用空的 <></> 来包裹内容即可. 比如下面这个最简单的组件, 不会报错, 页面中也没有多余的 div 节点出现.

1
2
3
4
5
6
7
8
function App() {
return (
<>
</>
)
}

export default App

|184

[!error] 注意

如果需要接收一些属性, 则不能使用这种 <> 空标签, 而是使用 <Fragment> 组件.

Content 跨层组件通信

虽然之前的代码中, 跨越多个层级的组件仍然可以通过逐步传递回调函数的形式来实现, 但是这毕竟不是最优解, 假如有多层层级的情况, 将会变得非常的复杂.

这里, 我们可以按照如下三个步骤, 实现通过上下文来跨组件通信.

  1. 使用 createContent 方法创建一个上下文对象 Ctx
  2. 在顶层组件中通过 Ctx.Provider 组件来提供数据
  3. 在底层组件 B 中, 通过 useContent 钩子函数获取消费数据

为了方便, 直接将几个组件放在一个文件中了.

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
function Child() {  
return (
<>
<div>This is Child.</div>
</>
)
}

function Parent() {
return (
<>
<div>This is Parent</div>
<Child/>
</>
)
}

function App() {
return (
<>
<h1>This is App</h1>
<Parent/>
</>
);
}

export default App

接下来, 创建一个上下文对象:

1
const MsgContext = createContext("Default Message")

然后, 在顶层组件, 使用 Provider 提供数据

1
2
3
4
5
6
7
8
9
10
11
12
function App() {  
const msg = "This is App Message";
return (
<>
<h1>This is App</h1>
{/*提供数据*/}
<MsgContext.Provider value={msg}>
<Parent/>
</MsgContext.Provider>
</>
);
}

最后, 在底层组件使用 useContext 钩子函数使用数据

1
2
3
4
5
6
7
8
9
10
function Child() {  
// 使用钩子 获取变量的值
const msg = useContext(MsgContext);
return (
<>
<div>This is Child.</div>
<h3>The Value is: {msg}</h3>
</>
)
}

这样就可以正常传递数据了, 页面渲染如下:

center|325

高阶函数 函数柯里化

什么是高阶函数

只要一个函数符合下面两个规范中的一个, 就是高阶函数:

  1. 接受的参数是一个函数
  2. 函数调用后的返回值也是一个函数

常见的高阶函数有哪些呢? 其实之前的 promise, click, setTimeout 之类的都是高级函数, 传入回调函数这种类型.

另外, 数组身上的一些方法, 基本都是高级函数.


对应上面的代码, 我们如果一个表单有很多的内容, 我们肯定不可能一个一个单独保存, 这是非常低效率的. 我们之所以不能直接调用函数, 是因为对应函数的返回值为 null, 那么如果我们函数的返回值是一个回调函数呢?

我们将代码改一下, 改成 saveFormData, 主要重构代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script type="text/babel">
class Login extends React.Component {
...
saveFormData = (event) => {
// 返回一个回调函数 让其调用即可
return () => {

}
}

render() {
return (
<form action="" onSubmit={this.handleSubmit}>
用户名: <input type="text" onChange={this.saveFormData('username')}/>
<br/>
密码: <input type="password" onChange={this.saveFormData('password')}/>
<br/>
<button>登陆</button>
</form>
);
}
}
...
</script>

这里我们传入的数据, 就不是 event 了, 应当是我们自定义的数据类型, 可以假设为 dataType 类型.

这里的 dataType 其实就是我们在回调函数中传入的内容.

1
2
3
4
5
6
7
8
9
saveFormData = (dataType) => {
// 返回一个回调函数 让其调用即可
return (event) => {
// 直接设置就好
this.setState({
[dataType]: event.target.value
})
}
}

这里我们需要注意一下, dataType 需要用一个 []​ 括起来.

总结来说, 函数柯里化, 就是让函数通过调用, 实现多次接收参数, 统一处理再次返回函数的编码形式.

案例

名称 / 链接 介绍 难度
TODO List 一个简单的 React 小案例.
:MiFolderReactComponents: Todo List TS 同样, 不过使用了 Hooks 以及 TS 语法, 更加强大. ★★
通用后台管理系统 通用的后台管理系统, 算是一个非常大的企业级项目了. ★★★