React笔记

React笔记

Voun MAX++

01 【react入门】

1.React简介

react是什么?

React 是一个用于构建用户界面的 JavaScript 库。

  • 是一个将数据渲染为 HTML 视图的开源 JS 库
  • 它遵循基于组件的方法,有助于构建可重用的 UI 组件
  • 它用于开发复杂的交互式的 web 和移动 UI

React 有什么特点?

  1. 使用虚拟 DOM 而不是真正的 DOM
  2. 它可以用服务器渲染
  3. 它遵循单向数据流或数据绑定
  4. 高效
  5. 声明式编码,组件化编码

React 的一些主要优点?

  1. 它提高了应用的性能
  2. 可以方便在客户端和服务器端使用
  3. 由于使用 JSX,代码的可读性更好
  4. 使用React,编写 UI 测试用例变得非常容易

为什么学?

1.原生JS操作DOM繁琐,效率低

2.使用JS直接操作DOM,浏览器会进行大量的重绘重排

3.原生JS没有组件化编码方案,代码复用低

在学习之前最好看一下关于npm的知识:下面是我在网上看见的一个写的还不错的npm的文章

npm

2.React 基础案例

首先需要引入几个 react 包

  • React 核心库、操作 DOM 的 react 扩展库、将 jsx 转为 js 的 babel 库

【先引入react.development.js,后引入react-dom.development.js】

react.development.js

react-dom.development.js

babel.min.js

  • 由于JSX最终需要转换为JS代码执行,所以浏览器并不能正常识别JSX,所以当我们在浏览器中直接使用JSX时,还必须引入babel来完成对代码的编译。
  • babel下载地址:https://unpkg.com/babel-standalone@6/babel.min.js

image-20221022171647360

1
2
3
react.development.js
react-dom.development.js
babel.min.js

2.创建一个容器

3.创建虚拟DOM,渲染到容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>hello_react</title>
</head>
<body>
<!-- 准备好一个“容器” -->
<div id="test"></div>

<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>

<script type="text/babel">
/* 此处一定要写babel */
//1.创建虚拟DOM
const VDOM = <h1>Hello</h1> /* 此处一定不要写引号,因为不是字符串 */
//2.渲染虚拟DOM到页面
const root = ReactDOM.createRoot(document.querySelector('#test'));
root.render(VDOM);
</script>
</body>
</html>

后面很多地方没有用createRoot这种方式是因为一开始学的课程是2020年的,这是现在新的创建方式。

这里我就只把第一个案例改成新方式了

这样,就会在页面中的这个div容器上添加这个h1.

image-20221022171539523

  • React.createElement()
    • React.createElement(type, [props], [...children])
    • 用来创建React元素
    • React元素无法修改
  • ReactDOM.createRoot()
    • createRoot(container[, options])
    • 用来创建React的根容器,容器用来放置React元素
  • ReactDOM.render()
    • root.render(element)
    • 用来将React元素渲染到根元素中
    • 根元素中所有的内容都会被删除,被React元素所替换
    • 当重复调用render()时,React会将两次的渲染结果进行比较,
    • 它会确保只修改那些发生变化的元素,对DOM做最少的修改

3.jsx 语法

JSX 是 JavaScript 的语法扩展,JSX 使得我们可以以类似于 HTML 的形式去使用 JS。JSX便是React中声明式编程的体现方式。声明式编程,简单理解就是以结果为导向的编程。使用JSX将我们所期望的网页结构编写出来,然后React再根据JSX自动生成JS代码。所以我们所编写的JSX代码,最终都会转换为以调用React.createElement()创建元素的代码。

  1. 定义虚拟DOM,JSX不是字符串,不要加引号
  2. 标签中混入JS表达式的时候使用{}
1
id = {myId.toUpperCase()}
  1. 样式的类名指定不能使用class,使用className
  2. 内敛样式要使用{{}}包裹
1
style={{color:'skyblue',fontSize:'24px'}}
  1. 不能有多个根标签,只能有一个根标签
  2. JSX的标签必须正确结束(自结束标签必须写/)
  3. JSX中html标签应该小写,React组件应该大写开头。如果小写字母开头,就将标签转化为 html 同名元素,如果 html 中无该标签对应的元素,就报错;如果是大写字母开头,react 就去渲染对应的组件,如果没有就报错
  4. 如果表达式是空值、布尔值、undefined,将不会显示

关于JS表达式和JS语句:

  • JS表达式:返回一个值,可以放在任何一个需要值的地方 a a+b demo(a) arr.map() function text(){}
  • JS语句:if(){} for(){} while(){} swith(){} 不会返回一个值

其它

  1. 注释

写在花括号里

1
2
3
4
5
6
7
ReactDOM.render(
<div>
<h1>小丞</h1>
{/*注释...*/}
</div>,
document.getElementById('example')
);
  1. class需要使用className代替
  2. style中必须使用对象设置 style={{background:'red'}}
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
<style>
.title{
background-color: orange;
width: 200px;
}
</style>

<!-- 准备好一个“容器” -->
<div id="test"></div>

<script type="text/babel" >
const myId = 'aTgUiGu'
const myData = 'HeLlo,rEaCt'

//1.创建虚拟DOM
const VDOM = (
<div>
<h2 className="title" id={myId.toLowerCase()}>
<span style={{color:'white',fontSize:'29px'}}>{myData.toLowerCase()}</span>
</h2>
<h2 className="title" id={myId.toLowerCase()}>
<span style={{color:'white',fontSize:'29px'}}>{myData.toUpperCase()}</span>
</h2>
<input type="text"/>
</div>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.getElementById('test'))
</script>

image-20221022204158589

  1. 数组

JSX 允许在模板中插入数组,数组自动展开全部成员

{} 只能用来放js表达式,而不能放语句(if for) 在语句中是可以去操作JSX

1
2
3
4
5
6
7
8
var arr = [
<h1>小丞</h1>,
<h2>同学</h2>,
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('example')
);

tip: JSX 小练习

根据动态数据生成 li

1
2
3
4
5
6
7
8
9
10
11
12
13
const data = ['A','B','C']
const VDOM = (
<div>
<ul>
{
data.map((item,index)=>{
return <li key={index}>{item}</li>
})
}
</ul>
</div>
)
ReactDOM.render(VDOM,document.querySelector('.test'))

image-20221022204645014

4.两种创建虚拟DOM的方式

使用JSX创建虚拟DOM

1
2
3
4
5
6
7
8
//1.创建虚拟DOM
const VDOM = ( /* 此处一定不要写引号,因为不是字符串 */
<h1 id="title">
<span>Hello,React</span>
</h1>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.querySelector('.test'))

这个在上面的案例中已经演示过了 ,下面看看另外一种创建虚拟DOM的方式

使用JS创建虚拟DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* React.createElement()
* - 用来创建一个React元素
* - 参数:
* 1.元素的名称(html标签必须小写)
* 2.标签中的属性
* - class属性需要使用className来设置
* - 在设置事件时,属性名需要修改为驼峰命名法
* 3.元素的内容(子元素)
* - 注意点:
* React元素最终会通过虚拟DOM转换为真实的DOM元素
* React元素一旦创建就无法修改,只能通过新创建的元素进行替换
* */
1
2
3
4
//1.创建虚拟DOM,创建嵌套格式的dom
const VDOM=React.createElement('h1',{id:'title'},React.createElement('span',{},'hello,React'))
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.querySelector('.test'))

使用JS和JSX都可以创建虚拟DOM,但是可以看出JS创建虚拟DOM比较繁琐,尤其是标签如果很多的情况下,所以还是比较推荐使用JSX来创建。

5.两种DOM的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 准备好一个“容器” -->
<div id="test"></div>

<script type="text/babel">
/* 此处一定要写babel */
//1.创建虚拟DOM
const VDOM = <h1>Hello,React</h1> /* 此处一定不要写引号,因为不是字符串 */
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('test'))
const TDOM = document.querySelector('#test')
console.log('虚拟DOM', VDOM)
console.dir('真实DOM')
console.dir(TDOM)
// debugger
console.log(typeof VDOM)
console.log(VDOM instanceof Object)

image-20221022194600803

关于虚拟DOM:

  1. 本质是Object类型的对象(一般对象)

  2. 虚拟DOM比较“轻”,真实DOM比较“重”,因为虚拟DOM是React内部在用,无需真实DOM上那么多的属性。

  3. 虚拟DOM最终会被React转化为真实DOM,呈现在页面上。

02 【面向组件编程】

1.组件的使用

当应用是以多组件的方式实现,这个应用就是一个组件化的应用

只有两种方式的组件

  • 函数组件
  • 类式组件

注意:

  1. 组件名必须是首字母大写(React 会将以小写字母开头的组件视为原生 DOM 标签。例如,< div />代表 HTML 的 div 标签,而< Weclome /> 则代表一个组件,并且需在作用域内使用 Welcome
  2. 虚拟DOM元素只能有一个根元素
  3. 虚拟DOM元素必须有结束标签 < />

渲染类组件标签的基本流程

  1. React 内部会创建组件实例对象
  2. 调用render()得到虚拟 DOM ,并解析为真实 DOM
  3. 插入到指定的页面元素内部

1.1 函数式组件

定义组件最简单的方式就是编写 JavaScript 函数:

1
2
3
4
5
6
7
8
//1.创建函数式组件
function MyComponent(props) {
console.log(this) //此处的this是undefined,因为babel编译后开启了严格模式
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}

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

该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。

让我们来回顾一下这个例子中发生了什么:

  1. React解析组件标签,找到了MyComponent组件。
  2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。

注意: 组件名称必须以大写字母开头。

React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div /> 代表 HTML 的 div 标签,而 <Welcome /> 则代表一个组件,并且需在作用域内使用 Welcome

你可以在深入 JSX 中了解更多关于此规范的原因。

1.2 类式组件

将函数组件转换成 class 组件

通过以下五步将 Clock 的函数组件转成 class 组件:

  1. 创建一个同名的 ES6 classopen in new window ,并且继承于 React.Component
  2. 添加一个空的 render() 方法。
  3. 将函数体移动到 render() 方法之中。
  4. render() 方法中使用 this.props 替换 props
  5. 删除剩余的空函数声明。
1
2
3
4
5
6
7
8
class MyComponent extends React.Component {
render() {
console.log('render中的this:', this)
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}

ReactDOM.render(<MyComponent />, document.getElementById('test'))

每次组件更新时 render 方法都会被调用,但只要在相同的 DOM 节点中渲染 <MyComponent/> ,就仅有一个 MyComponent 组件的 class 实例被创建使用。这就使得我们可以使用如 state 或生命周期方法等很多其他特性。

执行过程:

  1. React解析组件标签,找到相应的组件
  2. 发现组件是类定义的,随后new出来的类的实例,并通过该实例调用到原型上的render方法
  3. 将render返回的虚拟DOM转化为真实的DOM,随后呈现在页面中

1.3 组合组件

组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。

例如,我们可以创建一个可以多次渲染 Welcome 组件的 App 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}

image-20221023135154884

通常来说,每个新的 React 应用程序的顶层组件都是 App 组件。但是,如果你将 React 集成到现有的应用程序中,你可能需要使用像 Button 这样的小组件,并自下而上地将这类组件逐步应用到视图层的每一处。

1.4 提取组件

将组件拆分为更小的组件。

例如,参考如下 Comment 组件:

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
function formatDate(date) {
return date.toLocaleDateString()
}

function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar" src={props.author.avatarUrl} alt={props.author.name} />
<div className="UserInfo-name">{props.author.name}</div>
</div>
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate(props.date)}</div>
</div>
)
}

const comment = {
date: new Date(),
text: 'I hope you enjoy learning React!',
author: {
name: 'Hello Kitty',
avatarUrl: 'http://placekitten.com/g/64/64',
},
}

ReactDOM.render(
<Comment date={comment.date} text={comment.text} author={comment.author} />,
document.getElementById('test'),
)

在 CodePen 上试试

该组件用于描述一个社交媒体网站上的评论功能,它接收 author(对象),text (字符串)以及 date(日期)作为 props。

image-20221023135735919

该组件由于嵌套的关系,变得难以维护,且很难复用它的各个部分。因此,让我们从中提取一些组件出来。

首先,我们将提取 Avatar 组件:

1
2
3
4
function Avatar(props) {
return (
<img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} /> );
}

Avatar 不需知道它在 Comment 组件内部是如何渲染的。因此,我们给它的 props 起了一个更通用的名字:user,而不是 author

我们建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。

我们现在针对 Comment 做些微小调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Avatar(props) {
return <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />
}

function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">{props.author.name}</div>
</div>
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate(props.date)}</div>
</div>
)
}

接下来,我们将提取 UserInfo 组件,该组件在用户名旁渲染 Avatar 组件:

1
2
3
4
5
6
7
8
9
10
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}

进一步简化 Comment 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Avatar(props) {
return <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />
}

function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">{props.user.name}</div>
</div>
)
}

function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate(props.date)}</div>
</div>
)
}

在 CodePen 上试试

最初看上去,提取组件可能是一件繁重的工作,但是,在大型应用中,构建可复用组件库是完全值得的。根据经验来看,如果 UI 中有一部分被多次使用(ButtonPanelAvatar),或者组件本身就足够复杂(AppFeedStoryComment),那么它就是一个可提取出独立组件的候选项。

组件实例的三大属性 state props refs

2.state

2.1 基本使用

我们都说React是一个状态机,体现是什么地方呢,就是体现在state上,通过与用户的交互,实现不同的状态,然后去渲染UI,这样就让用户的数据和界面保持一致了。state是组件的私有属性。

在React中,更新组件的state,结果就会重新渲染用户界面(不需要操作DOM),一句话就是说,用户的界面会随着状态的改变而改变。

state是组件对象最重要的属性,值是对象(可以包含多个key-value的组合)

简单的说就是组件的状态,也就是该组件所存储的数据

案例

需求:页面显示【今天天气很炎热】,鼠标点击文字的时候,页面更改为【今天天气很凉爽】

核心代码如下:

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
<body>
<!-- 准备好容器 -->
<div id="test">

</div>
</body>

<!--这里使用了js来创建虚拟DOM-->
<script type="text/babel">
//1.创建组件
class St extends React.Component{
constructor(props){
super(props);
//先给state赋值
this.state = {isHot:true,win:"ss"};
//找到原型的dem,根据dem函数创建了一个dem1的函数,并且将实例对象的this赋值过去
this.dem1 = this.dem.bind(this);
}
//render会调用1+n次【1就是初始化的时候调用的,n就是每一次修改state的时候调用的】
render(){ //这个This也是实例对象
//如果加dem(),就是将函数的回调值放入这个地方
//this.dem这里面加入this,并不是调用,只不过是找到了dem这个函数,在调用的时候相当于直接调用,并不是实例对象的调用
return <h1 onClick = {this.dem1}>今天天气很{this.state.isHot?"炎热":"凉爽"}</h1>
}
//通过state的实例调用dem的时候,this就是实例对象
dem(){
const state = this.state.isHot;
//状态中的属性不能直接进行更改,需要借助API
// this.state.isHot = !isHot; 错误
//必须使用setState对其进行修改,并且这是一个合并
this.setState({isHot:!state});
}
}
// 2.渲染,如果有多个渲染同一个容器,后面的会将前面的覆盖掉
ReactDOM.render(<St/>,document.getElementById("test"));
</script>

类式组件的函数中,直接修改state

1
this.state.isHot = false

页面的渲染靠的是render函数

这时候会发现页面内容不会改变,原因是 React 中不建议 state不允许直接修改,而是通过类的原型对象上的方法 setState()

注意:

  1. 组件的构造函数,必须要传递一个props参数
  2. 特别关注this【重点】,类中所有的方法局部都开启了严格模式,如果直接进行调用,this就是undefined
  3. 想要改变state,需要使用setState进行修改,如果只是修改state的部分属性,则不会影响其他的属性,这个只是合并并不是覆盖。

在优化过程中遇到的问题

  1. 组件中的 render 方法中的 this 为组件实例对象
  2. 组件自定义方法中由于开启了严格模式,this 指向undefined如何解决
    1. 通过 bind 改变 this 指向
    2. 推荐采用箭头函数,箭头函数的 this 指向
  3. state 数据不能直接修改或者更新

2.2 setState()

this.setState(),该方法接收两种参数:对象或函数。

1
this.setState(partialState, [callback]);
  • partialState: 需要更新的状态的部分对象
  • callback: 更新完状态后的回调函数

有两种写法:

  1. 对象:即想要修改的state
1
2
3
this.setState({
isHot: false
})
  1. 函数:接收两个函数,第一个函数接受两个参数,第一个是当前state,第二个是当前props,该函数返回一个对象,和直接传递对象参数是一样的,就是要修改的state;第二个函数参数是state改变后触发的回调
1
this.setState(state => ({count: state.count+1});
  • 在执行 setState操作后,React 会自动调用一次 render()
  • render() 的执行次数是 1+n (1 为初始化时的自动调用,n 为状态更新的次数)

2.3 简化版本

  1. state的赋值可以不再构造函数中进行
  2. 使用了箭头函数,将this进行了改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<!-- 准备好容器 -->
<div id="test">

</div>
</body>

<script type="text/babel">
class St extends React.Component{
//可以直接对其进行赋值
state = {isHot:true};
render(){ //这个This也是实例对象
return <h1 onClick = {this.dem}>今天天气很{this.state.isHot?"炎热":"凉爽"}</h1>
//或者使用{()=>this.dem()也是可以的}
}
//箭头函数 [自定义方法--->要用赋值语句的形式+箭头函数]
dem = () =>{
console.log(this);
const state = this.state.isHot;
this.setState({isHot:!state});
}
}
ReactDOM.render(<St />,document.getElementById("test"));
</script>

如果想要在调用方法的时候传递参数,有两个方法:

1
2
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

上述两种方式是等价的,分别通过箭头函数 和 **Function.prototype.bind**来实现。

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

2.4 State 的更新可能是异步的

React控制之外的事件中调用setState是同步更新的。比如原生js绑定的事件,setTimeout/setInterval等

18版本中测试setTimeout回调函数中也是异步更新的

大部分开发中用到的都是React封装的事件,比如onChange、onClick、onTouchMove等,这些事件处理程序中的setState都是异步处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.创建组件
class St extends React.Component{
//可以直接对其进行赋值
state = {isHot:10};
render(){ //这个This也是实例对象
return <h1 onClick = {this.dem}>点击事件</h1>
}
//箭头函数 [自定义方法--->要用赋值语句的形式+箭头函数]
dem = () =>{
//修改isHot
this.setState({ isHot: this.state.isHot + 1})
console.log(this.state.isHot);
}
}

上面的案例中预期setState使得isHot变成了11,输出也应该是11。然而在控制台打印的却是10,也就是并没有对其进行更新。这是因为异步的进行了处理,在输出的时候还没有对其进行处理。

1
2
3
4
5
document.getElementById("test").addEventListener("click",()=>{
this.setState({isHot: this.state.isHot + 1});
console.log(this.state.isHot);
})
}

但是通过这个原生JS的,可以发现,控制台打印的就是11,也就是已经对其进行了处理。也就是进行了同步的更新。

React怎么调用同步或者异步的呢?

在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates将isBatchingUpdates修改为true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。

如果是同步更新,每一个setState对调用一个render,并且如果多次调用setState会以最后调用的为准,前面的将会作废;如果是异步更新,多个setSate会统一调用一次render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dem = () =>{
this.setState({
isHot: 1,
cont:444
})
this.setState({
isHot: this.state.isHot + 1

})
this.setState({
isHot: 888,
cont:888
})
}

上面的最后会输出:isHot是888,cont是888

1
2
3
4
5
6
7
8
9
10
11
12
13
dem = ()=> {  
this.setState({
isHot: this.state.isHot + 1,

})
this.setState({
isHot: this.state.isHot + 1,

})
this.setState({
isHot: this.state.isHot + 888
})
}

初始isHot为10,最后isHot输出为898,也就是前面两个都没有执行。

**注意!!这是异步更新才有的,如果同步更新,每一次都会调用render,这样每一次更新都会 **

2.5 异步更新解决方案

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.propsthis.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

例如,此代码可能会无法更新计数器:

1
2
3
4
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

1
2
3
4
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));

上面使用了箭头函数 ,不过使用普通的函数也同样可以:

1
2
3
4
5
6
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});

2.6 数据是向下流动的

不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。

这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。

组件可以选择把它的 state 作为 props 向下传递到它的子组件中:

1
<FormattedDate date={this.state.date} />

FormattedDate 组件会在其 props 中接收参数 date,但是组件本身无法知道它是来自于 Clock 的 state,或是 Clock 的 props,还是手动输入的:

1
2
3
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

在 CodePen 上尝试

这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。

如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。

为了证明每个组件都是真正独立的,我们可以创建一个渲染三个 ClockApp 组件:

1
2
3
4
5
6
7
8
9
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}

在 CodePen 上尝试

每个 Clock 组件都会单独设置它自己的计时器并且更新它。

在 React 应用中,组件是有状态组件还是无状态组件属于组件实现的细节,它可能会随着时间的推移而改变。你可以在有状态的组件中使用无状态的组件,反之亦然。

3.props

3.1 基本使用

state不同,state是组件自身的状态,而props则是外部传入的数据

基本使用:

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

</div>

</body>
<script type="text/babel">
class Person extends React.Component{
render(){
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age + 1}</li>
</ul>
)
}
}
//传递数据
ReactDOM.render(<Person name="tom" age = {41} sex="男"/>,document.getElementById("div"));
</script>

如果传递的数据是一个对象,可以更加简便的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script type="text/babel">
class Person extends React.Component{
render(){
return (
<ul>
<li>{this.props.name}</li>
<li>{this.props.age}</li>
<li>{this.props.sex}</li>
</ul>
)
}
}
const p = {name:"张三",age:"18",sex:"女"}
ReactDOM.render(<Person {...p}/>,document.getElementById("div"));
</script>

… 这个符号恐怕都不陌生,这个是一个展开运算符,主要用来展开数组,如下面这个例子:

1
2
3
arr = [1,2,3];
arr1 = [4,5,6];
arr2 = [...arr,...arr1]; //arr2 = [1,2,3,4,5,6]

但是他还有其他的用法:

  1. 复制一个对象给另一个对象{…对象名}。此时这两个对象并没有什么联系了
1
2
3
4
const p1 = {name:"张三",age:"18",sex:"女"}
const p2 = {...p1};
p1.name = "sss";
console.log(p2) //{name:"张三",age:"18",sex:"女"}
  1. 在复制的时候,合并其中的属性
1
2
3
4
const p1 = {name:"张三",age:"18",sex:"女"}
const p2 = {...p1,name : "111",hua:"ss"};
p1.name = "sss";
console.log(p2) //{name: "111", age: "18", sex: "女",hua:"ss"}

注意!! {…P}并不能展开一个对象

props传递一个对象,是因为babel+react使得{..p}可以展开对象,但是只有在标签中才能使用

3.2 props限制

注意:

自 React v15.5 起,React.PropTypes 已移入另一个包中。请使用 prop-types 代替。

我们提供了一个 codemod 脚本 来做自动转换。

随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 Flow TypeScript 等 JavaScript 扩展来对整个应用程序做类型检查。但即使你不使用这些扩展,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 propTypes 属性:

react对此提供了相应的解决方法:

  • propTypes:类型检查,还可以限制不能为空
  • defaultProps:默认值

从 ES2022 开始,你也可以在 React 类组件中将 defaultProps 声明为静态属性。欲了解更多信息,请参阅 class public static fields 。这种现代语法需要添加额外的编译步骤才能在老版浏览器中工作。

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
<!-- 准备好一个“容器” -->
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>

<script type="text/babel">
//创建组件
class Person extends React.Component{
render(){
// console.log(this);
const {name,age,sex} = this.props
//props是只读的
//this.props.name = 'jack' //此行代码会报错,因为props是只读的
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
}
//对标签属性进行类型、必要性的限制
Person.propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string,//限制sex为字符串
age:PropTypes.number,//限制age为数值
speak:PropTypes.func,//限制speak为函数
}
//指定默认标签属性值
Person.defaultProps = {
sex:'男',//sex默认值为男
age:18 //age默认值为18
}
//渲染组件到页面
ReactDOM.render(<Person name={100} speak={speak}/>,document.getElementById('test1'))
ReactDOM.render(<Person name="tom" age={18} sex="女"/>,document.getElementById('test2'))

const p = {name:'老刘',age:18,sex:'女'}
// console.log('@',...p);
// ReactDOM.render(<Person name={p.name} age={p.age} sex={p.sex}/>,document.getElementById('test3'))
ReactDOM.render(<Person {...p}/>,document.getElementById('test3'))

function speak(){
console.log('我说话了');
}
</script>

当传入的 prop 值类型不正确时,JavaScript 控制台将会显示警告。出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

defaultProps 用于确保 this.props.sex 在父组件没有指定其值时,有一个默认值。propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps

PropTypes

以下提供了使用不同验证器的例子:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import PropTypes from 'prop-types';

MyComponent.propTypes = {
// 你可以将属性声明为 JS 原生类型,默认情况下
// 这些属性都是可选的。
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,

// 任何可被渲染的元素(包括数字、字符串、元素或数组)
// (或 Fragment) 也包含这些类型。
optionalNode: PropTypes.node,

// 一个 React 元素。
optionalElement: PropTypes.element,

// 一个 React 元素类型(即,MyComponent)。
optionalElementType: PropTypes.elementType,

// 你也可以声明 prop 为类的实例,这里使用
// JS 的 instanceof 操作符。
optionalMessage: PropTypes.instanceOf(Message),

// 你可以让你的 prop 只能是特定的值,指定它为
// 枚举类型。
optionalEnum: PropTypes.oneOf(['News', 'Photos']),

// 一个对象可以是几种类型中的任意一个类型
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),

// 可以指定一个数组由某一类型的元素组成
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

// 可以指定一个对象由某一类型的值组成
optionalObjectOf: PropTypes.objectOf(PropTypes.number),

// 可以指定一个对象由特定的类型值组成
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),

// 具有额外属性警告的对象
optionalObjectWithStrictShape: PropTypes.exact({
name: PropTypes.string,
quantity: PropTypes.number
}),

// 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
// 这个 prop 没有被提供时,会打印警告信息。
requiredFunc: PropTypes.func.isRequired,

// 任意类型的必需数据
requiredAny: PropTypes.any.isRequired,

// 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
// 请不要使用 `console.warn` 或抛出异常,因为这在 `oneOfType` 中不会起作用。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
},

// 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
// 它应该在验证失败时返回一个 Error 对象。
// 验证器将验证数组或对象中的每个值。验证器的前两个参数
// 第一个是数组或对象本身
// 第二个是他们当前的键。
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
};

限制单个元素

你可以通过 PropTypes.element 来确保传递给组件的 children 中只包含一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import PropTypes from 'prop-types';

class MyComponent extends React.Component {
render() {
// 这必须只有一个元素,否则控制台会打印警告。
const children = this.props.children;
return (
<div>
{children}
</div>
);
}
}

MyComponent.propTypes = {
children: PropTypes.element.isRequired
};

3.3 简写方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!-- 准备好一个“容器” -->
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>


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

constructor(props){
//构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
// console.log(props);
super(props)
console.log('constructor',this.props);
}

//对标签属性进行类型、必要性的限制
static propTypes = {
name:PropTypes.string.isRequired, //限制name必传,且为字符串
sex:PropTypes.string,//限制sex为字符串
age:PropTypes.number,//限制age为数值
}

//指定默认标签属性值
static defaultProps = {
sex:'男',//sex默认值为男
age:18 //age默认值为18
}

render(){
// console.log(this);
const {name,age,sex} = this.props
//props是只读的
//this.props.name = 'jack' //此行代码会报错,因为props是只读的
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
}

//渲染组件到页面
ReactDOM.render(<Person name="jerry"/>,document.getElementById('test1'))
</script>

在使用的时候可以通过 this.props来获取值 类式组件的 props:

  1. 通过在组件标签上传递值,在组件中就可以获取到所传递的值
  2. 在构造器里的props参数里可以获取到 props
  3. 可以分别设置 propTypesdefaultProps 两个属性来分别操作 props的规范和默认值,两者都是直接添加在类式组件的原型对象上的(所以需要添加 static
  4. 同时可以通过...运算符来简化

3.4 函数式组件的使用

函数在使用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
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>对props进行限制</title>
</head>
<body>
<!-- 准备好一个“容器” -->
<div id="test1"></div>

<script type="text/babel">
//创建组件
function Person(props) {
const { name, age, sex } = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
Person.propTypes = {
name: PropTypes.string.isRequired, //限制name必传,且为字符串
sex: PropTypes.string, //限制sex为字符串
age: PropTypes.number, //限制age为数值
}

//指定默认标签属性值
Person.defaultProps = {
sex: '男', //sex默认值为男
age: 18, //age默认值为18
}
//渲染组件到页面
ReactDOM.render(<Person name="jerry" />, document.getElementById('test1'))
</script>
</body>
</html>

函数组件的 props定义:

  1. 在组件标签中传递 props的值
  2. 组件函数的参数为 props
  3. props的限制和默认值同样设置在原型对象上

3.5 props 的只读性

组件无论是使用函数声明还是通过 class 声明 ,都绝不能修改自身的 props。来看下这个 sum 函数:

1
2
3
function sum(a, b) {
return a + b;
}

这样的函数被称为“纯函数” ,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。

相反,下面这个函数则不是纯函数,因为它更改了自己的入参:

1
2
3
function withdraw(account, amount) {
account.total -= amount;
}

React 非常灵活,但它也有一个严格的规则:

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。state在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。

4.refs

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。

在我们正常的操作节点时,需要采用DOM API 来查找元素,但是这样违背了 React 的理念,因此有了refs

何时使用 Refs

下面是几个适合使用 refs 的情况:

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。

避免使用 refs 来做任何可以通过声明式实现来完成的事情。

有三种操作refs的方法,分别为:

  • 字符串形式
  • 回调形式
  • createRef形式

勿过度使用 Refs

你可能首先会想到使用 refs 在你的 app 中“让事情发生”。如果是这种情况,请花一点时间,认真再考虑一下 state 属性应该被安排在哪个组件层中。通常你会想明白,让更高的组件层级拥有这个 state,是更恰当的。查看 状态提升 以获取更多有关示例。

4.1 字符串形式

在想要获取到一个DOM节点,可以直接在这个节点上添加ref属性。利用该属性进行获取该节点的值。

案例:给需要的节点添加ref属性,此时该实例对象的refs上就会有这个值。就可以利用实例对象的refs获取已经添加节点的值

1
2
3
4
5
<input ref="dian" type="text" placeholder="点击弹出" />

inputBlur = () =>{
alert(this.refs.shiqu.value);
}

注意

不建议使用它,因为 string 类型的 refs 存在 一些问题 。它已过时并可能会在未来的版本被移除。

如果你目前还在使用 this.refs.textInput 这种方式访问 refs ,我们建议用回调函数 createRef API的方式代替。

4.2 回调形式

React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。

这种方式会将该DOM作为参数传递过去。

组件实例的ref属性传递一个回调函数c => this.input1 = c (箭头函数简写),这样会在实例的属性中存储对DOM节点的引用,使用时可通过this.input1来使用

1
<input ref={e => this.input1 = e } type="text" placeholder="点击按钮提示数据"/>

e会接收到当前节点作为参数,然后将当前节点赋值给实例的input1属性上面

关于回调 refs 的说明

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 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
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={c => {
this.input1 = c
console.log('@', c)
}}
type="text"
/>
<br />
<br />
<button onClick={this.changeWeather}>点我切换天气</button>
</div>
)
}
}

刚渲染完会调用一次

image-20221023153439400

触发模板更新会调用两次

image-20221023153510564

第一次传递一个null值把之前的属性清空,再重新赋值。

如果不想总是这样重新创建新的函数,可以使用下面的方案

下面的例子描述了一个通用的范例:使用 ref 回调函数,在实例的属性中存储对 DOM 节点的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//创建组件
class Demo extends React.Component {
state = { isHot: false }
// 在实例上面创建一个函数
setTextInputRef = e => {
this.input1 = e
}

changeWeather = () => {
console.log(this.input1)
//获取原来的状态
const { isHot } = this.state
//更新状态
this.setState({ isHot: !isHot })
}

render() {
const { isHot } = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
<input ref={this.setTextInputRef} type="text" />
<br />
<button onClick={this.changeWeather}>点我切换天气</button>
</div>
)
}
}

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null

你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef() 创建的对象 refs 一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}

class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el} />
);
}
}

在上面的例子中,Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 <input>。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点。

4.3 createRef 形式(推荐写法)

创建 Refs

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

1
2
3
4
5
6
7
8
9
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}

访问 Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

1
const node = this.myRef.current;

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。

4.4 为 DOM 元素添加 ref

以下代码使用 ref 去存储 DOM 节点的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}

focusTextInput() {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}

render() {
// 告诉 React 我们想把 <input> ref 关联到
// 构造器里创建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMountcomponentDidUpdate 生命周期钩子触发前更新。

注意:我们不要过度的使用 ref,如果发生时间的元素刚好是需要操作的元素,就可以使用事件对象去替代。

4.5 为 class 组件添加 Ref

如果我们想包装上面的 CustomTextInput,来模拟它挂载之后立即被点击的操作,我们可以使用 ref 来获取这个自定义的 input 组件并手动调用它的 focusTextInput 方法:

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

componentDidMount() {
this.textInput.current.focusTextInput();
}

render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}

请注意,这仅在 CustomTextInput 声明为 class 时才有效:

1
2
class CustomTextInput extends React.Component {  // ...
}

4.6 Refs 与函数组件

默认情况下,你不能在函数组件上使用 ref 属性,因为它们没有实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function MyFunctionComponent() {
return <input />;
}

class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// This will *not* work!
return (
<MyFunctionComponent ref={this.textInput} />
);
}
}

如果要在函数组件中使用 ref,你可以使用 forwardRef(可与 useImperativeHandle结合使用),或者可以将该组件转化为 class 组件。

不管怎样,你可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);

function handleClick() {
textInput.current.focus();
}

return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}

03 【事件处理】

React的事件是通过onXxx属性指定事件处理函数

React 使用的是自定义事件,而不是原生的 DOM 事件

React 的事件是通过事件委托方式处理的(为了更加的高效)

可以通过事件的 event.target获取发生的 DOM 元素对象,可以尽量减少 refs的使用

事件中必须返回的是函数

1.React事件

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

例如,传统的 HTML:

1
2
3
<button onclick="activateLasers()">
Activate Lasers
</button>

在 React 中略微不同:

1
2
3
<button onClick={activateLasers}>  
Activate Lasers
</button>

在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式地使用 preventDefault。例如,传统的 HTML 中阻止表单的默认提交行为,你可以这样写:

1
2
3
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>

在 React 中,可能是这样的:

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

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

在这里,e 是一个合成事件。React 根据 W3C 规范 来定义这些合成事件,所以你不需要担心跨浏览器的兼容性问题。React 事件与原生事件不完全相同。如果想了解更多,请查看 SyntheticEvent 参考指南。

使用 React 时,你一般不需要使用 addEventListener 为已创建的 DOM 元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加监听器即可。

2.类式组件绑定事件

当你使用 ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。例如,下面的 Toggle 组件会渲染一个让用户切换开关状态的按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 为了在回调中使用 `this`,这个绑定是必不可少的 this.handleClick = this.handleClick.bind(this); }

handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}

render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}

在 CodePen 上尝试

你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined

这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理 有关。通常情况下,如果你没有在方法后面添加 (),例如 onClick={this.handleClick},你应该为这个方法绑定 this

如果觉得使用 bind 很麻烦,这里有两种方式可以解决。你可以使用 public class fields 语法 to correctly bind callbacks:

1
2
3
4
5
6
7
8
9
10
11
12
13
class LoggingButton extends React.Component {
// This syntax ensures `this` is bound within handleClick.
handleClick = () => {
console.log('this is:', this);
};
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}

Create React App 默认启用此语法。

如果你没有使用 class fields 语法,你可以在回调中使用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}

render() {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}

此语法问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。

3.向事件处理程序传递参数

在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:

1
2
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

上述两种方式是等价的,分别通过箭头函数 Function.prototype.bind来实现。

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。、

4.收集表单数据

首先我们先来创建一个简单的表单组件:

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

const MyForm = () => {
return (
<form>
<div>
用户名 <input type="text"/>
</div>
<div>
密码 <input type="password"/>
</div>
<div>
电子邮件 <input type="email"/>
</div>

<div>
<button>提交</button>
</div>
</form>
);
};

export default MyForm;

首先使用React定义表单和之前传统网页中的表单有一些区别,传统网页中form需要指定action和method两个属性,而表单项也必须要指定name属性,这些属性都是提交表单所必须的。但是在React中定义表单时,这些属性通通都可以不指定,因为React中的表单所有的功能都需要通过代码来控制,包括获取表单值和提交表单,所以这些东西都可以在函数中指定并通过AJAX发送请求,无需直接在表单中设置。

首先我们来研究一下如何获取表单中的用户所填写的内容,要获取用户所填写的内容我们必须要监听表单onChange事件,在表单项发生变化时获取其中的内容,在响应函数中通过事件对象的target.value来获取用户填写的内容。事件响应函数大概是这个样子:

1
2
3
const nameChangeHandler= e => {
//e.target.value 表示当前用户输入的值
};

然后我们再将该函数设置为input元素的onChange事件的响应函数:

1
2
3
<div>
用户名 <input type="text" onChange={nameChangeHandler}/>
</div>

这样一来当用户输入内容时,nameChangeHandler就会被触发,从而通过e.target.value来获取用户输入的值。通常我们还会为表单项创建一个state用来存储值:

1
2
3
4
5
const [inputName, setInputName] = useState(''); 
const nameChangeHandler = e => {
//e.target.value 表示当前用户输入的值
setInputName(e.target.value);
};

上例中用户名存储到了变量inputName中,inputName也会设置为对应表单项的value属性值,这样一来当inputName发生变化时,表单项中的内容也会随之改变:

1
2
3
<div>
用户名 <input type="text" onChange={nameChangeHandler} value={inputName}/>
</div>

如此设置后,当用户输入内容后会触发onChange事件从而调用nameChangeHandler函数,在函数内部调用了setInputName设置了用户输入的用户名。换句话说用户在表单中输入内容会影响到state的值,同时当我们修改state的值时,由于表单项的value属性值指向了state,表单也会随state值一起改变。这种绑定方式我们称为双向绑定,即表单会改变state,state也可以改变表单,在开发中使用双向绑定的表单项是最佳实践。

5.受控和非受控组件

先来说说受控组件:

使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
saveName = (event) =>{
this.setState({name:event.target.value});
}

savePwd = (event) => {
this.setState({pwd:event.target.value});
}

render() {
return (
<form action="http://www.baidu.com" onSubmit={this.login}>
用户名:<input value={this.state.name} onChange={this.saveName} type = "text" />
密码<input value={this.state.pwd} onChange={this.savePwd} type = "password"/>
<button>登录</button>
</form>
)
}

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 onchange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。

对于受控组件来说,输入的值始终由 React 的 state 驱动。

非受控组件:

非受控组件其实就是表单元素的值不会更新state。输入数据都是现用现取的。

如下:下面并没有使用state来控制属性,使用的是事件来控制表单的属性值。

表单提交同样需要通过事件来处理,提交表单的事件通过form标签的onSubmit事件来绑定,处理表单的方式因情况而已,但是一定要注意,必须要取消默认行为,否则会触发表单的默认提交行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Login extends React.Component{

login = (event) =>{
event.preventDefault(); //阻止表单默认事件
console.log(this.name.value);
console.log(this.pwd.value);
}
render() {
return (
<form action="http://www.baidu.com" onSubmit={this.login}>
用户名:<input ref = {e => this.name =e } type = "text" name ="username"/>
密码: <input ref = {e => this.pwd =e } type = "password" name ="password"/>
<button>登录</button>
</form>
)
}
}

5.函数的柯里化

高级函数

  1. 如果函数的参数是函数
  2. 如果函数返回一个函数

通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式

如下,我们将上面的案例简化,创建高级函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 class Login extends React.Component{
state = {name:"",pwd:""};

//返回一个函数
saveType = (type) =>{
return (event) => {
this.setState({[type]:event.target.value});
}
}

//因为事件中必须是一个函数,所以返回的也是一个函数,这样就符合规范了
render() {
return (
<form>
<input onChange = {this.saveType('name')} type = "text"/>
<button>登录</button>
</form>
)
}
}

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

不使用函数柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 class Login extends React.Component{
state = {name:"",pwd:""};

//返回一个函数
saveType = (type,event) =>{
this.setState({[type]:event.target.value});
}

//因为事件中必须是一个函数,所以返回的也是一个函数,这样就符合规范了
render() {
return (
<form>
<input onChange = {event => this.saveType('name',event)} type = "text"/>
<button>登录</button>
</form>
)
}
}

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

04 【生命周期】

1.简介

组件从创建到死亡,会经过一些特定的阶段

React组件中包含一系列钩子函数{生命周期回调函数},会在特定的时刻调用

我们在定义组件的时候,会在特定的声明周期回调函数中,做特定的工作

在 React 中为我们提供了一些生命周期钩子函数,让我们能在 React 执行的重要阶段,在钩子函数中做一些事情。那么在 React 的生命周期中,有哪些钩子函数呢,我们来总结一下

react生命周期(旧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
1. constructor()
2. componentWillMount()
3. render()
4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件render触发
1. shouldComponentUpdate()
2. componentWillUpdate()
3. render() =====> 必须使用的一个
4. componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

react生命周期(旧)

在最新的react版本中,有些生命周期钩子被抛弃了,具体函数如下:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

这些生命周期方法经常被误解和滥用;此外,我们预计,在异步渲染中,它们潜在的误用问题可能更大。我们将在即将发布的版本中为这些生命周期添加 “UNSAFE_” 前缀。(这里的 “unsafe” 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染之后。)

由此可见,新版本中并不推荐持有这三个函数,取而代之的是带有UNSAFE_ 前缀的三个函数,比如: UNSAFE_ componentWillMount。即便如此,其实React官方还是不推荐大家去使用,在以后版本中有可能会去除这几个函数。

react生命周期(新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
1. constructor()
2. getDerivedStateFromProps
3. render()
4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
1. getDerivedStateFromProps
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate
5. componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

image-20221023222949399

2.初始化阶段

在组件实例被创建并插入到dom中时,生命周期调用顺序如下

旧生命周期:

  1. constructor(props)
  2. componentWillMount()————-可以用但是不建议使用
  3. render()
  4. componentDidMount()

新生命周期:

  1. constructor(props)
  2. static getDerivedStateFromProps(props,state)–替代了componentWillReceiveProps
  3. render()
  4. componentDidMount()

2.1 constructor

数据的初始化。

接收props和context,当想在函数内使用这两个参数需要在super传入参数,当使用constructor时必须使用super,否则可能会有this的指向问题,如果不初始化state或者不进行方法绑定,则可以不为组件实现构造函数;

避免将 props 的值复制给 state!这是一个常见的错误:

1
2
3
4
5
constructor(props) {
super(props);
// 不要这样做
this.state = { color: props.color };
}

如此做毫无必要(可以直接使用 this.props.color),同时还产生了 bug(更新 prop 中的 color 时,并不会影响 state)。

现在我们通常不会使用 constructor 属性,而是改用类加箭头函数的方法,来替代 constructor

例如,我们可以这样初始化 state

1
2
3
state = {
count: 0
};

2.2 componentWillMount(即将废弃)

该方法只在挂载的时候调用一次,表示组件将要被挂载,并且在 render 方法之前调用。

如果存在 getDerivedStateFromPropsgetSnapshotBeforeUpdate 就不会执行生命周期componentWillMount

在服务端渲染唯一会调用的函数,代表已经初始化数据但是没有渲染dom,因此在此方法中同步调用 setState() 不会触发额外渲染。

这个方法在 React 18版本中将要被废弃,官方解释是在 React 异步机制下,如果滥用这个钩子可能会有 Bug

2.3 static getDerivedStateFromProps(新钩子)

从props获取state。

替代了componentWillReceiveProps,此方法适用于罕见的用例 ,即 state 的值在任何时候都取决于 props。

这个是 React 新版本中新增的2个钩子之一,据说很少用。

  1. 首先,该函数会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用;
  2. 该函数必须是静态的;
  3. 给组件传递的数据(props)以及组件状态(state),会作为参数到这个函数中;
  4. 该函数也必须有返回值,返回一个Null或者state对象。因为初始化和后续更新都会执行这个方法,因此在这个方法返回state对象,就相当于将原来的state进行了覆盖,所以倒是修改状态不起作用。

注意:state 的值在任何时候都取决于传入的 props ,不会再改变

如下

1
2
3
4
static getDerivedStateFromProps(props, state) {
return null
}
ReactDOM.render(<Count count="109"/>,document.querySelector('.test'))

count 的值不会改变,一直是 109

React的生命周期 - 简书

老版本中的componentWillReceiveProps()方法判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。

这两者最大的不同就是: 在 componentWillReceiveProps 中,我们一般会做以下两件事,一是根据 props 来更新 state,二是触发一些回调,如动画或页面跳转等。

  1. 在老版本的 React 中,这两件事我们都需要在 componentWillReceiveProps 中去做。
  2. 而在新版本中,官方将更新 state 与触发回调重新分配到了 getDerivedStateFromProps 与 componentDidUpdate 中,使得组件整体的更新逻辑更为清晰。而且在 getDerivedStateFromProps 中还禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情

2.4 render

class组件中唯一必须实现的方法。

render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。

注意:避免在 render 中使用 setState ,否则会死循环

当render被调用时,他会检查this.props.和this.state的变化并返回以下类型之一:

  1. 通过jsx创建的react元素
  2. 数组或者fragments:使得render可以返回多个元素
  3. Portals:可以渲染子节点到不同的dom树上
  4. 字符串或数值类型:他们在dom中会被渲染为文本节点
  5. 布尔类型或者null:什么都不渲染

2.5 componentDidMount

在组件挂在后(插入到dom树中)后立即调用

componentDidMount 的执行意味着初始化挂载操作已经基本完成,它主要用于组件挂载完成后做某些操作

这个挂载完成指的是:组件插入 DOM tree

可以在这里调用Ajax请求,返回的数据可以通过setState使组件重新渲染,或者添加订阅,但是要在conponentWillUnmount中取消订阅

2.6 初始化阶段总结

执行顺序 constructor -> getDerivedStateFromProps 或者 componentWillMount -> render -> componentDidMount

image-20221023223048451

3.更新阶段

当组件的 props 或 state 发生变化时会触发更新。

旧生命周期:

  1. componentWillReceiveProps (nextProps)——————可以用但是不建议使用
  2. shouldComponentUpdate(nextProps,nextState)
  3. componetnWillUpdate(nextProps,nextState)—————-可以用但是不建议使用
  4. render()
  5. componentDidUpdate(prevProps,precState,snapshot)

新生命周期:

  1. static getDerivedStateFromProps(nextProps, prevState)
  2. shouldComponentUpdate(nextProps,nextState)
  3. render()
  4. getSnapshotBeforeUpdate(prevProps,prevState)
  5. componentDidUpdate(prevProps,precState,snapshot)

3.1 componentWillReceiveProps (即将废弃)

在已挂载的组件接收新的props之前调用。

通过对比nextProps和this.props,将nextProps的state为当前组件的state,从而重新渲染组件,可以在此方法中使用this.setState改变state。

1
2
3
4
5
6
7
8
componentWillReceiveProps (nextProps) {
nextProps.openNotice !== this.props.openNotice&&this.setState({
openNotice:nextProps.openNotice
},() => {
console.log(this.state.openNotice:nextProps)
//将state更新为nextProps,在setState的第二个参数(回调)可以打 印出新的state
})
}

请注意,如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。

React 不会针对初始 props 调用 UNSAFE_componentWillReceiveProps()。组件只会在组件的 props 更新时调用此方法。调用 this.setState() 通常不会触发该生命周期。

3.2 shouldComponentUpdate

在渲染之前被调用,默认返回为true。

返回值是判断组件的输出是否受当前state或props更改的影响,默认每次state发生变化都重新渲染,首次渲染或使用forceUpdate(使用this.forceUpdate())时不被调用。

他主要用于性能优化,会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。不建议深层比较,会影响性能。如果返回false,则不会调用componentWillUpdate、render和componentDidUpdate

  • 唯一用于控制组件重新渲染的生命周期,由于在react中,setState以后,state发生变化,组件会进入重新渲染的流程,在这里return false可以阻止组件的更新,但是不建议,建议使用 PureComponent
  • 因为react父组件的重新渲染会导致其所有子组件的重新渲染,这个时候其实我们是不需要所有子组件都跟着重新渲染的,因此需要在子组件的该生命周期中做判断

3.3 componentWillUpdate (即将废弃)

当组件接收到新的props和state会在渲染前调用,初始渲染不会调用该方法。

shouldComponentUpdate返回true以后,组件进入重新渲染的流程,进入componentWillUpdate,不能在这使用setState,在函数返回之前不能执行任何其他更新组件的操作

此方法可以替换为 componentDidUpdate()。如果你在此方法中读取 DOM 信息(例如,为了保存滚动位置),则可以将此逻辑移至 getSnapshotBeforeUpdate() 中。

3.4 getSnapshotBeforeUpdate(新钩子)

在最近一次的渲染输出之前被提交之前调用,也就是即将挂载时调用,替换componetnWillUpdate。

相当于淘宝购物的快照,会保留下单前的商品内容,在 React 中就相当于是 即将更新前的状态

它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给 componentDidUpdate()。如不需要传递任何值,那么请返回 null

和componentWillUpdate的区别

  • 在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
  • getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。

3.5 componentDidUpdate

组件在更新完毕后会立即被调用,首次渲染不会调用

可以在该方法调用setState,但是要包含在条件语句中,否则一直更新会造成死循环。

当组件更新后,可以在此处对 DOM 进行操作。如果对更新前后的props进行了比较,可以进行网络请求。(当 props 未发生变化时,则不会执行网络请求)。

1
2
3
4
5
6
componentDidUpdate(prevProps,prevState,snapshotValue) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}

如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshotValue” 参数传递。否则此参数将为 undefined。如果返回false就不会调用这个函数。

3.6 getSnapshotBeforeUpdate使用场景

在一个区域内,定时的输出以行话,如果内容大小超过了区域大小,就出现滚动条,但是内容不进行移动

BeforeGender

如上面的动图:区域内部的内容展现没有变化,但是可以看见滚动条在变化,也就是说上面依旧有内容在输出,只不过不在这个区域内部展现。

  1. 首先我们先实现定时输出内容

我们可以使用state状态,改变新闻后面的值,但是为了同时显示这些内容,我们应该为state的属性定义一个数组。并在创建组件之后开启一个定时器,不断的进行更新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
class New extends React.Component{

state = {num:[]};

//在组件创建之后,开启一个定时任务
componentDidMount(){
setInterval(()=>{
let {num} = this.state;
const news = (num.length+1);
this.setState({num:[news,...num]});
},2000);
}

render(){
return (
<div ref = "list" className = "list">{
this.state.num.map((n,index)=>{
return <div className="news" key={index} >新闻{n}</div>
})
}</div>
)
}
}
ReactDOM.render(<New />,document.getElementById("div"));
  1. 接下来就是控制滚动条了

我们在组件渲染到DOM之前获取组件的高度,然后用组件渲染之后的高度减去之前的高度就是一条新的内容的高度,这样在不断的累加到滚动条位置上。

1
2
3
4
5
6
7
getSnapshotBeforeUpdate(){
return this.refs.list.scrollHeight;
}

componentDidUpdate(preProps,preState,height){
this.refs.list.scrollTop += (this.refs.list.scrollHeight - height);
}

这样就实现了这个功能。

4.卸载组件

当组件从 DOM中移除时会调用如下方法

4.1 componentWillUnmount

在组件卸载和销毁之前调用

使用这样的方式去卸载ReactDOM.unmountComponentAtNode(document.getElementById('test'))

在这执行必要的清理操作,例如,清除timer(setTimeout,setInterval),取消网络请求,或者取消在componentDidMount的订阅,移除所有监听

有时候我们会碰到这个warning:

1
Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a   no-op. Please check the code for the undefined component.

原因:因为你在组件中的ajax请求返回setState,而你组件销毁的时候,请求还未完成,因此会报warning

解决方法:

1
2
3
4
5
6
7
8
9
10
11
componentDidMount() {
this.isMount === true
axios.post().then((res) => {
this.isMount && this.setState({ // 增加条件ismount为true时
aaa:res
})
})
}
componentWillUnmount() {
this.isMount === false
}

componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

05 【条件渲染】

在 React 中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。

基础配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<style>
.other {
color: #ff0000;
}
</style>
<body>
<div id="app"></div>

<script type="text/babel">
class Demo extends React.Component {
state = {
type: 1,
isLogin:false
}

render() {
const {type} = this.state
return (
<div>
{type}
</div>
);
}
}

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

1.条件判断语句

React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if 或者条件运算符 去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。

  • 适合逻辑较多的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1. 第一种方法,声明函数返回dom
showMsg = () => {
let type = this.state.type
if (type === 1) {
return (<h2>第一种写法:type值等于1</h2>)
} else {
return (<h2 className="other">第一种写法:type值不等于1</h2>)
}
}
render() {
return (
<div>
{this.showMsg()}
</div>
);
}

页面展示:

image-20221024141313955

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
render() {
let welcome = ''
let btnText = ''
if (this.state.isLogin) {
welcome = '欢迎回来'
btnText = '退出'
} else {
welcome = '请先登录~'
btnText = '登录'
}

return (
<div>
<h2>{welcome}</h2>
<button>{btnText}</button>
</div>
)
}

image-20221024141928366

2.三目运算符

另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 condition ? true : false

  • 适合逻辑比较简单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render() {
const { type } = this.state
return (
<div>
{
//3. 第三种方法,利用三目运算符渲染需要渲染的变量
type === 1 ? (
<h2>第二种写法:type值等于1</h2>
) : (
<h2 className="other">第三种写法:type值不等于1</h2>
)
}
</div>
)
}
}

image-20221024142209840

在下面这个示例中,我们用它来条件渲染一小段文本

1
2
3
4
5
6
7
8
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
</div>
);
}

同样的,它也可以用于较为复杂的表达式中,虽然看起来不是很直观:

1
2
3
4
5
6
7
8
9
10
11
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}

就像在 JavaScript 中一样,你可以根据团队的习惯来选择可读性更高的代码风格。需要注意的是,如果条件变得过于复杂,那你应该考虑如何提取组件

3.与运算符&&

通过花括号包裹代码,你可以在 JSX 中嵌入表达式 。这也包括 JavaScript 中的逻辑与 (&&) 运算符。它可以很方便地进行元素的条件渲染:

  • 适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染;
1
2
3
4
5
6
7
8
9
10
render() {
const { type } = this.state
return (
<div>
{type === 1 && <h2>第三种写法:type值等于1</h2>}
{type !== 1 && <h2 className="other">第三种写法:type值不等于1</h2>}
</div>
)
}
}

image-20221024142459699

之所以能这样做,是因为在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false

因此,如果条件是 true&& 右侧的元素就会被渲染,如果是 false,React 会忽略并跳过它。

请注意,falsy 表达式 会使 && 后面的元素被跳过,但会返回 falsy 表达式的值。在下面示例中,render 方法的返回值是 <div>0</div>

1
2
3
4
5
6
7
8
render() {
const count = 0;
return (
<div>
{count && <h1>Messages: {count}</h1>}
</div>
);
}

4.元素变量

你可以使用变量来储存元素。 它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。

1
2
3
4
5
6
7
8
9
10
11
12
render() {
const { type } = this.state
//2. 第二种方法 声明变量 给变量赋值
let test = null
if (type === 1) {
test = <h2>第四种写法:type值等于1</h2>
} else {
test = <h2 className="other">第四种写法:type值不等于1</h2>
}
return <div>{test}</div>
}
}

image-20221024142858454

声明一个变量并使用 if 语句进行条件渲染是不错的方式,但有时你可能会想使用更为简洁的语法,那就是内联条件渲染的方法与运算和三目运算符

5.阻止组件渲染

在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。

下面的示例中,<WarningBanner /> 会根据 prop 中 warn 的值来进行条件渲染。如果 warn 的值是 false,那么组件则不会渲染:

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
function WarningBanner(props) {
if (!props.warn) {
return null;
}

return (
<div className="warning">
Warning!
</div>
);
}

class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true};
this.handleToggleClick = this.handleToggleClick.bind(this);
}

handleToggleClick() {
this.setState(state => ({
showWarning: !state.showWarning
}));
}

render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
);
}
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Page />);

在组件的 render 方法中返回 null 并不会影响组件的生命周期。例如,上面这个示例中,componentDidUpdate 依然会被调用。

06 【列表 & Key】

首先,让我们看下在 Javascript 中如何转化列表。

如下代码,我们使用 map()函数让数组中的每一项变双倍,然后我们得到了一个新的列表 doubled 并打印出来:

1
2
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);console.log(doubled);

代码打印出 [2, 4, 6, 8, 10]

在 React 中,把数组转化为元素 列表的过程是相似的。

1.列表

1.1 渲染多个组件

你可以通过使用 {} 在 JSX 内构建一个元素集合

下面,我们使用 Javascript 中的 map()方法来遍历 numbers 数组。将数组中的每个元素变成 <li> 标签,最后我们将得到的数组赋值给 listItems

1
2
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) => <li>{number}</li>);

然后,我们可以将整个 listItems 插入到 <ul> 元素中:

1
<ul>{listItems}</ul>

在 CodePen 上尝试

1
2
3
4
5
6
7
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((numbers) =>
<li>{numbers}</li>
);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ul>{listItems}</ul>);

这段代码生成了一个 1 到 5 的项目符号列表。

image-20221024211657792

1.2 基础列表组件

通常你需要在一个组件 中渲染列表。

我们可以把前面的例子重构成一个组件,这个组件接收 numbers 数组作为参数并输出一个元素列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}

const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);

当我们运行这段代码,将会看到一个警告 a key should be provided for list items,意思是当你创建一个元素时,必须包括一个特殊的 key 属性。我们将在下一节讨论这是为什么。

让我们来给每个列表元素分配一个 key 属性来解决上面的那个警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}

const numbers = [1, 2, 3, 4, 5];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( <NumberList numbers={numbers} />);

在 CodePen 上尝试

image-20221024211835947

2.key

2.1 基本使用

key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。

1
2
3
4
5
6
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);

一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key:

1
2
3
4
5
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);

当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key:

1
2
3
4
5
6
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);

如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。可以看看 Robin Pokorny 的深度解析使用索引作为 key 的负面影响 这一篇文章。如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。

要是你有兴趣了解更多的话,这里有一篇文章深入解析为什么 key 是必须的 可以参考。

2.2 用 key 提取组件

元素的 key 只有放在就近的数组上下文中才有意义。

比方说,如果你提取 出一个 ListItem 组件,你应该把 key 保留在数组中的这个 <ListItem /> 元素上,而不是放在 ListItem 组件中的 <li> 元素上。

例子:不正确的使用 key 的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function ListItem(props) {
const value = props.value;
return (
// 错误!你不需要在这里指定 key:
<li key={value.toString()}>
{value}
</li>
);
}

function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 错误!元素的 key 应该在这里指定:
<ListItem value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}

例子:正确的使用 key 的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ListItem(props) {
// 正确!这里不需要指定 key:
return <li>{props.value}</li>;
}

function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在数组的上下文中被指定
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}

在 CodePen 上尝试

一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性。

2.3 key 值在兄弟节点之间必须唯一

数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值:

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
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}

const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);

在 CodePen 上尝试

key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值:

1
2
3
4
5
6
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);

上面例子中,Post 组件可以读出 props.id,但是不能读出 props.key

2.4 在 JSX 中嵌入 map()

在上面的例子中,我们声明了一个单独的 listItems 变量并将其包含在 JSX 中:

1
2
3
4
5
6
7
8
9
10
11
12
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}

JSX 允许在大括号中嵌入任何表达式 ,所以我们可以内联 map() 返回的结果:

1
2
3
4
5
6
7
8
9
10
11
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}

在 CodePen 上尝试

这么做有时可以使你的代码更清晰,但有时这种风格也会被滥用。就像在 JavaScript 中一样,何时需要为了可读性提取出一个变量,这完全取决于你。但请记住,如果一个 map() 嵌套了太多层级,那可能就是你提取组件 的一个好时机。

3.diff算法

3.1 什么是虚拟 DOM ?

在谈 diff 算法之前,我们需要先了解虚拟 DOM 。它是一种编程概念,在这个概念里,以一种虚拟的表现形式被保存在内存中。在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,而是 JavaScript 对象

虚拟 DOM 只保留了真实 DOM 节点的一些基本属性,和节点之间的层次关系,它相当于建立在 JavaScript 和 DOM 之间的一层“缓存”

1
2
3
<div class="hello">
<span>hello world!</span>
</div>

上面的这段代码会转化可以转化为虚拟 DOM 结构

1
2
3
4
5
6
7
8
9
10
11
{
tag: "div",
props: {
class: "hello"
},
children: [{
tag: "span",
props: {},
children: ["hello world!"]
}]
}

其中对于一个节点必备的三个属性 tag,props,children

  • tag 指定元素的标签类型,如“lidiv
  • props 指定元素身上的属性,如 classstyle,自定义属性
  • children 指定元素是否有子节点,参数以数组形式传入

而我们在 render 中编写的 JSX 代码就是一种虚拟 DOM 结构。

3.2 diff 算法

每个组件中的每个标签都会有一个key,不过有的必须显示的指定,有的可以隐藏。

如果生成的render出来后就不会改变里面的内容,那么你不需要指定key(不指定key时,React也会生成一个默认的标识),或者将index作为key,只要key不重复即可。

但是如果你的标签是动态的,是有可能刷新的,就必须显示的指定key。使用map进行遍历的时候就必须指定Key:

1
2
3
this.state.num.map((n,index)=>{
return <div className="news" key={index} >新闻{n}</div>
})

这个地方虽然显示的指定了key,但是官网并不推荐使用Index作为Key去使用

这样会很有可能会有效率上的问题

举个例子:

在一个组件中,我们先创建了两个对象,通过循环的方式放入< li>标签中,此时key使用的是index。

1
2
3
4
5
6
7
8
person:[
{id:1,name:"张三",age:18},
{id:2,name:"李四",age:19}
]

this.state.person.map((preson,index)=>{
return <li key = {index}>{preson.name}</li>
})

如下图展现在页面中:

image-20221024225054061

此时,我们想在点击按钮之后动态的添加一个对象,并且放入到li标签中,在重新渲染到页面中。

我们通过修改State来控制对象的添加。

1
2
3
4
5
6
<button onClick={this.addObject}>点击增加对象</button>
addObject = () =>{
let {person} = this.state;
const p = {id:(person.length+1),name:"王五",age:20};
this.setState({person:[p,...person]});
}

如下动图所示:

addObject

这样看,虽然完成了功能。但是其实存在效率上的问题, 我们先来看一下两个前后组件状态的变化:

image-20221024225208300

我们发现,组件第一个变成了王五,张三和李四都移下去了。因为我们使用Index作为Key,这三个标签的key也就发生了改变【张三原本的key是0,现在变成了1,李四的key原本是1,现在变成了2,王五变成了0】

在组件更新状态重新渲染的时候,就出现了问题:

因为react是通过key来比较组件标签是否一致的,拿这个案例来说:

首先,状态更新导致组件标签更新,react根据Key,判断旧的虚拟DOM和新的虚拟DOM是否一致

key = 0 的时候 旧的虚拟DOM 内容是张三 新的虚拟DOM为王五 ,react认为内容改变,从而重新创建新的真实DOM.

key = 1 的时候 旧的虚拟DOM 内容是李四,新的虚拟DOM为张三,react认为内容改变,从而重新创建新的真实DOM

key = 2 的时候 旧的虚拟DOM没有,创建新的真实DOM

这样原本有两个虚拟DOM可以复用,但都没有进行复用,完完全全的都是新创建的;这就导致效率极大的降低。

其实这是因为我们将新创建的对象放在了首位,如果放在最后其实是没有问题的,但是因为官方并不推荐使用Index作为key值,我们推荐使用id作为key值。从而完全避免这样的情况。

3.3 用index作为key可能会引发的问题

key不需要全局唯一,只需在当前列表中唯一即可。元素的key最好是固定的,这里直接举个反例,有些场景我们会使用元素的索引为key像这种:

1
2
const students = ['孙悟空', '猪八戒', '沙和尚'];
const ele = <ul>{students.map((item, index) => <li key={index}>{item}</li>)}</ul>

上例中,我使用了元素的索引(index)作为key来使用,但这有什么用吗?没用!因为index是根据元素位置的改变而改变的,当我们在前边插入一个新元素时,所有元素的顺序都会一起改变,那么它和React中按顺序比较有什么区别吗?没有区别!而且还麻烦了,唯一的作用就是去除了警告。所以我们开发的时候偶尔也会使用索引作为key,但前提是元素的顺序不会发生变化,除此之外不要用索引做key。

  1. 若对数据进行:逆序添加、逆序删除等破坏 顺序操作:会产生没有必要的真实DOM更新 界面效果没问题,但效率低。
  2. 如果结构中还包含输入类的DOM:会产生错误DOM更新 界面有问题。
  3. 注意! 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

开发如何选择key?

最好使用每一条数据的唯一标识作为key 比如id,手机号,身份证号

如果确定只是简单的展示数据,用Index也是可以的

而这个判断key的比较规则就是Diff算法

Diff算法其实就是react生成的新虚拟DOM和以前的旧虚拟DOM的比较规则:

  • 如果旧的虚拟DOM中找到了与新虚拟DOM相同的key:
    • 如果内容没有变化,就直接只用之前旧的真实DOM
    • 如果内容发生了变化,就生成新的真实DOM
  • 如果旧的虚拟DOM中没有找到了与新虚拟DOM相同的key:
    • 根据数据创建新的真实的DOM,随后渲染到页面上

3.4 李立超老师对于虚拟DOM的解释

当我们通过 React 操作DOM时,比如通过 React.createElement() 创建元素时。我们所创建的元素并不是真正的DOM对象而是React元素。这一点可以通过在控制台中打印对象来查看。React元素是React应用的最小组成部分,通过JSX也就是React.createElement()所创建的元素都属于React元素。与浏览器的 DOM 元素不同,React 元素就是一个普通的JS对象,且创建的开销极小。

React元素不是DOM对象,那为什么可以被添加到页面中去呢?实际上每个React元素都会有一个对应的DOM元素,对React元素的所有操作,最终都会转换为对DOM元素操作,也就是所谓的虚拟DOM。要理解虚拟DOM,我们需要先了解它的作用。虚拟DOM就好像我们和真实DOM之间的一个桥梁。有了虚拟DOM,使得我们无需去操作真实的DOM元素,只需要对React元素进行操作,所有操作最终都会映射到真实的DOM元素上。

这不是有点多余吗?直接操作DOM不好吗?为什么要多此一举呢?原因其实很多,这里简单举几个出来。

首先,虚拟DOM简化了DOM操作。凡是用过DOM的都知道Web API到底有多复杂,各种方法,各种属性,数不胜数。查询的、修改的、删除的、添加的等等等等。然而在虚拟DOM将所有的操作都简化为了一种,那就是创建!React元素是不可变对象,一旦创建就不可更改。要修改元素的唯一方式就是创建一个新的元素去替换旧的元素,看起来虽然简单粗暴,实则却是简化了DOM的操作。

其次,解决DOM的兼容性问题。DOM的兼容性是一个历史悠久的问题,如果使用原生DOM,总有一些API会遇到兼容性的问题。使用虚拟DOM就完美的避开了这些问题,所有的操作都是在虚拟DOM上进行的,而虚拟DOM是没有兼容问题的,至于原生DOM是否兼容就不需要我们操心了,全都交给React吧!

最后,我们手动操作DOM时,由于无法完全掌握全局DOM情况,经常会出现不必要的DOM操作,比如,本来只需要修改一个子节点,但却不小心修改了父节点,导致所有的子节点都被修改。效果呈现上可能没有什么问题,但是性能上确实千差万别,修改一个节点和修改多个节点对于系统的消耗可是完全不同的。然而在虚拟DOM中,引入了diff算法,React元素在更新时会通过diff算法和之前的元素进行比较,然后只会对DOM做必要的更新来呈现结果。简单来说,就是拿新建的元素和旧的元素进行比较,只对发生变化的部分对DOM进行更新,减少DOM的操作,从而提升了性能。

07 【收集表单数据】

在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,这是因为表单元素通常会保持一些内部的 state。例如这个纯 HTML 表单只接受一个名称:

1
2
3
4
5
6
7
<form>
<label>
名字:
<input type="text" name="name" />
</label>
<input type="submit" value="提交" />
</form>

此表单具有默认的 HTML 表单行为,即在用户提交表单后浏览到新页面。如果你在 React 中执行相同的代码,它依然有效。但大多数情况下,使用 JavaScript 函数可以很方便的处理表单的提交, 同时还可以访问用户填写的表单数据。实现这种效果的标准方式是使用“受控组件”。

状态属性

表单元素有这么几种属于状态的属性:

  • value,对应 <input><textarea> 所有
  • checked,对应类型为 checkboxradio<input> 所有
  • selected,对应 <option> 所有

在 HTML 中 <textarea> 的值可以由子节点(文本)赋值,但是在 React 中,要用 value 来设置。

表单元素包含以上任意一种状态属性都支持 onChange 事件监听状态值的更改。

针对这些状态属性不同的处理策略,表单元素在 React 里面有两种表现形式。

1.受控组件

在 HTML 中,表单元素(如<input><textarea><select>)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作,被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleChange(event) {
this.setState({value: event.target.value});
}

handleSubmit(event) {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}

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

在 CodePen 上尝试

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 handlechange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。

对于受控组件来说,输入的值始终由 React 的 state 驱动。你也可以将 value 传递给其他 UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。

为什么要有受控组件?

引入受控组件不是说它有什么好处,而是因为 React 的 UI 渲染机制,对于表单元素不得不引入这一特殊的处理方式。

在浏览器 DOM 里面是有区分 attributeproperty 的。attribute 是在 HTML 里指定的属性,而每个 HTML 元素在 JS 对应是一个 DOM 节点对象,这个对象拥有的属性就是 property(可以在 console 里展开一个 DOM 节点对象看一下,HTML attributes 只是对应其中的一部分属性),attribute 对应的 property 会从 attribute 拿到初始值,有些会有相同的名称,但是有些名称会不一样,比如 attribute class 对应的 property 就是 className。(详细解释:.prop .prop() vs .attr()

回到 React 里的 <input> 输入框,当用户输入内容的时候,输入框的 value property 会改变,但是 value attribute 依然会是 HTML 上指定的值(attribute 要用 setAttribute 去更改)。

React 组件必须呈现这个组件的状态视图,这个视图 HTML 是由 render 生成,所以对于

1
2
3
render: function() {
return <input type="text" value="hello"/>;
}

在任意时刻,这个视图总是返回一个显示 hello 的输入框。

2.非受控组件

在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。

2.1 基本概念

要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 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
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}

handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}

在 CodePen 上尝试

因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

如果你还是不清楚在某个特殊场景中应该使用哪种组件,那么 这篇关于受控和非受控输入组件的文章 会很有帮助。

2.2 默认值

在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值。在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。在一个组件已经挂载之后去更新 defaultValue 属性的值,不会造成 DOM 上值的任何更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}

同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked<select><textarea> 支持 defaultValue

3.标签变化

3.1 textarea 标签

在 HTML 中, <textarea> 元素通过其子元素定义其文本:

1
2
3
<textarea>
你好, 这是在 text area 里的文本
</textarea>

而在 React 中,<textarea> 使用 value 属性代替。这样,可以使得使用 <textarea> 的表单和使用单行 input 的表单非常类似:

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 EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
};

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleChange(event) {
this.setState({value: event.target.value});
}

handleSubmit(event) {
alert('提交的文章: ' + this.state.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
文章:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}

请注意,this.state.value 初始化于构造函数中,因此文本区域默认有初值。

3.2 select 标签

在 HTML 中,<select> 创建下拉列表标签。例如,如下 HTML 创建了水果相关的下拉列表:

1
2
3
4
5
6
<select>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option selected value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>

请注意,由于 selected 属性的缘故,椰子选项默认被选中。React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。例如:

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
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleChange(event) {
this.setState({value: event.target.value});
}

handleSubmit(event) {
alert('你喜欢的风味是: ' + this.state.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}

在 CodePen 上尝试

总的来说,这使得 <input type="text">, <textarea><select> 之类的标签都非常相似—它们都接受一个 value 属性,你可以使用它来实现受控组件。

注意

你可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项:

1
<select multiple={true} value={['B', 'C']}>

3.3 文件 input 标签

在 HTML 中,<input type="file"> 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。

1
<input type="file" />

在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。

您应该使用 File API 与文件进行交互。下面的例子显示了如何创建一个 DOM 节点的 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
class FileInput extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
}
handleSubmit(event) {
event.preventDefault();
alert(
`Selected file - ${this.fileInput.current.files[0].name}`
);
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}

const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<FileInput />);

在 CodePen 上尝试

08 【状态提升】

1.介绍

所谓 状态提升 就是将各个子组件的 公共state 提升到它们的父组件进行统一存储、处理(这就是所谓的”单一数据源“),负责setState的函数传到下边的子级组件,然后再将父组件处理后的数据或函数props到各子组件中。

那么如果子组件 要 修改父组件的state该怎么办呢?我们的做法就是 将父组件中负责setState的函数,以props的形式传给子组件,然后子组件在需要改变state时调用即可。

实现方式

实现方式是 利用最近的共同的父级组件中,用props的方式传过去到两个子组件,props中传的是一个setState的方法,通过子组件触发props传过去的方法,进而调用父级组件的setState的方法,改变了父级组件的state,调用父级组件的render方法,进而同时改变了两个子级组件的render。

这是 两个有关连的同级组件的传值,因为react的单项数据流,所以不在两个组件中进行传值,而是提升到 最近的共同的父级组件中,改变父级的state,进而影响了两个子级组件的render。

官网介绍

通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。

2.案例

先写一个温度输入组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TemperatureInput extends React.Component {
state = {
temperature: ''
};
handleChange = (e) => {
this.setState({
temperature : e.target.value
})
};
render() {
return (
<fieldset>
<legend>输入{scaleNames[this.props.scale]}:</legend>
<input type="number" value={this.state.temperature} onChange={this.handleChange}
</fieldset>
)
}
}

这个组件就是一个普通的受控组件,有stateprops以及处理函数。

我们在写另一个组件:

1
2
3
4
5
6
7
8
9
10
class Calculator extends React.Component {
render () {
return (
<div>
<TemperatureInput scale='c'/>
<TemperatureInput scale='f'/>
</div>
)
}
}

这个组件现在没有什么存在的价值,我们仅仅是给两个温度输入组件提供一个父组件,以便我们进行后续的状态提升

现在我们看看网页的样子:

image-20221025123600431

我们可以输入摄氏度和华氏度,但是我们现在想要让这两个温度保持一致,就是我们如果输入摄氏度,那么下面的华氏度可以自动算出来,如果我们输入华氏度,那么摄氏度就可以自动算出来。

那么我们按照现在这种结构的话,是非常难以实现的,因为我们知道这两个组件之间没有任何关系,它们之间是不知道对方的存在,所以我们需要把它们的状态进行提升,提升到它们的父组件当中。

那我们看看如何做修改,首先把子组件(温度输入组件)的状态(state)全部删除,看看是什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TemperatureInput extends React.Component {

handleChange = (e) => {

};

render() {
return (
<fieldset>
<legend>输入{scaleNames[this.props.scale]}:</legend>
<input type="number" value={this.props.temperature} onChange={this.handleChange}/>
</fieldset>
)
}
}

可以看到所有与state有关的东西全部删掉了,然后inputvalue也变成了props,通过父组件传入。那么现在这个温度输入组件其实就是一个受控组件了,仔细回忆一下我们之前讲的受控组件,看看是不是这样意思?

我们通常会在受控组件发生改变的时候传入一个onChange函数来改变受控组件的状态,那么我们这里也是一样,我们通过给 温度输入组件 传入某个函数来让 温度输入组件 中的input发生变化的时候调用,当然这个函数我们可以随意命名,假如我们这里叫做onTemperatureChange函数,那么我们继续修改子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TemperatureInput extends React.Component {

handleChange = (e) => {
this.props.onTemperatureChange(e.target.value);
};

render() {
return (
<fieldset>
<legend>输入{scaleNames[this.props.scale]}:</legend>
<input type="number" value={this.props.temperature} onChange={this.handleChange}/>
</fieldset>
)
}
}

好了,我们的子组件差不多就是这样了,当然我们可以省略那个handleChange函数,因为可以看到这个函数就是调用了一下那个props里的函数,所以我们完全把inputonChange这么写 <input type="number" value={this.props.temperature} onChange={this.props.onTemperatureChange}/>这么写的话注意onTemperatrueChange函数的参数是那个事件,而不是我这里写的e.target.value

再看看我们的父组件如何修改,我们首先补上state,以及子组件对应的onChange处理方法,以及子组件的值。写好之后大概是这个样子:

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
class Calculator extends React.Component {
state = {
celsius: '',
fahrenheit: ''
};

onCelsiusChange = (value) => {
this.setState({
celsius: value,
fahrenheit: tryConvert(value, toFahrenheit)
});
};

onFahrenheitChange = (value) => {
this.setState({
celsius: tryConvert(value, toCelsius),
fahrenheit: value
});
};

render() {
return (
<div>
<TemperatureInput scale='c' temperature={this.state.celsius}
onTemperatureChange={this.onCelsiusChange}/>
<TemperatureInput scale='f' temperature={this.state.fahrenheit}
onTemperatureChange={this.onFahrenheitChange}/>
</div>
)
}
}

这里我们省略的摄氏度与华氏度的转换函数,比较简单,大家自行搜索方法。

09 【组合组件】

1.包含关系

有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。

我们建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中:

组件标签里面包含的子元素会通过 props.children 传递进来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function One(props) {
return (
<div>{props.children}</div>
//特殊的children props
);
}

function Two(props) {
return (
//这使别的组件可以通过JSX嵌套,来将任意组件作为子组件来传递给他们
<One>
<div>Hello</div>
<div>World</div>
</One>
);
}

image-20221025135313079

2.特例关系问题

有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 WelcomeDialog 可以说是 Dialog 的特殊实例。

在 React 中,我们也可以通过组合来实现这一点。“特殊”组件可以通过 props 定制并渲染“一般”组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.FancyBorder {
padding: 10px 10px;
border: 10px solid;
}

.FancyBorder-blue {
border-color: blue;
}

.Dialog-title {
margin: 0;
font-family: sans-serif;
}

.Dialog-message {
font-size: larger;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}

function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thank you for visiting our spacecraft!" />
);
}

在 CodePen 上尝试

组合也同样适用于以 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
33
34
35
36
37
38
39
40
41
42
43
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}

class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}

render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}

handleChange(e) {
this.setState({login: e.target.value});
}

handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}

在 CodePen 上尝试

image-20221025135929891

10 【初始化脚手架】

1.什么是 React 脚手架?

在我们的现实生活中,脚手架最常用的使用场景是在工地,它是为了保证施工顺利的、方便的进行而搭建的,在工地上搭建的脚手架可以帮助工人们高校的去完成工作,同时在大楼建设完成后,拆除脚手架并不会有任何的影响。

在我们的 React 项目中,脚手架的作用与之有异曲同工之妙

React 脚手架其实是一个工具帮我们快速的生成项目的工程化结构,每个项目的结构其实大致都是相同的,所以 React 给我提前的搭建好了,这也是脚手架强大之处之一,也是用 React 创建 SPA 应用的最佳方式

2.为什么要用脚手架?

在前面的介绍中,我们也有了一定的认知,脚手架可以帮助我们快速的搭建一个项目结构

在我之前学习 webpack 的过程中,每次都需要配置 webpack.config.js 文件,用于配置我们项目的相关 loaderplugin,这些操作比较复杂,但是它的重复性很高,而且在项目打包时又很有必要,那 React 脚手架就帮助我们做了这些,它不需要我们人为的去编写 webpack 配置文件,它将这些配置文件全部都已经提前的配置好了。

3.怎么用 React 脚手架?

这也是这篇文章的重点,如何去安装 React 脚手架,并且理解它其中的相关文件作用

首先介绍如何安装脚手架

3.1 安装 React 脚手架

首先确保安装了 npmNode,版本不要太古老,具体是多少不大清楚,建议还是用 npm update 更新一下

然后打开 cmd 命令行工具,全局安装 create-react-app

1
npm i create-react-app -g

然后可以新建一个文件夹用于存放项目

在当前的文件夹下执行

1
create-react-app hello-react

快速搭建项目

再在生成好的 hello-react 文件夹中执行

1
npm start

启动项目

接下来我们看看这些文件都有什么作用

3.2 使用vite创建react项目

vite官网:https://vitejs.cn

  • 什么是vite?—— 新一代前端构建工具。
  • 优势如下:
    • 开发环境中,无需打包操作,可快速的冷启动。
    • 轻量快速的热重载(HMR)。
    • 真正的按需编译,不再等待整个应用编译完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## 创建工程

# npm 6.x
$ npm init vite@latest <project-name> --template react
## 如: npm init vite@latest react-app --template react

# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <project-name> --template react

## 使用 PNPM:
pnpm create vite <project-name> --template react
# pnpm create vite react-app -- --template react

## 进入工程目录
cd <project-name>
## 安装依赖
pnpm install
## 运行
pnpm run dev

3.3 脚手架项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hello-react
├─ .gitignore // 自动创建本地仓库
├─ package.json // 相关配置文件
├─ public // 公共资源
│ ├─ favicon.ico // 浏览器顶部的icon图标
│ ├─ index.html // 应用的 index.html入口
│ ├─ logo192.png // 在 manifest 中使用的logo图
│ ├─ logo512.png // 同上
│ ├─ manifest.json // 应用加壳的配置文件
│ └─ robots.txt // 爬虫给协议文件
├─ src // 源码文件夹
│ ├─ App.css // App组件的样式
│ ├─ App.js // App组件
│ ├─ App.test.js // 用于给APP做测试
│ ├─ index.css // 样式
│ ├─ index.js // 入口文件
│ ├─ logo.svg // logo图
│ ├─ reportWebVitals.js // 页面性能分析文件
│ └─ setupTests.js // 组件单元测试文件
└─ yarn.lock

再介绍一下public目录下的 index.html 文件中的代码意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

以上是删除代码注释后的全部代码

第5行

指定浏览器图标的路径,这里直接采用 %PUBLIC_URL% 原因是 webpack 配置好了,它代表的意思就是 public 文件夹

1
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />

第6行

用于做移动端网页适配

1
<meta name="viewport" content="width=device-width, initial-scale=1" />

第七行

用于配置安卓手机浏览器顶部颜色,兼容性不大好

1
<meta name="theme-color" content="#000000" />

8到11行

用于描述网站信息

1
2
3
4
<meta
name="description"
content="Web site created using create-react-app"
/>

第12行

苹果手机触摸版应用图标

1
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

第13行

应用加壳时的配置文件

1
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

这里面其实最主要的就是App.js以及index.js,一个是组件,一个是将组件渲染到页面中的。

4.第一个脚手架应用

  1. 我们保持public中的Index.html不变
  2. 修改src下面的APP.js以及index.js文件
1
2
3
4
5
6
7
8
9
10
11
12
//创建外壳组件APP
import React from 'react'

class App extends React.Component{
render(){
return (
<div>Hello word</div>
)
}
}

export default App

index.js: 【主要的作用其实就是将App这个组件渲染到页面上】

1
2
3
4
5
6
7
//引入核心库
import React from 'react'
import ReactDOM from 'react-dom'
//引入组件
import App from './App'

ReactDOM.render(<App />,document.getElementById("root"))

这样在重新启动应用,就成功了。

image-20221025142625901

我们也不建议这样直接将内容放入App组件中,尽量还是用内部组件。

我们在顶一个Hello组件:

1
2
3
4
5
6
7
8
9
import React,{Componet} from 'react'

export default class Hello extends Componet{
render() {
return (
<h1>Hello</h1>
)
}
}

在App组件中,进行使用

1
2
3
4
5
6
7
8
9
10
11
12
import React,{Componet} from 'react'
import Hello form './Hello'

class App extends Component{
render(){
return (
<div>
<Hello />
</div>
)
}
}

这样的结果和前面是一样的。

推荐使用这种目录结构去使用组件

image-20221025142952888

5.组件

5.1 组件基本概念

在React中网页被拆分为了一个一个组件,组件是独立可复用的代码片段。具体来说,组件可能是页面中的一个按钮,一个对话框,一个弹出层等。React中定义组件的方式有两种:基于函数的组件和基于类的组件。本节我们先看看基于函数的组件。

基于函数的组件其实就是一个会返回JSX(React元素)的普通的JS函数,你可以这样定义:

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

// 这就是一个组件
function App(){
return <h1>我是一个React的组件!</h1>
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);

函数式组件主要有两个注意点:

  1. 函数名首字母大写
  2. 返回值是一个JSX(React元素)

为了使得项目结构更加的清晰,更易于维护,每个组件通常会存储到一个单独的文件中,比如上例中的App组件,可以存储到App.js中,并通过export导出。

1
2
3
4
5
6
App.js
function App(){
return <h1>我是一个React的组件!</h1>
}

export default App;

或者使用箭头函数

1
2
3
4
5
const App = () => {
return <h1>我是一个React的组件!</h1>;
};

export default App;

在其他文件中使用时,需要先通过import进行引入:

1
2
3
4
5
6
index.js
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);

引入后通过<组件名/><组件名></组件名>即可引入组件。

5.2 组件化编码流程

1.拆分组件:拆分界面,抽取组件

2.实现静态组件

3.实现动态组件

  • 动态的显示初始化数据
    • 数据类型
    • 数据名称
    • 保存在哪个组件
  • 交互

注意事项:

1.拆分组件、实现静态组件。注意className、style的写法

2.动态初始化列表,如何确定将数据放在哪个组件的state中?

  • 某个组件使用:放在自身的state中
  • 某些组件使用:放在他们共同的父组件中【状态提升】

3.关于父子组件之间的通信

  • 父组件给子组件传递数据:通过props传递
  • 子组件给父组件传递数据:通过props传递,要求父组件提前给子组件传递一个函数

4.注意defaultChecked 和checked区别,defalutChecked只是在初始化的时候执行一次,checked没有这个限制,但是必须添加onChange方法类似的还有:defaultValue 和value

5.状态在哪里,操作状态的方法就在哪里

6.CSS样式

6.1 内联样式

在React中可以直接通过标签的style属性来为元素设置样式。style属性需要的是一个对象作为值,来为元素设置样式。

1
2
3
<div style={{color:'red'}}>
我是Div
</div>

传递样式时,需要注意如果样式名不符合驼峰命名法,需要将其修改为符合驼峰命名法的名字。比如:background-color改为backgroundColor。

如果内联样式编写过多,会导致JSX变得异常混乱,此时也可以将样式对象定义到JSX外,然后通过变量引入。

样式过多,JSX会比较混乱:

1
2
3
4
5
6
7
8
9
const StyleDemo = () => {
return (
<div style={{color:'red', backgroundColor:'#bfa', fontSize:20, borderRadius:12}}>
我是Div
</div>
);
};

export default StyleDemo;

可以这样修改:

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

const StyleDemo = () => {
const divStyle = {color: 'red', backgroundColor: '#bfa', fontSize: 20, borderRadius: 12}

return (
<div style={divStyle}>
我是Div
</div>
);
};

export default StyleDemo;

相比第一段代码来说,第二段代码中JSX结构更加简洁。

6.2 在内联样式中使用State

设置样式时,可以根据不同的state值应用不同的样式,比如我们可以在组件中添加一个按钮,并希望通过点击按钮可以切换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
import React, {useState} from 'react';

const StyleDemo = () => {

const [showBorder, setShowBorder] = useState(false);

const divStyle = {
color: 'red',
backgroundColor: '#bfa',
fontSize: 20,
borderRadius: 12,
border: showBorder?'2px red solid':'none'
};

const toggleBorderHandler = ()=> {
setShowBorder(prevState => !prevState);
};

return (
<div style={divStyle}>
我是Div
<button onClick={toggleBorderHandler}>切换边框</button>
</div>
);
};

export default StyleDemo;

上例中添加一个新的state,命名为showBorder,代码是这样的const [showBorder, setShowBorder] = useState(false);当该值为true时,我们希望div可以显示一条2像素的红色边框,当为false时,我们希望div没有边框。默认值为false。

divStyle的最后一个属性是这样设置的border: showBorder?'2px red solid':'none',这里我们根据showBorder的值来设置border样式的值,如果值为true,则设置边框,否则边框设置为none。

toggleBorderHandler 是负责修改showBorder的响应函数,当我们点击按钮后函数会对showBorder进行取反,这样我们的样式就可以根据state的不同值而呈现出不同的效果了。

6.3 外部样式表

那么如何为React组件引入样式呢?很简单直接在组件中import即可。例如:我们打算为Button组件编写一组样式,并将其存储到Button.css中。我们只需要直接在Button.js中引入Button.css就能轻易完成样式的设置。

Button.css:

1
2
3
button{
background-color: #bfa;
}

Button.js:

1
2
3
4
5
import './Button.css';
const Button = () => {
return <button>我是一个按钮</button>;
};
export default Button;

使用这种方式引入的样式,需要注意以下几点:

  1. CSS就是标准的CSS语法,各种选择器、样式、媒体查询之类正常写即可。
  2. 尽量将js文件和css文件的文件名设置为相同的文件名。
  3. 引入样式时直接import,无需指定名字,且引入样式必须以./或../开头。
  4. 这种形式引入的样式是全局样式,有可能会被其他样式覆盖。

6.4 css模块化

当组件逐渐增多起来的时候,我们发现,组件的样式也是越来越丰富,这样就很有可能产生两个组件中样式名称有可能会冲突,这样会根据引入App这个组件的先后顺序,后面的会覆盖前面的,

为了解决这个问题React中还为我们提供了一中方式,CSS Module。

我们可以将CSS Module理解为外部样式表的一种进化版,它的大部分使用方式都和外部样式表类似,不同点在于使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一,所以使用CSS Module后,我们再也不用担心类名重复了

使用方式

CSS Module在React中已经默认支持了(前提是使用了react-scripts),所以无需再引入其他多余的模块。使用CSS Module时需要遵循如下几个步骤:

1.将css文件名修改: index.css — > index.module.css

2.引入并使用的时候改变方式:

1
2
3
4
5
6
7
8
9
10
import React,{Component} from 'react'
import hello from './index.module.css' //引入的时候给一个名称

export default class Hello extends Component{
render() {
return (
<h1 className={hello.title}>Hello</h1> //通过大括号进行调用
)
}
}

这就是一个简单的CSS Module的案例,设置完成后你可以自己通过开发者工具查看元素的class属性,你会发现class属性和你设置的并不完全一样,这是因为CSS Module通过算法确保了每一个模块中类名的唯一性。

总之,相较于标准的外部样式表来说,CSS Module就是多了一点——确保类名的唯一,通过内部算法避免了两个组件中出现重复的类名,如果你能保证不会出现重复的类名,其实直接使用外部样式表也是一样的。

7.配置代理

本案例需要下载axiosnpm install axios

React本身只关注与页面,并不包含发送ajax请求的代码,所以一般都是集成第三方的一些库,或者自己进行封装。

推荐使用axios。

在使用的过程中很有可能会出现跨域的问题,这样就应该配置代理。

所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port), 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域 。

那么react通过代理解决跨域问题呢

利用服务器之间访问不会有跨域,在中间开启一个服务器,端口号和项目端口号一样

7.1 方法一

在package.json中追加如下配置

1
"proxy":"请求的地址"      "proxy":"http://localhost:5000"  

说明:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀。
  2. 缺点:不能配置多个代理。
  3. 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)

7.2 方法二

方法二

  1. 第一步:创建代理配置文件

    1
    src下创建配置文件:src/setupProxy.js
  2. 编写setupProxy.js配置具体代理规则:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const proxy = require('http-proxy-middleware')

    module.exports = function(app) {
    app.use(
    proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
    target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
    changeOrigin: true, //控制服务器接收到的请求头中host字段的值
    /*
    changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
    changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
    changeOrigin默认值为false,但我们一般将changeOrigin值设为true
    */
    pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
    }),
    proxy('/api2', {
    target: 'http://localhost:5001',
    changeOrigin: true,
    pathRewrite: {'^/api2': ''}
    })
    )
    }

说明:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
  2. 缺点:配置繁琐,前端请求资源时必须加前缀。

7.3 vite配置proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
// 是否自动打开浏览器
open: true,
// 代理
proxy: {
'/api': {
target: 'http://127.0.0.1:5000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
})

11 【react-router 5】

1.准备

1.1 SPA

而为了减少这样的情况,我们还有另一种应用,叫做 SPA ,单页应用程序

它比传统的 Web 应用程序更快,因为它们在 Web 浏览器本身而不是在服务器上执行逻辑。在初始页面加载后,只有数据来回发送,而不是整个 HTML,这会降低带宽。它们可以独立请求标记和数据,并直接在浏览器中呈现页面

1.2 什么是路由?

路由是根据不同的 URL 地址展示不同的内容或页面

在 SPA 应用中,大部分页面结果不改变,只改变部分内容的使用

一个路由其实就是一个映射关系(k:v)

key为路径,value可能是function 或者是 component

后端路由:

value是function,用来处理客户端提交的请求

注册路由:router.get(path,function(req,res))

工作过程:当node接收一个请求的时候,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应的数据

前端路由:

浏览器端路由,value是Component,用于展示页面内容

注册路由:< Route path=”/test” component={Test}>

工作过程:当浏览器的path变为/test的时候,当前路由组件就会变成Test组件

前端路由的优缺点

优点

用户体验好,不需要每次都从服务器全部获取整个 HTML,快速展现给用户

缺点

  1. SPA 无法记住之前页面滚动的位置,再次回到页面时无法记住滚动的位置
  2. 使用浏览器的前进和后退键会重新请求,没有合理利用缓存‘

1.3 前端路由的原理

前端路由的主要依靠的时 history ,也就是浏览器的历史记录

history 是 BOM 对象下的一个属性,在 H5 中新增了一些操作 history 的 API

浏览器的历史记录就类似于一个栈的数据结构,前进就相当于入栈,后退就相当于出栈

并且历史记录上可以采用 listen 来监听请求路由的改变,从而判断是否改变路径

在 H5 中新增了 createBrowserHistory 的 API ,用于创建一个 history 栈,允许我们手动操作浏览器的历史记录

新增 API:pushStatereplaceState,原理类似于 Hash 实现。 用 H5 实现,单页路由的 URL 不会多出一个 # 号,这样会更加的美观

2.react-router-dom 的理解和使用

react的路由有三类:

web【主要适用于前端】,native【主要适用于本地】,anywhere【任何地方】

在这主要使用web也就是这个标题 react-router-dom

专门给 web 人员使用的库

  1. 一个 react 的仓库
  2. 很常用,基本是每个应用都会使用的这个库
  3. 专门来实现 SPA 应用

安装:npm i react-router-dom@5

首先我们要明确好页面的布局 ,分好导航区、展示区

要引入 react-router-dom 库,暴露一些属性 Link、BrowserRouter...

1
import { Link, BrowserRouter, Route } from 'react-router-dom'

导航区的 a 标签改为 Link 标签

1
<Link className="list-group-item" to="/about">About</Link>

同时我们需要用 Route 标签,来进行路径的匹配,从而实现不同路径的组件切换

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

这样之后我们还需要一步,加个路由器,在上面我们写了两组路由,同时还会报错指示我们需要添加 Router 来解决错误,这就是需要我们添加路由器来管理路由,如果我们在 Link 和 Route 中分别用路由器管理,那这样是实现不了的,只有在一个路由器的管理下才能进行页面的跳转工作。

因此我们也可以在 Link 和 Route 标签的外层标签采用 BrowserRouter(或者HashRouter) 包裹,但是这样当我们的路由过多时,我们要不停的更改标签包裹的位置,因此我们可以这么做

我们回到 App.jsx 目录下的 index.js 文件,将整个 App 组件标签采用 BrowserRouter 标签去包裹,这样整个 App 组件都在一个路由器的管理下

1
2
3
4
// index.js
<BrowserRouter>
< App />
</BrowserRouter>

image-20221025230322592

3.路由组件和一般组件

在我们前面的内容中,我们是把组件 Home 和组件 About 当成是一般组件来使用,我们将它们写在了 src 目录下的 components 文件夹下,但是我们又会发现它和普通的组件又有点不同,对于普通组件而言,我们在引入它们的时候我们是通过标签的形式来引用的。但是在上面我们可以看到,我们把它当作路由来引用时,我们是通过 {Home} 来引用的。

从这一点我们就可以认定一般组件和路由组件存在着差异

  1. 写法不同

一般组件<Demo/>路由组件<Route path="/demo" component={Demo}/>

  1. 存放的位置不同

同时为了规范我们的书写,一般将路由组件放在 pages/views 文件夹中,路由组件放在 components

而最重要的一点就是它们接收到的 props 不同,在一般组件中,如果我们不进行传递,就不会收到值。而对于路由组件而言,它会接收到 3 个固定属性 historylocation 以及 match

image-20221025230429965

重要的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined

match:
params: {}
path: "/about"
url: "/about"

4.1 基本使用

NavLink 标签是和 Link 标签作用相同的,但是它又比 Link 更加强大。

在前面的 demo 展示中,你可能会发现点击的按钮并没有出现高亮的效果,正常情况下我们给标签多添加一个 active 的类就可以实现高亮的效果

而 NavLink 标签正可以帮助我们实现这一步

当我们选中某个 NavLink 标签时,就会自动的在类上添加一个 active 属性

1
<NavLink className="list-group-item" to="/about">About</NavLink>

当然 NavLink 标签是默认的添加上 active 类,我们也可以改变它,在标签上添加一个属性 activeClassName

如下代码,就写了activeClassName,当点击的时候就会触发这个class的样式

1
2
3
4
5
{/*NavLink在点击的时候就会去找activeClassName="ss"所指定的class的值,如果不添加默认是active
这是因为Link相当于是把标签写死了,不能去改变什么。*/}

<NavLink activeClassName="ss" className="list-group-item" to="/about">About</NavLink>
<NavLink className="list-group-item" to="/home">Home</NavLink>

在上面的 NavLink 标签种,我们可以发现我们每次都需要重复的去写这些样式名称或者是 activeClassName ,这并不是一个很好的情况,代码过于冗余。那我们是不是可以想想办法封装一下它们呢?

我们可以采用 MyNavLink 组件,对 NavLink 进行封装

首先我们需要新建一个 MyNavLink 组件

return 一个结构

1
2
 // 通过{...对象}的形式解析对象,相当于将对象中的属性全部展开
<NavLink className="list-group-item" {...this.props} />

首先,有一点非常重要的是,我们在标签体内写的内容都会成为一个 children 属性,因此我们在调用 MyNavLink 时,在标签体中写的内容,都会成为 props 中的一部分,从而能够实现

接下来我们在调用时,直接写

1
2
{/*将NavLink进行封装,成为MyNavLink,通过props进行传参数,标签体内容props是特殊的一个属性,叫做children */}
<MyNavLink to="/home">home</MyNavLink>

5.解决二级路由样式丢失的问题

拿上面的案例来说:

这里面会有一个样式:

image-20221025231105964

此时,加载该样式的路径为:

image-20221025231114257

但是在写路由的时候,有的时候就会出现多级路由,

1
2
3
<MyNavLink to = "/cyk/about" >About</MyNavLink>

<Route path="/cyk/about"component={About}/>

这个时候就在刷新页面,就会出现问题:

样式因为路径问题加载失败,此时页面返回public下面的Index.html

image-20221025231213614

解决这个问题,有三个方法:

1.样式加载使用绝对位置

1
<link href="/css/bootstrap.css" rel="stylesheet"> 

2.使用 %PUBLIC_URL%

1
<link href="%PUBLIC_URL%/css/bootstrap.css" rel="stylesheet">

3.使用HashRouter

因为HashRouter会添加#,默认不会处理#后面的路径,所以也是可以解决的

6.模糊匹配和精准匹配

路由的匹配有两种形式,一种是精准匹配一种是模糊匹配,React 中默认开启的是模糊匹配

模糊匹配可以理解为,在匹配路由时,只要有匹配到的就好了

精准匹配就是,两者必须相同

比如:

1
<MyNavLink to = "/home/a/b" >Home</MyNavLink>

此时该标签匹配的路由,分为三个部分 home a b;将会根据这个先后顺序匹配路由。

如下就可以匹配到相应的路由:

1
<Route path="/home"component={Home}/>

但是如果是下面这个就会失败,也就是说他是根据路径一级一级查询的,可以包含前面那一部分,但并不是只包含部分就可以。

1
<Route path="/a" component={Home}/>

当然也可以使用这个精确的匹配 exact={true}

如以下:这样就精确的匹配/home,则上面的/home/a/b就不行了

1
2
3
<Route exact={true}  path="/home" component={Home}/>
或者
<Route exact path="/home" component={Home}/>

7.Switch 解决相同路径问题

首先我们看一段这样的代码

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

这是两个路由组件,在2,3行中,我们同时使用了相同的路径 /about

image-20221026132313014

我们发现它出现了两个 about 组件的内容,那这是为什么呢?

其实是因为,Route 的机制,当匹配上了第一个 /about 组件后,它还会继续向下匹配,因此会出现两个 About 组件,这时我们可以采用 Switch 组件进行包裹

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

在使用 Switch 时,我们需要先从 react-router-dom 中暴露出 Switch 组件

这样我们就能成功的解决掉这个问题了

8.路由重定向

在配置好路由,最开始打开页面的时候,应该是不会匹配到任意一个组件。这个时候页面就显得极其不合适,此时应该默认的匹配到一个组件。

这个时候我们就需要时候 Redirecrt 进行默认匹配了。

1
<Redirect to="/home" />

当我们加上这条语句时,页面找不到指定路径时,就会重定向到 /home 页面下因此当我们请求3000端口时,就会重定向到 /home 这样就能够实现我们想要的效果了

如下的代码就是默认匹配/home路径所到的组件

1
2
3
4
5
6
7
<Switch>
<Route path="/about"component={About}/>
{/* exact={true}:开启严格匹配的模式,路径必须一致 */}
<Route path="/home" component={Home}/>
{/* Redirect:如果上面的都没有匹配到,就匹配到这个路径下面 */}
<Redirect to = "/home"/>
</Switch>

9.嵌套路由

嵌套路由也就是我们前面有提及的二级路由,但是嵌套路由包括了二级、三级…还有很多级路由,当我们需要在一个路由组件中添加两个组件,一个是头部,一个是内容区

我们将我们的嵌套内容写在相应的组件里面,这个是在 Home 组件的 return 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div>
<h2>Home组件内容</h2>
<div>
<ul className="nav nav-tabs">
<li>
<MyNavLink className="list-group-item" to="/home/news">News</MyNavLink>
</li>
<li>
<MyNavLink className="list-group-item " to="/home/message">Message</MyNavLink>
</li>
</ul>
{/* 注册路由 */}
<Switch>
<Route path="/home/news" component={News} />
<Route path="/home/message" component={Message} />
</Switch>
</div>
</div>

在这里我们需要使用嵌套路由的方式,才能完成匹配

首先我们得 React 中路由得注册是有顺序的,我们在匹配得时候,因为 Home 组件是先注册得,因此在匹配的时候先去找 home 路由,由于是模糊匹配,会成功的匹配

在 Home 组件里面去匹配相应的路由,从而找到 /home/news 进行匹配,因此找到 News 组件,进行匹配渲染

如果开启精确匹配的话,第一步的 /home/news 匹配 /home 就会卡住不动,这个时候就不会显示有用的东西了!

10.传递参数

10.1 传递 params 参数

image-20221026132713300

首先我们需要实现的效果是,点击消息列表,展示出消息的详细内容

这个案例实现的方法有三种,第一种就是传递 params 参数,由于我们所显示的数据都是从数据集中取出来的,因此我们需要有数据的传输给 Detail 组件

我们首先需要将详细内容的数据列表,保存在 DetailData 中,将消息列表保存在 Message 的 state 中。

我们可以通过将数据拼接在路由地址末尾来实现数据的传递

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

如上,我们将消息列表的 id 和 title 写在了路由地址后面

这里我们需要注意的是:需要采用模板字符串以及 $ 符的方式来进行数据的获取

在注册路由时,我们可以通过 :参数名 来传递数据

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

如上,使用了 :id/:title 成功的接收了由 Link 传递过来的 id 和 title 数据

这样我们既成功的实现了路由的跳转,又将需要获取的数据传递给了 Detail 组件

我们在 Detail 组件中打印 this.props 来查看当前接收的数据情况

我们可以发现,我们传递的数据被接收到了对象的 match 属性下的 params 中

因此我们可以在 Detail 组件中获取到又 Message 组件中传递来的 params 数据

并通过 params 数据中的 id 值,在详细内容的数据集中查找出指定 id 的详细内容

1
2
3
4
const { id, title } = this.props.match.params
const findResult = DetailData.find((detailObj) => {
return detailObj.id === id
})

最后渲染数据即可

10.2 传递 search 参数

我们还可以采用传递 search 参数的方法来实现

首先我们先确定数据传输的方式

我们先在 Link 中采用 ? 符号的方式来表示后面的为可用数据

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

采用 search 传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到

image-20221026132937804

我们可以发现,我们的数据保存在了 location 对象下的 search 中,是一种字符串的形式保存的,我们可以引用一个库来进行转化 qs

qs是一个npm仓库所管理的包,可通过npm install qs命令进行安装.

  1. qs.parse()将URL解析成对象的形式
  2. qs.stringify()将对象 序列化成URL的形式,以&进行拼接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// nodejs中调试
const qs = require('qs');

1.qs.parse()
const str = "username='admin'&password='123456'";
console.log(qs.parse(str));
// Object { username: "admin", password: "123456" }

2.qs.stringify()
const a = qs.stringify({ username: 'admin', password: '123456' });
console.log(a);
// username=admin&password=123456



qs.stringify() 和JSON.stringify()有什么区别?

var a = {name:'hehe',age:10};
qs.stringify序列化结果如
name=hehe&age=10
--------------------
JSON.stringify序列化结果如下:
"{"a":"hehe","age":10}"

我们可以采用 parse 方法,将字符串转化为键值对形式的对象

1
2
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1)) // 从?后面开始截取字符串

这样我们就能成功的获取数据,并进行渲染

10.3 传递 state 参数

采用传递 state 参数的方法,是我觉得最完美的一种方法,因为它不会将数据携带到地址栏上,采用内部的状态来维护

1
<Link to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>

首先,我们需要在 Link 中注册跳转时,传递一个路由对象,包括一个 跳转地址名,一个 state 数据,这样我们就可以在 Detail 组件中获取到这个传递的 state 数据

注意:采用这种方式传递,无需声明接收

我们可以在 Detail 组件中的 location 对象下的 state 中取出我们所传递的数据

1
const { id, title } = this.props.location.state

image-20221026133411288

解决清除缓存造成报错的问题,我们可以在获取不到数据的时候用空对象来替代,例如,

1
const { id, title } = this.props.location.state || {}

当获取不到 state 时,则用空对象代替

这里的 state 和状态里的 state 有所不同

10.4 小结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.params参数
路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>
注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
接收参数:this.props.match.params
2.search参数
路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.search
备注:获取到的search是urlencoded编码字符串,需要借助querystring解析
3.state参数
路由链接(携带参数):<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
接收参数:this.props.location.state
备注:刷新也可以保留住参数

接收参数

1
2
3
4
5
6
7
8
9
// 接收params参数
// const {id,title} = this.props.match.params

// 接收search参数
// const {search} = this.props.location
// const {id,title} = qs.parse(search.slice(1))

// 接收state参数
const {id,title} = this.props.location.state || {}

11.路由跳转

11.1 push 与 replace 模式

默认情况下,开启的是 push 模式,也就是说,每次点击跳转,都会向栈中压入一个新的地址,在点击返回时,可以返回到上一个打开的地址,

当我们在读消息的时候,有时候我们可能会不喜欢这种繁琐的跳转,我们可以开启 replace 模式,这种模式与 push 模式不同,它会将当前地址替换成点击的地址,也就是替换了新的栈顶

我们只需要在需要开启的链接上加上 replace 即可

1
<Link replace to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>

image-20221026134437721

11.2 编程式路由导航

1
2
3
4
5
6
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
-this.prosp.history.push()
-this.prosp.history.replace()
-this.prosp.history.goBack()
-this.prosp.history.goForward()
-this.prosp.history.go(1)

我们可以采用绑定事件的方式实现路由的跳转,我们在按钮上绑定一个 onClick 事件,当事件触发时,我们执行一个回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//push跳转+携带params参数
// this.props.history.push(`/home/message/detail/${id}/${title}`)

//push跳转+携带search参数
// this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)

//push跳转+携带state参数
this.props.history.push(`/home/message/detail`,{id,title})

//replace跳转+携带params参数
//this.props.history.replace(`/home/message/detail/${id}/${title}`)

//replace跳转+携带search参数
// this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)

//replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`,{id,title})

11.3 withRouter

当我们需要在页面内部添加回退前进等按钮时,由于这些组件我们一般通过一般组件的方式去编写,因此我们会遇到一个问题,无法获得 history 对象,这正是因为我们采用的是一般组件造成的。

只有路由组件才能获取到 history 对象

因此我们需要如何解决这个问题呢

我们可以利用 react-router-dom 对象下的 withRouter 函数来对我们导出的 Header 组件进行包装,这样我们就能获得一个拥有 history 对象的一般组件

我们需要对哪个组件包装就在哪个组件下引入

1
2
3
4
// Header/index.jsx
import { withRouter } from 'react-router-dom'
// 在最后导出对象时,用 `withRouter` 函数对 index 进行包装
export default withRouter(index);

这样就能让一般组件获得路由组件所特有的 API

12.BrowserRouter 和 HashRouter 的区别

它们的底层实现原理不一样

对于 BrowserRouter 来说它使用的是 React 为它封装的 history API ,这里的 history 和浏览器中的 history 有所不同噢!通过操作这些 API 来实现路由的保存等操作,但是这些 API 是 H5 中提出的,因此不兼容 IE9 以下版本。

对于 HashRouter 而言,它实现的原理是通过 URL 的哈希值,但是这句话我不是很理解,用一个简单的解释就是

我们可以理解为是锚点跳转,因为锚点跳转会保存历史记录,从而让 HashRouter 有了相关的前进后退操作,HashRouter 不会将 # 符号后面的内容请求。兼容性更好!

地址栏的表现形式不一样

  • HashRouter 的路径中包含 # ,例如 localhost:3000/#/demo/test

刷新后路由 state 参数改变

  1. 在BrowserRouter 中,state 保存在history 对象中,刷新不会丢失
  2. HashRouter 则刷新会丢失 state

12 【react高级指引(上)】

1.setState 扩展

1.1 对象式 setState

首先在我们以前的认知中,setState 是用来更新状态的,我们一般给它传递一个对象,就像这样

1
2
3
this.setState({
count: count + 1
})

这样每次更新都会让 count 的值加 1。这也是我们最常做的东西

这里我们做一个案例,点我加 1,一个按钮一个值,我要在控制台输出每次的 count 的值

image-20221027095114944

那我们需要在控制台输出,要如何实现呢?

我们会考虑在 setState 更新之后 log 一下

1
2
3
4
5
6
7
add = () => {
const { count } = this.state
this.setState({
count: count + 1
})
console.log(this.state.count);
}

因此可能会写出这样的代码,看起来很合理,在调用完 setState 之后,输出 count

image-20221027095134650

我们发现显示的 count 和我们控制台输出的 count 值是不一样的

这是因为,我们调用的 setState 是同步事件,但是它的作用是让 React 去更新数据,而 React 不会立即的去更新数据,这是一个异步的任务,因此我们输出的 count 值会是状态更新之前的数据。“React 状态更新是异步的

那我们要如何实现同步呢?

其实在 setState 调用的第二个参数,我们可以接收一个函数,这个函数会在状态更新完毕并且界面更新之后调用,我们可以试试

setState(stateChange, [callback])——对象式的setState 1.stateChange为状态改变对象(该对象可以体现出状态的更改) 2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用

1
2
3
4
5
6
7
8
9
10
11
add = () => {
const { count } = this.state
this.setState(
{
count: count + 1,
},
() => {
document.title = `当前值是${this.state.count}`
},
)
}

我们将 setState 填上第二个参数,输出更新后的 count

image-20221027173513180

这样我们就能成功的获取到最新的数据了,如果有这个需求我们可以在第二个参数输出噢~

1.2 函数式 setState

,函数式的 setState 也是接收两个参数

第一个参数是 updater ,它是一个能够返回 stateChange 对象的函数

第二个参数是一个回调函数,用于在状态更新完毕,界面也更新之后调用

与对象式 setState 不同的是,我们传递的第一个参数 updater 可以接收到2个参数 stateprops

我们尝试一下

setState(updater, [callback])——函数式的setState 1.updater为返回stateChange对象的函数。 2.updater可以接收到state和props。 4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。

1
2
3
4
5
6
7
8
add = () => {
this.setState(
(state, props) => ({ count: state.count + 1 }),
() => {
document.title = `当前值是${this.state.count}`
},
)
}

image-20221027173515460

我们也成功的实现了

我们在第一个参数中传入了一个函数,这个函数可以接收到 state ,我们通过更新 state 中的 count 值,来驱动页面的更新

利用函数式 setState 的优势还是很不错的,可以直接获得 stateprops

可以理解为对象式的 setState 是函数式 setState 的语法糖

1.3 总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(1). setState(stateChange, [callback])------对象式的setState
1.stateChange为状态改变对象(该对象可以体现出状态的更改)
2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用

(2). setState(updater, [callback])------函数式的setState
1.updater为返回stateChange对象的函数。
2.updater可以接收到state和props。
4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。
总结:
1.对象式的setState是函数式的setState的简写方式(语法糖)
2.使用原则:
(1).如果新状态不依赖于原状态 ===> 使用对象方式
(2).如果新状态依赖于原状态 ===> 使用函数方式
(3).如果需要在setState()执行后获取最新的状态数据,
要在第二个callback函数中读取

2.Context

在React中组件间的数据通信是通过props进行的,父组件给子组件设置props,子组件给后代组件设置props,props在组件间自上向下(父传子)的逐层传递数据。但并不是所有的数据都适合这种传递方式,有些数据需要在多个组件中共同使用,如果还通过props一层一层传递,麻烦自不必多说。

Context为我们提供了一种在不同组件间共享数据的方式,它不再拘泥于props刻板的逐层传递,而是在外层组件中统一设置,设置后内层所有的组件都可以访问到Context中所存储的数据。换句话说,Context类似于JS中的全局作用域,可以将一些公共数据设置到一个同一个Context中,使得所有的组件都可以访问到这些数据。

2.1 何时使用 Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A extends React.Component {
render() {
return <B theme="dark" />;
}
}

function B(props) {
// B 组件接受一个额外的“theme”属性,然后传递给 C 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<C theme={props.theme} />
</div>
);
}

class C extends React.Component {
render() {
return h4>我从A组件接收到的主题模式:{this.props.theme}</h4>
}
}

使用 context, 我们可以避免通过中间元素传递 props。

2.2 类式组件

当我们想要给子类的子类传递数据时,前面我们讲过了 redux 的做法,这里介绍的 Context 我觉得也类似于 Redux

1
2
// React.createContext
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

首先我们需要引入一个 ThemeContext 组件,我们需要引用ThemeContext 下的 Provider

1
2
const ThemeContext  = React.createContext();
const { Provider } = ThemeContext ;

Provider译为生产者,和Consumer消费者对应。Provider会设置在外层组件中,通过value属性来指定Context的值。这个Context值在所有的Provider子组件中都可以访问。Context的搜索流程和JS中函数作用域类似,当我们获取Context时,React会在它的外层查找最近的Provider,然后返回它的Context值。如果没有找到Provider,则会返回Context模块中设置的默认值。

1
2
3
4
5
6
7
8
9
10
<Provider value={{ theme }}>
<B />
</Provider>
/*
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。
*/

但是我们需要在使用数据的组件中引入 ThemeContext

1
static contextType = ThemeContext ;

在使用时,直接从 this.context 上取值即可

1
const {theme} = this.context

完整版

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
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light')

export default class A extends Component {

state = {theme:'dark'}

render() {
const {theme} = this.state
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value={theme}>
<B/>
</ThemeContext.Provider>
)
}
}

// 中间的组件再也不必指明往下传递 theme 了。
class B extends Component {
render() {
return (
<>
<h3>我是B组件</h3>
<C/>
</>
)
}
}

class C extends Component {
//声明接收context
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext

render() {
const {theme} = this.context
return (
<>
<h3>我是C组件</h3>
<h4>我从A组件接收到的主题模式:{theme}</h4>
</>
)
}
}

挂载在 class 上的 contextType 属性可以赋值为由 React.createContext() 创建的 Context 对象。此属性可以让你使用 this.context 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。

2.3 函数组件

函数组件和类式组件只有一点点小差别

Context对象中有一个属性叫做Consumer,直译过来为消费者,如果你了解生产消费者模式这里就比较好理解了,如果没接触过,你可以将Consumer理解为数据的获取工具。你可以将它理解为一个特殊的组件,所以你需要这样使用它

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
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light')

export default class A extends Component {

state = {theme:'dark'}

render() {
const {theme} = this.state
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value={theme}>
<B/>
</ThemeContext.Provider>
)
}
}

// 中间的组件再也不必指明往下传递 theme 了。
class B extends Component {
render() {
return (
<>
<h3>我是B组件</h3>
<C/>
</>
)
}
}


function C(){
return (
<div>
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:
<ThemeContext.Consumer>
{ctx => {
return (<span>ctx</span>)
}}
</ThemeContext.Consumer>
</h4>
</div>
)
}

Consumer的标签体必须是一个函数,这个函数会在组件渲染时调用并且将Context中存储的数据作为参数传递进函数,该函数的返回值将会作为组件被最终渲染到页面中。这里我们将参数命名为了ctx,在回调函数中我们就可以通过ctx.xxx访问到Context中的数据。如果需要访问多个Context可以使用多个Consumer嵌套即可。

2.4 hook-useContext

通过Consumer使用Context实在是不够优雅,所以React还为我们提供了一个钩子函数useContext(),我们只需要将Context对象作为参数传递给钩子函数,它就会直接给我们返回Context对象中存储的数据。

因为我们平时的组件不会写的一个文件中,所以React.createContext要单独写在一个文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
store/theme-context.js
import React from "react";

const ThemeContext = React.createContext('light')

export default ThemeContext;
import React, {useContext} from 'react';
import ThemeContext from '../store/theme-context';

function C(){

const ctx = useContext(TestContext);

return (
<div>
<h3>我是C组件</h3>
<h4>我从A组件接收到的用户名:
<span>{ctx}</span>
</h4>
</div>
)
}

3.错误边界

3.1 基本使用

当不可控因素导致数据不正常时,我们不能直接将报错页面呈现在用户的面前,由于我们没有办法给每一个组件、每一个文件添加判断,来确保正常运行,这样很不现实,因此我们要用到错误边界技术

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误

错误边界就是让这块组件报错的影响降到最小,不要影响到其他组件或者全局的正常运行

例如 A 组件报错了,我们可以在 A 组件内添加一小段的提示,并把错误控制在 A 组件内,不影响其他组件

  • 我们要对容易出错的组件的父组件做手脚,而不是组件本身

我们在父组件中通过 getDerivedStateFromError 来配置子组件出错时的处理函数

# 编写生命周期函数 getDerivedStateFromError

  1. 静态函数
  2. 运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
  3. 注意:只有子组件发生错误,才会运行该函数
  4. 该函数返回一个对象,React会将该对象的属性覆盖掉当前组件的state(必须返回 null 或者状态对象(State Obect))
  5. 参数:错误对象
  6. 通常,该函数用于改变状态
1
2
3
4
5
6
7
8
state={
hasError:false,
}

static getDerivedStateFromError(error) {
console.log(error);
return { hasError: error }
}

我们可以将 hasError 配置到状态当中,当 hasError 状态改变成 error 时,表明有错误发生,我们需要在组件中通过判断 hasError 值,来指定是否显示子组件

1
{this.state.hasError ? <h2>Child出错啦</h2> : <Child />}

但是我们会发现这个效果过了几秒之后自动又出现报错页面了,那是因为开发环境还是会报错生产环境不会报错 直接显示 要显示的文字,白话一些就是这个适用于生产环境,为了生产环境不报错。 开发中我们可以将Child出错啦这种错误提示换成一个错误组件。

3.2 综合案例

按照React官方的约定,一个类组件定义了static getDerivedStateFromError()componentDidCatch() 这两个生命周期函数中的任意一个(或两个),即可被称作ErrorBoundary组件,实现错误边界的功能。

其中,getDerivedStateFromError方法被约定为渲染备用UI,componentDidCatch方法被约定为捕获打印错误信息。

编写生命周期函数 componentDidCatch

  1. 实例方法
  2. 运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
  3. 通常,该函数用于记录错误消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
Error: null,
ErrorInfo: null
};
}
//控制渲染降级UI
static getDerivedStateFromError(error,info) {
return {hasError: error};
}
//捕获抛出异常
componentDidCatch(error, errorInfo) {
// 1、错误信息(error)
// 2、错误堆栈(errorInfo)
//传递异常信息
this.setState((preState) =>
({hasError: preState.hasError, Error: error, ErrorInfo: errorInfo})
);
//可以将异常信息抛出给日志系统等等
//do something....
}
render() {
//如果捕获到异常,渲染降级UI
if (this.state.hasError) {
return <div>
<h1>{`Error:${this.state.Error?.message}`}</h1>
{this.state.ErrorInfo?.componentStack}
</div>;
}
return this.props.children;
}
}

虽然函数式组件无法定义 Error Boundary,但 Error Boundary 可以捕获函数式组件的异常错误

实现ErrorBoundary组件后,我们只需要将其当作常规组件使用,将其需要捕获的组件放入其中即可。

使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//main.js
import ReactDOM from 'react-dom/client';
import {ErrorBoundary} from './ErrorBoundary.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(
<ErrorBoundary>
<App/>
</ErrorBoundary>
);
//app.js
import React from 'react';
function App() {
const [count, setCount] = useState(0);
if (count>0){
throw new Error('count>0!');
}
return (
<div>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
);
}
export default App;

点击按钮后即可展示抛出异常时,应该渲染的降级UI:

image-20221027094444543

3.3 让子组件不影响父组件正常显示案例

假设B组件(子组件)的出错:users不是一个数组,却是一个字符串。此时,会触发调用getDerivedStateFromError,并返回状态数据{hasError:error}。A组件(父组件)将根据hasError值判断是渲染备用的错误页面还是B组件。

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

export default class A extends Component {
state = { hasError: '' }
static getDerivedStateFromError(error) {
return {
hasError: error,
}
}

componentDidCatch(error, info) {
console.log('error:', error)
console.log('info:', info)
console.log('用于统计错误信息并反馈给后台,将通知开发人员进行bug修复')
}

render() {
const { hasError } = this.state
return (
<div className="a">
<div>我是组件A</div>
{hasError ? '当前网络不稳定,请稍候再试!' : <B />}
</div>
)
}
}

class B extends Component {
state = {
users: '',
}
render() {
const { users } = this.state
return (
<div className="b">
<div>我是组件B</div>
{users.map(userObj => (
<li key={userObj.id}>
{userObj.name},{userObj.age}
</li>
))}
</div>
)
}
}

image-20221027190233518

3.4 使用错误边界需要注意什么

没有什么技术栈或者技术思维是银弹,错误边界看起来用得很爽,但是需要注意以下几点:

  • 错误边界实际上是用来捕获render阶段时抛出的异常,而React事件处理器中的错误并不会渲染过程中被触发,所以错误边界捕获不到事件处理器中的错误
  • React官方推荐使用try/catch来自行处理事件处理器中的异常。
  • 错误边界无法捕获异步代码中的错误(例如 setTimeoutrequestAnimationFrame回调函数),这两个函数中的代码通常不在当前任务队列内执行。
  • 目前错误边界只能在类组件中实现,也只能捕获其子组件树的错误信息。错误边界无法捕获自身的错误,如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,类似于JavaScript中的cantch的工作机制。
  • 错误边界无法在服务端渲染中生效,因为根本的渲染方法已经ReactDOM.createRoot().render()修改为了ReactDOM.hydrateRoot(), 而上面也提到了,错误边界捕获的是render阶段时抛出的异常。

总结:仅处理渲染子组件期间的同步错误

4.路由组件的lazyLoad

懒加载在 React 中用的最多的就是路由组件了,页面刷新时,所有的页面都会重新加载,这并不是我们想要的,我们想要实现点击哪个路由链接再加载即可,这样避免了不必要的加载

image-20221027095740307

我们可以发现,我们页面一加载时,所有的路由组件都会被加载

如果我们有 100 个路由组件,但是用户只点击了几个,这就会有很大的消耗,因此我们需要做懒加载处理,我们点击哪个时,才去加载哪一个

首先我们需要从 react 库中暴露一个 lazy 函数

React.lazy() 允许你定义一个动态加载的组件。这有助于缩减 bundle 的体积,并延迟加载在初次渲染时未用到的组件。

1
import React, { Component ,lazy} from 'react';

然后我们需要更改引入组件的方式

1
2
3
// 这个组件是动态加载的
const Home = lazy(() => import('./Home'))
const About = lazy(() => import('./About'))

采用 lazy 函数包裹

image-20221027095800440

我们会遇到这样的错误,提示我们用一个标签包裹

这里是因为,当我们网速慢的时候,路由组件就会有可能加载不出来,页面就会白屏,它需要我们来指定一个路由组件加载的东西,相对于 loading

React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。在未来,我们计划让 Suspense 处理更多的场景,如数据获取等。你可以在 我们的路线图 了解这一点。

1
2
3
4
5
6
<Suspense fallback={<h1>loading</h1>}>
<Routes>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
</Routes>
</Suspense>

初次登录页面的时候

image-20221027100147592

注意噢,这些文件都不是路由组件,当我们点击了对应组件之后才会加载

68747470733a2f2f6c6a63696d672e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f696d672f72656163742d657874656e73696f6e2d6c617a796c6f61642d332e676966

从上图我们可以看出,每次点击时,才会去请求 chunk 文件

那我们更改写的 fallback 有什么用呢?它会在页面还没有加载出来的时候显示

注意:因为 loading 是作为一个兜底的存在,因此 loading 是 必须提前引入的,不能懒加载

.Fragment

我们编写组件的时候每次都需要采用一个 div 标签包裹,才能让它正常的编译,但是这样会引发什么问题呢?我们打开控制台看看它的层级

image-20221027100328758image-20221027100328758

它包裹了几层无意义的 div 标签,我们可以采用 Fragment 来解决这个问题

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

首先,我们需要从 react 中暴露出 Fragment ,将我们所写的内容采用 Fragment 标签进行包裹,当它解析到 Fragment 标签的时候,就会把它去掉

这样我们的内容就直接挂在了 root 标签下

1
2
3
4
5
6
7
8
9
render() {
return (
<React.Fragment 可选 key={xxx.id}>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}

在React中为我们提供了一种更加便捷的方式,直接使用<></>代替Fragment更加简单:

同时采用空标签,也能实现,但是它不能接收任何值,而 Fragment 能够接收 1 个值key

1
2
3
4
5
6
7
8
9
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}

6.使用 PropTypes 进行类型检查

已在 02 【面向组件编程】3.props 进行说明

13【react高级指引(下)】

1.组件优化

1.1 shouldComponentUpdate 优化

在我们之前一直写的代码中,我们一直使用的Component 是有问题存在的

  1. 只要执行 setState ,即使不改变状态数据,组件也会调用 render
  2. 当前组件状态更新,也会引起子组件 render

而我们想要的是只有组件的 state 或者 props 数据发生改变的时候,再调用 render

我们可以采用重写 shouldComponentUpdate 的方法,但是这个方法不能根治这个问题,当状态很多时,我们没有办法增加判断

看个案例来了解下原理:

如果你的组件只有当 props.color 或者 state.count 的值改变才需要更新时,你可以使用 shouldComponentUpdate 来进行检查:

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
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}

shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}

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

在这段代码中,shouldComponentUpdate 仅检查了 props.colorstate.count 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 propsstate 中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent 就行了。

1.2 PureComponent 优化

这段代码可以改成以下这种更简洁的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}

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

大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。

PureComponent 会对比当前对象和下一个状态的 propstate ,而这个比较属于浅比较,比较基本数据类型是否相同,而对于引用数据类型,比较的是它的引用地址是否相同,这个比较与内容无关

1
2
3
4
5
6
7
8
9
10
state = {stus:['小张','小李','小王']}

addStu = ()=>{
/* const {stus} = this.state
stus.unshift('小刘')
this.setState({stus}) */

const {stus} = this.state
this.setState({stus:['小刘',...stus]})
}

注释掉的那部分,我们是用unshift方法为stus数组添加了一项,它本身的地址是不变的,这样的话会被当做没有产生变化(因为引用数据类型比较的是地址),所以我们平时都是采用合并数组的方式去更新数组。

1.3 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { PureComponent } from 'react'
import "./index.css";

export default class A extends PureComponent {
state = {
username:"张三"
}

handleClick = () => {
this.setState({})
}

render() {
console.log("A:enter render()")
const {username} = this.state;
const {handleClick} = this;

return (
<div className="a">
<div>我是组件A</div>
<span>我的username是{username}</span>&nbsp;&nbsp;
<button onClick={handleClick}>执行setState且不改变状态数据</button>
<B/>
</div>
)
}
}

class B extends PureComponent{
render(){
console.log("B:enter render()")
return (
<div className="b">
<div>我是组件B</div>
</div>
)
}
}

点击按钮后不会有任何变化,render函数也没有调用

image-20221027191454468

修改代码

1
2
3
4
5
handleClick = () => {
this.setState({
username: '李四',
})
}

点击按钮后只有A组件的render函数会调用

image-20221027192124322

修改代码

1
2
3
4
5
handleClick = () => {
const { state } = this
state.username = '李四'
this.setState(state)
}

image-20221027192253591

点击后不会有任何变化,render函数没有调用,这个时候其实是shouldComponentUpdate返回的false

2.Render Props

如何向组件内部动态传入带内容的结构(标签)?

1
2
3
4
5
Vue中: 
使用slot技术, 也就是通过组件标签体传入结构 <AA><BB/></AA>
React中:
使用children props: 通过组件标签体传入结构
使用render props: 通过组件标签属性传入结构, 一般用render函数属性

children props

1
2
3
4
5
6
7
8
9
10
render() {
return (
<A>
<B>xxxx</B>
</A>
)
}


问题: 如果B组件需要A组件内的数据, ==> 做不到

术语 “render prop”open in new window 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

采用 render props 技术,我们可以像组件内部动态传入带有内容的结构

当我们在一个组件标签中填写内容时,这个内容会被定义为 children props,我们可以通过 this.props.children 来获取

例如:

1
<A>hello</A>

这个 hello 我们就可以通过 children 来获取

而我们所说的 render props 就是在组件标签中传入一个 render 方法(名字可以自己定义,这个名字更语义化),又因为属于 props ,因而被叫做了 render props

1
2
3
<A render={(name) => <B name={name} />} />
A组件: {this.props.render(内部state数据)}
B组件: 读取A组件传入的数据显示 {this.props.data}

你可以把 render 看作是 props,只是它有特殊作用,当然它也可以用其他名字来命名

在上面的代码中,我们需要在 A 组件中预留出 B 组件渲染的位置 在需要的位置上加上{this.props.render(name)}

那我们在 B 组件中,如何接收 A 组件传递的 name 值呢?通过 this.props.name 的方式

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
export default class Parent extends Component {
render() {
return (
<div className="parent">
<h3>我是Parent组件</h3>
<A render={ name => (<B name={name}/>) }/>
</div>
)
}
}

class A extends Component {
state = {name:'tom'}
render() {
console.log(this.props);
const {name} = this.state
return (
<div className="a">
<h3>我是A组件</h3>
{this.props.render(name)}
</div>
)
}
}

class B extends Component {
render() {
console.log('B--render');
return (
<div className="b">
<h3>我是B组件,{this.props.name}</h3>
</div>
)
}
}

3.Portal

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

李立超老师的博客

这篇博客对于Portal的引出我觉得写的很好

portal – 李立超 | lilichao.com

3.1 问题的引出

在React中,父组件引入子组件后,子组件会直接在父组件内部渲染。换句话说,React元素中的子组件,在DOM中,也会是其父组件对应DOM的后代元素。

但是,在有些场景下如果将子组件直接渲染为父组件的后代,在网页显示时会出现一些问题。比如,需要在React中添加一个会盖住其他元素的Backdrop组件,Backdrop显示后,页面中所有的元素都会被遮盖。很显然这里需要用到定位,但是如果将遮罩层直接在当前组件中渲染的话,遮罩层会成为当前组件的后代元素。如果此时,当前元素后边的兄弟元素中有开启定位的情况出现,且层级不低于当前元素时,便会出现盖住遮罩层的情况。

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
const Backdrop = () => {

return <div
style={
{
position:'fixed',
top:0,
bottom:0,
left:0,
right:0,
background:'rgba(0,0,0,.3)',
zIndex:9999
}
}
>
</div>
};

const Box = props => {

return <div
style={
{
width:100,
height:100,
background:props.bgColor
}
}
>
{props.children}
</div>
};

const App = () => {

return (
<div>
<Box bgColor='yellowgreen'>
<Backdrop/>
</Box>
<Box bgColor='orange' />
</div>;
)
};

上例代码中,App组件中引入了两个Box组件,一个绿色,一个橙色。绿色组件中引入了Backdrop组件,Backdrop组件是一个遮罩层,可以在覆盖住整个网页。

现在三个组件的关系是,绿色Box是橙色Box的兄弟元素,Backdrop是绿色Box的子元素。如果Box组件没有开启定位,遮罩层可以正常显示覆盖整个页面。

image-20221029232123087

Backdrop能够盖住页面

但是如果为Box开启定位,并设置层级会出现什么情况呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Box = props => {

return <div
style={
{
width:100,
height:100,
background:props.bgColor,
position:'relative',
zIndex:1
}
}
>
{props.children}
</div>
};

现在修改Box组件,开启相对定位,并设置了z-index为1,结果页面变成了这个样子:

image-20221029232209420

和上图对比,显然橙色的box没有被盖住,这是为什么呢?首先我们来看看他们的结构:

1
2
3
4
5
6
<App>
<绿色Box>
<遮罩/>
</绿色Box>
<橙色Box/>
</App>

绿色Box和橙色Box都开启了定位,且z-index相同都为1,但是由于橙色在后边,所以实际层级是高于绿色的。由于绿色是遮罩层的父元素,所以即使遮罩的层级是9999也依然盖不住橙色。

问题出在了哪?遮罩层的作用,是用来盖住其他元素的,它本就不该作为Box的子元素出现,作为子元素了,就难免会出现类似问题。所以我们需要在Box中使用遮罩,但是又不能使他成为Box的子元素。怎么办呢?React为我们提供了一个“传送门”可以将元素传送到指定的位置上。

通过ReactDOM中的createPortal()方法,可以在渲染元素时将元素渲染到网页中的指定位置。这个方法就和他的名字一样,给React元素开启了一个传送门,让它可以去到它应该去的地方。

3.2 Portal的用法

  1. 在index.html中添加一个新的元素
  2. 在组件中中通过ReactDOM.createPortal()将元素渲染到新建的元素中

在index.html中添加新元素:

1
<div id="backdrop"></div>

修改Backdrop组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const backdropDOM = document.getElementById('backdrop');

const Backdrop = () => {
return ReactDOM.createPortal(
<div
style={
{
position:'fixed',
top:0,
bottom:0,
left:0,
right:0,
zIndex:9999,
background:'rgba(0,0,0,.3)'
}
}
>
</div>,
backdropDOM
);
};

如此一来,我们虽然是在Box中引入了Backdrop,但是由于在Backdrop中开启了“传送门”,Backdrop就会直接渲染到网页中id为backdrop的div中,这样一来上边的问题就解决了

3.3 通过 Portal 进行事件冒泡

尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。

这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。假设存在如下 HTML 结构:

1
2
3
4
5
6
<html>
<body>
<div id="app-root"></div>
<div id="modal-root"></div>
</body>
</html>

#app-root 里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #modal-root 冒泡上来的事件。

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 在 DOM 中有两个容器是兄弟级 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}

componentDidMount() {
// 在 Modal 的所有子元素被挂载后,
// 这个 portal 元素会被嵌入到 DOM 树中,
// 这意味着子元素将被挂载到一个分离的 DOM 节点中。
// 如果要求子组件在挂载时可以立刻接入 DOM 树,
// 例如衡量一个 DOM 节点,
// 或者在后代节点中使用 ‘autoFocus’,
// 则需添加 state 到 Modal 中,
// 仅当 Modal 被插入 DOM 树中才能渲染子元素。
modalRoot.appendChild(this.el);
}

componentWillUnmount() {
modalRoot.removeChild(this.el);
}

render() {
return ReactDOM.createPortal(
this.props.children,
this.el
);
}
}

class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// 当子元素里的按钮被点击时,
// 这个将会被触发更新父元素的 state,
// 即使这个按钮在 DOM 中不是直接关联的后代
this.setState(state => ({
clicks: state.clicks + 1
}));
}

render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}

function Child() {
// 这个按钮的点击事件会冒泡到父元素
// 因为这里没有定义 'onClick' 属性
return (
<div className="modal">
<button>Click</button>
</div>
);
}

const root = ReactDOM.createRoot(appRoot);
root.render(<Parent />);

image-20221029233009114

点击click后,可以发现数字从0变成1了

image-20221029233124852

子组件Child的点击事件能冒泡到父组件Parent ,触发父元素的点击事件

在 CodePen 上尝试

在父组件里捕获一个来自 portal 冒泡上来的事件,使之能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。例如,如果你在渲染一个 <Modal /> 组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。

14【react-Hook (上)】

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

1.准备

1.1 什么是 Hook

Hooks 译为钩子,Hooks 就是在函数组件内,负责钩进外部功能的函数。

React 提供了一些常用钩子,React 也支持自定义钩子,这些钩子都是用于为函数引入外部功能。

当我们在组件中,需要引入外部功能时,就可以使用 React 提供的钩子,或者自定义钩子。

1.2 动机

在组件之间复用状态逻辑很难 React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。 你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。 复杂组件变得难以理解 我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。 Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

难以理解的 class 你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。 class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。 为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。

1.3 Hook API

.4 什么时候会用 Hook

如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook。

注意:

在组件中有些特殊的规则,规定什么地方能使用 Hook,什么地方不能使用。我们将在 Hook 规则 中学习它们。

2.使用 State Hook

2.1 声明 State 变量

首先我们需要明确一点,函数式组件没有自己的 this

在 class 中,我们通过在构造函数中设置 this.state{ count: 0 } 来初始化 count state 为 0

1
2
3
4
5
6
7
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

在函数组件中,我们没有 this,所以我们不能分配或读取 this.state。我们直接在组件中调用 useState Hook:

1
2
3
4
5
6
7
import React, { useState } from 'react';

function Example() {
// 声明一个叫 “count” 的 state 变量
const [count, setCount] = useState(0);
console.log(count, setCount)
}

image-20221027201435122

调用 useState 方法的时候做了什么? 它定义一个 “state 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 banana。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

useState 需要哪些参数? useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)

useState 方法的返回值是什么? 返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.countthis.setState 类似,唯一区别就是你需要成对的获取它们。如果你不熟悉我们使用的语法,我们会在本章节的底部

介绍它。

简单说

它让函数式组件能够维护自己的 state ,它接收一个参数,作为初始化 state 的值,赋值给 count,因此 useState 的初始值只有第一次有效,它所映射出的两个变量 countsetCount 我们可以理解为 setState 来使用

useState 能够返回一个数组,第一个元素是 state ,第二个是更新 state 的函数

既然我们知道了 useState 的作用,我们的示例应该更容易理解了:

我们声明了一个叫 count 的 state 变量,然后把它设为 0。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount 来更新当前的 count

注意

你可能想知道:为什么叫 useState 而不叫 createState?

“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字总是use 开头的一个原因。我们将在后面的 Hook 规则 中了解原因。

2.2 读取 State

当我们想在 class 中显示当前的 count,我们读取 this.state.count

1
<p>You clicked {this.state.count} times</p>

在函数中,我们可以直接用 count:

1
<p>You clicked {count} times</p>

2.3 更新 State

在 class 中,我们需要调用 this.setState() 来更新 count 值:

1
2
3
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>

在函数中,我们已经有了 setCountcount 变量,所以我们不需要 this:

1
2
3
<button onClick={() => setCount(count + 1)}>
Click me
</button>

2.4 使用多个 state 变量

将 state 变量声明为一对 [something, setSomething] 也很方便,因为如果我们想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:

1
2
3
4
5
function ExampleWithManyStates() {
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

在以上组件中,我们有局部变量 agefruittodos,并且我们可以单独更新它们:

1
2
3
4
function handleOrangeClick() {
// 和 this.setState({ fruit: 'orange' }) 类似
setFruit('orange');
}

不必使用多个 state 变量。State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。然而,不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它。

2.5 总结

现在让我们来仔细回顾一下学到的知识,看下我们是否真正理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 1:  import React, { useState } from 'react';
2:
3: function Example() {
4: const [count, setCount] = useState(0);
5:
6: return (
7: <div>
8: <p>You clicked {count} times</p>
9: <button onClick={() => setCount(count + 1)}>
10: Click me
11: </button>
12: </div>
13: );
14: }
  • 第一行: 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。
  • 第四行:Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount
  • 第九行: 当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。

乍一看这似乎有点太多了。不要急于求成!如果你有不理解的地方,请再次查看以上代码并从头到尾阅读。我们保证一旦你试着”忘记” class 里面 state 是如何工作的,并用新的眼光看这段代码,就容易理解了。

3.使用 Effect Hook

3.1 副作用

React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。

例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。

3.2 React.StrictMode

编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了React.StrictMode标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的。

React并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:

类组件的的 constructor, render, 和 shouldComponentUpdate 方法 类组件的静态方法 getDerivedStateFromProps 函数组件的函数体 参数为函数的setState 参数为函数的useState, useMemo, or useReducer

重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。

如果你无法通过浏览器正常安装React Developer Tools 可以通过点击这里下载。

3.3 Effect 基本使用

在类式组件中,提供了一些声明周期钩子给我们使用,我们可以在组件的特殊时期执行特定的事情,例如 componentDidMount ,能够在组件挂载完成后执行一些东西

在函数式组件中也可以实现,它采用的是 Effect Hook ,它的语法更加的简单,同时融合了 componentDidUpdata 生命周期,极大的方便了我们的开发

Effect Hook 可以让你在函数组件中执行副作用操作,专门用来处理那些不能直接写在组件内部的代码。

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

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

// 类似于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用这个bom api更新网页标题
document.title = `You clicked ${count} times`;
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。

useEffect()中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染。

通过使用这个Hook,我设置了React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。

提示

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。我们来更仔细地看一下他们之间的区别。

3.2 无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。

3.2.1 使用 class 的示例

在 React 的 class 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。

这就是为什么在 React class 中,我们把副作用操作放到 componentDidMountcomponentDidUpdate 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性

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
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}

componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}

render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}

注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。

这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。

现在让我们来看看如何使用 useEffect 执行相同的操作。

3.2.2 使用 Hook 的示例

我们在本章节开始时已经看到了这个示例,但让我们再仔细观察它:

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

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

useEffect(() => {
document.title = `You clicked ${count} times`;
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffectuseEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后每次更新之后都会执行。(我们稍后会谈到如何控制它 )你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

3.2.3 详细说明

现在我们已经对 effect 有了大致了解,下面这些代码应该不难看懂了:

1
2
3
4
5
6
7
function Example() {
const [count, setCount] = useState(0);

useEffect(() => {
document.title = `You clicked ${count} times`;
});
}

我们声明了 count state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect Hook。此函数就是我们的 effect。然后使用 document.title 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count 值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。

经验丰富的 JavaScript 开发人员可能会注意到,传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。我们将在本章节后续部分open in new window 更清楚地了解这样做的意义。

提示

componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffectHook 供你使用,其 API 与 useEffect 相同。

3.3 需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

3.3.1 使用 Class 的示例

在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 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
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
}

componentDidMount() {
ChatAPI.subscribe(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribe(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange = (status) => {
this.setState({
isOnline: status.isOnline
});
}

render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}

你会注意到 componentDidMountcomponentWillUnmount 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

注意

眼尖的读者可能已经注意到了,这个示例还需要编写 componentDidUpdate 方法才能保证完全正确。我们先暂时忽略这一点,本章节中后续部分 会介绍它。

3.3.2 使用 Hook 的示例

如何使用 Hook 编写这个组件。

你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在下一次effect执行前调用它,我们可以在这个函数中清除掉前一次effect执行所带来的影响。

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

function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);

useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribe(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
};
});

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。

注意

并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。

3.4 通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解决:

1
2
3
4
5
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}

这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

1
2
3
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

对于有清除操作的 effect 同样适用:

1
2
3
4
5
6
7
8
9
10
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribe(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

未来版本,可能会在构建时自动添加第二个参数。

注意:

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。参阅文档,了解更多关于如何处理函数 以及数组频繁变化时的措施 内容。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMountcomponentWillUnmount 思维模式,但我们有更好的 方式 来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。

我们推荐启用 eslint-plugin-react-hooks中的 exhaustive-deps规则。此规则会在添加错误依赖时发出警告并给出修复建议。

4.useRef

1
const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

一个常见的用例便是命令式地访问子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

我们要获取元素的真实DOM对象,首先我们需要使用useRef()这个钩子函数获取一个对象,这个对象就是一个容器,React会自动将DOM对象传递到容器中。代码const divRef = useRef()就是通过钩子函数在创建这个对象,并将其存储到变量中。

创建对象后,还需要在被获取引用的元素上添加一个ref属性,该属性的值就是刚刚我们所声明的变量,像是这样ref={divRef}这句话的意思就是将对象的引用赋值给变量divRef。这两个步骤缺一不可,都处理完了,就可以通过divRef来访问原生DOM对象了。

useRef()返回的是一个普通的JS对象,JS对象中有一个current属性,它指向的便是原生的DOM对象。上例中,如果想访问div的原生DOM对象,只需通过divRef.current即可访问,它可以调用DOM对象的各种方法和属性,但还是要再次强调:慎用!

尽量减少在React中操作原生的DOM对象,如果实在非得操作也尽量是那些不会对数据产生影响的操作,像是设置焦点、读取信息等。

useRef()所返回的对象就是一个普通的JS对象,所以上例中即使我们不使用钩子函数,仅仅创建一个形如{current:null}的对象也是可以的。只是我们自己创建的对象组件每次渲染时都会重新创建一个新的对象,而通过useRef()创建的对象可以确保组件每次的重渲染获取到的都是相同的对象。

5.useReducer

5.1 基本使用

为了解决复杂State带来的不便,React为我们提供了一个新的使用State的方式。Reducer横空出世,reduce单词中文意味减少,而reducer我觉得可以翻译为“当你的state的过于复杂时,你就可以使用的可以对state进行整合的工具”。当然这是个玩笑话,个人认为Reducer可以翻译为“整合器”,它的作用就是将那些和同一个state相关的所有函数都整合到一起,方便在组件中进行调用。

当然工具都有其使用场景,Reducer也不例外,它只适用于那些比较复杂的state,对于简单的state使用Reducer只能是徒增烦恼。

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

它的返回值和useState()类似,第一个参数是state用来读取state的值,第二个参数同样是一个函数,不同于setState()这个函数我们可以称它是一个“派发器”,通过它可以向reducer()发送不同的指令,控制reducer()做不同的操作。

它的参数有三个,第三个我们暂且忽略,只看前两个。reducer()是一个函数,也是我们所谓的“整合器”。它的返回值会成为新的state值。当我们调用dispatch()时,dispatch()会将消息发送给reducer()reducer()可以根据不同的消息对state进行不同的处理。initialArg就是state的初始值,和useState()参数一样。

以下是用 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
38
39
40
41
42
/*
* 参数:
* reducer : 整合函数
* 对于我们当前state的所有操作都应该在该函数中定义
* 该函数的返回值,会成为state的新值
* reducer在执行时,会收到两个参数:
* state 当前最新的state
* action 它需要一个对象
* 在对象中会存储dispatch所发送的指令
* initialArg : state的初始值,作用和useState()中的值是一样
* 返回值:
* 数组:
* 第一个参数,state 用来获取state的值
* 第二个参数,state 修改的派发器
* 通过派发器可以发送操作state的命令
* 具体的修改行为将会由另外一个函数(reducer)执行
* */

// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
function countReducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}

function Counter() {
const [count, countDispatch] = useReducer(countReducer, {count: 0});
// 这里本来初始值是直接给0的,但是为了countReducer函数中的state写成对象形式

return (
<>
Count: {count.count}
<button onClick={() => countDispatch({type: 'decrement'})}>-</button>
<button onClick={() => countDispatch({type: 'increment'})}>+</button>
</>
);
}

5.2 state初始化的两种方式

指定初始 state

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer 是最简单的方法:

1
2
3
const [state, dispatch] = useReducer(
reducer,
{count: 0} );

惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 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
export default function App() {
return (
<div>
<Counter initialCount={0} />
</div>
)
}

function countInit(initialCount) {
return {count: initialCount};
}

function countReducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return countInit(action.payload);
default:
throw new Error();
}
}

function Counter({initialCount}) {
const [count, countDispatch] = useReducer(countReducer, initialCount, countInit);
return (
<>
Count: {count.count}
<button
onClick={() => countDispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => countDispatch({type: 'decrement'})}>-</button>
<button onClick={() => countDispatch({type: 'increment'})}>+</button>
</>
);
}

image-20221030143937094

5.3 跳过 dispatch

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法来比较 state。)

这里的state如果是个对象,还是会渲染子组件,因为我们返回的是一个新对象,我想应该比较的是地址,如果直接将state返回,子组件是不会重新渲染的

需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

15【react-Hook (下)】

1.React.memo

1.1 基本介绍

这是一个高阶组件,用来做性能优化的,这个本来应该是写在React高级指引中的,但是这个案例会和后面的useCallback联合起来,所以就写在这里了

  • React.memo() 是一个高阶组件,如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在React.memo中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。
    • 它接收另一个组件作为参数,并且会返回一个包装过的新组件
    • 包装过的新组件就会具有缓存功能,这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
    • 包装过后,只有组件的props发生变化,才会触发组件的重新的渲染,否则总是返回缓存中结果。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReduceruseContext的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

1.2 问题的引出

React组件会在两种情况下发生重新渲染。第一种,当组件自身的state发生变化时。第二种,当组件的父组件重新渲染时。第一种情况下的重新渲染无可厚非,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
App.jsx
import React, { useState } from 'react'

export default function App() {
console.log('App渲染')

const [count, setCount] = useState(1)

const clickHandler = () => {
setCount(prevState => prevState + 1)
}

return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A />
</div>
)
}

function A() {
console.log('A渲染')
return <div>我是A组件</div>
}

在点击增加后,我们发现AppA都重新渲染了。

当APP组件重新渲染时,A组件也会重新渲染。A组件中没有state,甚至连props都没有设置。换言之,A组件无论如何渲染,每次渲染的结果都是相同的,虽然重渲染并不会应用到真实DOM上,但很显然这种渲染是完全没有必要的。

image-20221030172720453

为了减少像A组件这样组件的渲染,React为我们提供了一个方法React.memo()。该方法是一个高阶函数,可以用来根据组件的props对组件进行缓存,当一个组件的父组件发生重新渲染,而子组件的props没有发生变化时,它会直接将缓存中的组件渲染结果返回而不是再次触发子组件的重新渲染,这样一来就大大的降低了子组件重新渲染的次数。

1.3 使用React.memo

使用React.memo包裹A组件

这里只是为了演示方便,把所有组件写一个文件,就用这种方式包裹A组件,平时单文件组件的时候我们这样使用,export default React.memo(A)

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

export default function App() {
console.log('App渲染')

const [count, setCount] = useState(1)

const clickHandler = () => {
setCount(prevState => prevState + 1)
}

return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A />
</div>
)
}

const A = React.memo(() => {
console.log('A渲染')
return <div>我是A组件</div>
})

修改后的代码中,并没有直接使用A组件,而是在A组件外层套了一层函数React.memo(),这样一来,返回的A组件就增加了缓存功能,只有当A组件的props属性发生变化时,才会触发组件的重新渲染。memo只会根据props判断是否需要重新渲染,和state和context无关,state或context发生变化时,组件依然会正常的进行重新渲染

在点击增加后,我们发现只有App重新渲染了。

image-20221030173239606

这时我们改下代码

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
export default function App() {
console.log('App渲染')

const [count, setCount] = useState(1)

const clickHandler = () => {
setCount(prevState => prevState + 1)
}

// 增加
const test = count % 4 === 0

return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
{/* 改动 */}
<A test={test} />
</div>
)
}

const A = React.memo(props => {
console.log('A渲染')
return (
<div>
我是A组件
{/* 增加 */}
<p>{props.test && 'props.test 为 true'}</p>
</div>
)
})

这次加了个表达式的结果传给A组件,一开始是false,只有为true的时候,A组件才会重新渲染

这时界面是这样的

image-20221030174105525

点击3次后,表达式为true,A组件的props发生改变,所以重新渲染了。

image-20221030173754653

1.4 使用注意

  1. 此方法仅作为**性能优化 **的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。
  2. 与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

1.5 容易出错的情况

先回到这个案例的初始代码,在这之上进行修改

我们把App组件clickHandler方法传递给A组件,让A组件也能够改变App组件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
import React, { useState } from 'react'

export default function App() {
console.log('App渲染')

const [count, setCount] = useState(1)

const clickHandler = () => {
setCount(prevState => prevState + 1)
}

return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A clickHandler={clickHandler} />
</div>
)
}

const A = React.memo(props => {
console.log('A渲染')
return (
<div>
我是A组件
<button onClick={props.clickHandler}>A组件的增加</button>
</div>
)
})

点击A组件的增加,发现A组件也重新渲染了

image-20221030175830062

这是因为App组件重新渲染的时候,clickHandler也重新创建了,这时传递给子组件的clickHandler和上一次不一样,所以react.memo失效了。

这个问题可以用useCallback解决。

2.useCallback

2.1 基本介绍

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallbackuseMemo设计的初衷是用来做性能优化的。在Class Component中考虑以下的场景:

1
2
3
4
5
6
7
8
class Foo extends Component {
handleClick() {
console.log('Click happened');
}
render() {
return <Button onClick={() => this.handleClick()}>Click Me</Button>;
}
}

传给 Button 的 onClick 方法每次都是重新创建的,这会导致每次 Foo render 的时候,Button 也跟着 render。优化方法有 2 种,箭头函数和 bind。下面以 bind 为例子:

1
2
3
4
5
6
7
8
9
10
11
12
class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Click happened');
}
render() {
return <Button onClick={this.handleClick}>Click Me</Button>;
}
}

同样的,Function Component也有这个问题:

1
2
3
4
5
6
7
8
function Foo() {
const [count, setCount] = useState(0);

const handleClick() {
console.log(`Click happened with dependency: ${count}`)
}
return <Button onClick={handleClick}>Click Me</Button>;
}

而 React 给出的方案是useCallback Hook。在依赖不变的情况下 (在我们的例子中是 count ),它会返回相同的引用,避免子组件进行无意义的重复渲染

2.2 解决1.5遗留的问题

1
2
3
4
5
6
7
8
9
10
11
/*
* useCallback()
* 这个hook会缓存方法的引用
* 参数:
* 1. 回调函数
* 2. 依赖数组
* - 当依赖数组中的变量发生变化时,回调函数才会重新创建
* - 如果不指定依赖数组,回调函数每次都会重新创建
* - 一定要将回调函数中使用到的所有变量都设置到依赖数组中
* 除了(setState)
* */

我们将clickHandler方法改造一下

1
2
3
const clickHandler = useCallback(() => {
setCount(prevState => prevState + 1)
}, [])

第二个参数一定要加,不然和平常写没有区别

依赖项[]的意思是只有第一次渲染时才会创建,之后都不会重新创建了

点击A组件的增加,发现只有App组件重新渲染了。因为clickHandler没有重新创建,传给子组件的没有变化,所以子组件这次没有重新渲染。

image-20221030180349406

完整代码

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, useCallback } from 'react'

export default function App() {
console.log('App渲染')

const [count, setCount] = useState(1)

const clickHandler = useCallback(() => {
setCount(prevState => prevState + 1)
}, [])

return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A clickHandler={clickHandler} />
</div>
)
}

const A = React.memo(props => {
console.log('A渲染')
return (
<div>
我是A组件
<button onClick={props.clickHandler}>A组件的增加</button>
</div>
)
})

2.3 第二个参数的使用

继续改造上面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { useState, useCallback } from 'react'

export default function App() {
console.log('App渲染')

const [count, setCount] = useState(1)
// 增加
const [num, setNum] = useState(1)

const clickHandler = useCallback(() => {
setCount(prevState => prevState + num)
// 增加
setNum(prevState => prevState + 1)
}, [])

return (
<div>
<h2>App -- {count}</h2>
<button onClick={clickHandler}>增加</button>
<A clickHandler={clickHandler} />
</div>
)
}

const A = React.memo(props => {
console.log('A渲染')
return (
<div>
我是A组件
<button onClick={props.clickHandler}>A组件的增加</button>
</div>
)
})

增加了一个num,让每一次count的增加比上次多1,现在这样写是有问题的。

image-20221030181249832

点击了两次增加后,预期值应该是4,但是显示的是3,是为什么呢?

因为clickHandler只在初次渲染的时候创建,当时num的值是1,这个函数一直没有重新创建,内部用的num一直是1

这时我们可以加一个依赖项

1
2
3
4
const clickHandler = useCallback(() => {
setCount(prevState => prevState + num)
setNum(prevState => prevState + 1)
}, [num])

这样num变化了,这个函数也会重新创建。

image-20221030181534667

点击了两次增加后,count变成了预期值4。

3.useMemo

useMemo和useCallback十分相似,useCallback用来缓存函数对象,useMemo用来缓存函数的执行结果。在组件中,会有一些函数具有十分的复杂的逻辑,执行速度比较慢。闭了避免这些执行速度慢的函数返回执行,可以通过useMemo来缓存它们的执行结果,像是这样:

1
2
3
const result = useMemo(()=>{
return 复杂逻辑函数();
},[依赖项])

useMemo中的函数会在依赖项发生变化时执行,注意!是执行,这点和useCallback不同,useCallback是创建。执行后返回执行结果,如果依赖项不发生变化,则一直会返回上次的结果,不会再执行函数。这样一来就避免复杂逻辑的重复执行。

3.1 问题的引出

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
App.jsx
import React, { useMemo, useState } from 'react'

const App = () => {
const [count, setCount] = useState(1)

let a = 123
let b = 456

function sum(a, b) {
console.log('sum执行了')
return a + b
}

return (
<div>
<h1>App</h1>
<p>sum的结果:{sum(a, b)}</p>
<h3>{count}</h3>
<button onClick={() => setCount(prevState => prevState + 1)}>点我</button>
</div>
)
}

export default App

这是一个计数器案例,但是多添加了一个函数展示结果,这种情况这个函数只需要在一开始调用一次就够了,但是count的改变会导致重新渲染模板,这样sum函数也会反复执行。

image-20221107143139632

现在这个sum函数太简单了,体现不出性能上的问题,我们可以把sum中的逻辑改复杂一点。

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, { useMemo, useState } from 'react'

const App = () => {
const [count, setCount] = useState(1)

let a = 123
let b = 456

function sum(a, b) {
console.log('sum执行了')
const begin = +new Date()
while (true) {
if (Date.now() - begin > 3000) break
}
return a + b
}
return (
<div>
<h1>App</h1>
<p>sum的结果:{sum(a, b)}</p>
<h3>{count}</h3>
<button onClick={() => setCount(prevState => prevState + 1)}>点我</button>
</div>
)
}

export default App

增加了一个功能,让这个函数起码3秒才能执行完。

image-20221107143451560

这个时候因为sum函数要3秒才能执行完,导致下面数字显示也变慢了3秒。

3.2 使用 useMemo 解决上面的问题

1
App.jsx

改写模板中的sum方法的调用

1
<p>sum的结果:{useMemo(() => sum(a, b), [])}</p>

image-20221107143946116

第一次加载慢是不可避免的,但是这个钩子函数将sum函数的返回值缓存起来,这样我们模板重新渲染时就没有再去执行sum函数,而是直接使用上一次的返回值。

3.3 第二个参数的使用

继续改造上面的代码,把Sum单独抽离成一个组件

1
2
3
4
5
6
7
8
Sum.jsx
import React from 'react'

export default function Sum(props) {
console.log('Sum执行了')
return <span>{props.a + props.b}</span>
}
App.jsx

添加了一个功能可以变换a的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useMemo, useState } from 'react'
import Sum from './Sum'

const App = () => {
const [count, setCount] = useState(1)

let a = 123
let b = 456

if (count % 2 === 0) a = a + 1

const result = useMemo(() => <Sum a={a} b={b} />, [])

return (
<div>
<h1>App</h1>
<p>sum的结果:{result}</p>
<h3>{count}</h3>
<button onClick={() => setCount(prevState => prevState + 1)}>点我</button>
</div>
)
}

export default App

现在有一个问题,如果Sum组件接收的值变化了,网页上显示的还是原来的缓存值,这个时候就要利用第二个参数。

image-20221107145159066

1
2
App.jsx
const result = useMemo(() => <Sum a={a} b={b} />, [a])

这里的意思和以前是一样的,如果a的值变化了,将会重新计算。

image-20221107145403725

4.React.forwardRef

这是一个高阶组件,用来做性能优化的,这个本来应该是写在React高级指引中的,但是这个案例会和后面的useImperativeHandle联合起来,所以就写在这里了

React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

React.forwardRef 接受渲染函数作为参数。React 将使用 propsref 作为参数来调用此函数。此函数应返回 React 节点。

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

const Child = React.forwardRef((props, ref) => {
return (
<>
<h2>这是Child组件</h2>
<input type="text" ref={ref} />
</>
)
})

export default function App() {
const childRef = useRef(null)
console.log(childRef)
return (
<div>
<h2>这是App组件</h2>
<Child ref={childRef} />
</div>
)
}

在上述的示例中,React 会将 <Child ref={childRef}> 元素的 ref 作为第二个参数传递给 React.forwardRef 函数中的渲染函数。该渲染函数会将 ref 传递给 <input ref={ref}> 元素。

因此,当 React 附加了 ref 属性之后,ref.current 将直接指向 <input> DOM 元素实例。

image-20221107150428406

我们改造App组件

1
2
3
4
5
6
7
8
9
10
11
12
export default function App() {
const childRef = useRef(null)

childRef.current.value = 'App组件设置的'

return (
<div>
<h2>这是App组件</h2>
<Child ref={childRef} />
</div>
)
}

image-20221107150535398

我们可以直接在App组件操作Child组件的内容,但是这样并不好,我们希望Child组件的内容只由Child组件自己去操作,所以引出了useImperativeHandle

5.useImperativeHandle

1
useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef一起使用:

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
App.jsx
import React, { useRef, useEffect, useImperativeHandle } from 'react'

const Child = React.forwardRef((props, ref) => {
const inputRef = useRef(null)

const changeInputValue = value => (inputRef.current.value = value)

// useImperativeHandle 可以用来指定ref返回的值
useImperativeHandle(ref, () => ({
changeInputValue,
}))

return (
<>
<h2>这是Child组件</h2>
<input type="text" ref={inputRef} />
</>
)
})

export default function App() {
const childRef = useRef(null)

useEffect(() => {
console.log(childRef)
}, [])

return (
<div>
<h2>这是App组件</h2>
<button onClick={() => childRef.current.changeInputValue('App组件修改的')}>点击改变</button>
<Child ref={childRef} />
</div>
)
}

我们来看看childRef的输出是什么

image-20221107151926890

可以发现我们把子组件的changeInputValue暴露出去了。

image-20221107152122394

点击按钮发现也是可以正常使用的。

16 【react-router 6】

关于路由的知识已在11 【react-router 5】中进行说明,这里主要是对于5版本的api的变换说明

1.概述

官方文档:Home v6.4.1 | React Router React Router 以三个不同的包发布到 npm 上,它们分别为:

    1. react-router: 路由的核心库,提供了很多的:组件、钩子。
    2. *react-router-dom:* 包含react-router所有内容,并添加一些专门用于 DOM 的组件,例如 <BrowserRouter>
    3. react-router-native: 包括react-router所有内容,并添加一些专门用于ReactNative的API,例如:<NativeRouter>等。
  1. 与React Router 5.x 版本相比,改变了什么?

    1. 内置组件的变化:移除<Switch/> ,新增 <Routes/>等。

    2. 语法的变化:component={About} 变为 element={<About/>}等。

    3. 新增多个hook:useParamsuseNavigateuseMatch等。

    4. 官方明确推荐函数式组件了!!!

      ……

安装

1
npm install react-router-dom@6

2.BrowserRouter和HashRouter

在 React Router 中,最外层的 API 通常就是用 BrowserRouter。BrowserRouter 的内部实现是用了 history 这个库和 React Context 来实现的,所以当你的用户前进后退时,history 这个库会记住用户的历史记录,这样需要跳转时可以直接操作。

BrowserRouter 使用时,通常用来包住其它需要路由的组件,所以通常会需要在你的应用的最外层用它,比如如下

1
2
3
4
5
6
7
8
9
10
11
import ReactDOM from 'react-dom'
import * as React from 'react'
import { BrowserRouter } from 'react-router-dom'
import App from './App`

ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>
, document.getElementById('app))
<HashRouter>
  1. 说明:作用与<BrowserRouter>一样,但<HashRouter>修改的是地址栏的hash值。
  2. 备注:6.x版本中<HashRouter><BrowserRouter> 的用法与 5.x 相同。

3.Routes 与 Route

  1. v6版本中移出了先前的<Switch>,引入了新的替代者:<Routes>
  2. <Routes><Route>要配合使用,且必须要用<Routes>包裹<Route>
  3. <Route> 相当于一个 if 语句,如果其路径与当前 URL 匹配,则呈现其对应的组件。
  4. <Route caseSensitive> 属性用于指定:匹配时是否区分大小写(默认为 false)。
  5. 当URL发生变化时,<Routes> 都会查看其所有子 <Route> 元素以找到最佳匹配并呈现组件 。
  6. <Route> 也可以嵌套使用,且可配合useRoutes()配置 “路由表” ,但需要通过 <Outlet> 组件来渲染其子路由。

Route

Route 用来定义一个访问路径与 React 组件之间的关系。比如说,如果你希望用户访问 https://your_site.com/about 的时候加载 <About /> 这个 React 页面,那么你就需要用 Route:

1
<Route path="/about" element={<About />} />

Routes

Routes 是用来包住路由访问路径(Route)的。它决定用户在浏览器中输入的路径到对应加载什么 React 组件,因此绝大多数情况下,Routes 的唯一作用是用来包住一系列的 Route,比如如下

1
2
3
4
5
6
7
8
9
10
import { Routes, Route } from "react-router-dom";

function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
);
}

在这里,Routes 告诉了 React Router 每当用户访问根地址时,加载 Home 这个页面,而当用户访问 /about 时,就加载 <About /> 页面。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Routes>
/*path属性用于定义路径,element属性用于定义当前路径所对应的组件*/
<Route path="/login" element={<Login />}></Route>

/*用于定义嵌套路由,home是一级路由,对应的路径/home*/
<Route path="home" element={<Home />}>
/*test1 和 test2 是二级路由,对应的路径是/home/test1 或 /home/test2*/
<Route path="test1" element={<Test/>}></Route>
<Route path="test2" element={<Test2/>}></Route>
</Route>

//Route也可以不写element属性, 这时就是用于展示嵌套的路由 .所对应的路径是/users/xxx
<Route path="users">
<Route path="xxx" element={<Demo />} />
</Route>
</Routes>

4.React Router 实操案例

首先我们建起几个页面

1
2
3
4
5
<Home />

<About />

<Dashboard />

Home 用于展示一个简单的导航列表,About用于展示关于页,而 Dashboard 则需要用户登录以后才可以访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import './App.css';
import { BrowserRouter, Route, Routes } from "react-router-dom"

function App() {

return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
)
}


const Home = () => {
return <div>hello world</div>
}

export default App;

这里我们直接在 App.js 中加上一个叫 Home 的组件,里面只是单纯地展示 hello wolrd 而已。接下来,我们再把另外两个路径写好,加入 About 和 Dashboard 两个组件

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
import './App.css';
import { BrowserRouter, Route, Routes } from "react-router-dom"

function App() {

return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
)
}


const Home = () => {
return <div>hello world</div>
}

const About = () => {
return <div>这里是卡拉云的主页</div>
}

const Dashboard = () => {
return <div>今日活跃用户: 42</div>
}

export default App;

此时,当我们在浏览器中切换到 //about/dashboard 时,就会显示对应的组件了。注意,在上面每个 Route 中,用 element 项将组件传下去,同时在 path 项中指定路径。在 Route 外,用 Routes 包裹起整路由列表。

5.如何设置默认页路径(如 404 页)

在上文的路由列表 Routes 中,我们可以加入一个 catch all 的默认页面,比如用来作 404 页面。

我们只要在最后加入 path* 的一个路径,意为匹配所有路径,即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
return <BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
}

// 用来作为 404 页面的组件
const NotFound = () => {
return <div>你来到了没有知识的荒原</div>
}
  1. 作用: 修改URL,且不发送网络请求(路由链接)。
  2. 注意: 外侧需要用<BrowserRouter><HashRouter>包裹。
1
2
3
4
5
6
7
8
9
import { Link } from "react-router-dom";

function Test() {
return (
<div>
<Link to="/路径">按钮</Link>
</div>
);
}

作用: 与<Link>组件类似,且可实现导航的“高亮”效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注意: NavLink默认类名是active,下面是指定自定义的class

//自定义样式
// 这里的isActive是个boolean值,如果你激活了对应路由就会返回true
<NavLink
to="login"
className={({ isActive }) => {
console.log('home', isActive)
return isActive ? 'list-group-item myActive' : 'list-group-item'
}}
>login</NavLink>

/*
默认情况下,当Home的子组件匹配成功,Home的导航也会高亮,
当NavLink上添加了end属性后,若Home的子组件匹配成功,则Home的导航没有高亮效果。
可以说没有用
*/
<NavLink to="home" end >home</NavLink>

我们可以把这个逻辑抽离出来

1
2
3
4
5
6
function computeClassName({isActive}){
return isActive?"list-group-item myActive":"list-group-item";
}

<NavLink className={computeClassName} to="/about">About</NavLink>
<NavLink className={computeClassName} to="/home">Home</NavLink>

8.Navigate

  1. 作用:只要<Navigate>组件被渲染,就会修改路径,切换视图。
  2. replace属性用于控制跳转模式(push 或 replace,默认是push)。

相当于5版本的Redirect,对于我来说Redirect语义化会更好的

http://localhost:3000/home时,展示Home组件;http://localhost:3000/about时,展示About组件。http://localhost:3000/时,既不展示Home组件,也不展示About组件。http://localhost:3000/时,既不展示Home组件,也不展示About组件。)现在,我们使用Redirect Navigate组件实现重定向:<Route path="/" element={<Navigate to="/about"/>}></Route>。因此,当访问http://localhost:3000/时,重定向至http://localhost:3000/about,即默认展示About组件。

1
2
3
4
5
6
7
8
9
10
import React from 'react'
import About from "./pages/About";
import Home from "./pages/Home";
import {Route,Routes,Navigate} from "react-router-dom";

<Routes>
<Route path="/about" element={<About/>}></Route>
<Route path="/home" element={<Home/>}></Route>
<Route path="/" element={<Navigate to="/about"/>}></Route>
</Routes>

跳转模式的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React,{useState} from 'react'
import {Navigate} from 'react-router-dom'

export default function Home() {
const [sum,setSum] = useState(1)
return (
<div>
<h3>我是Home的内容</h3>
{/* 根据sum的值决定是否切换视图 */}
{sum === 1 ? <h4>sum的值为{sum}</h4> : <Navigate to="/about" replace={true}/>}
<button onClick={()=>setSum(2)}>点我将sum变为2</button>
</div>
)
}

9.使用useRoutes注册路由

9.1 使用useRoutes注册路由表-第一次改进

1
useRoutes()
  • 作用:根据路由表,动态创建<Routes><Route>
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
import React from 'react'
import {NavLink,Navigate,useRoutes} from "react-router-dom";
import About from "./pages/About";
import Home from "./pages/Home";

export default function App() {
const element = useRoutes([
{
path:"home",
element:<Home/>
},
{
path:"about",
element:<About/>
},
{
path:"/",
element:<Navigate to="/about"/>
},
])
return (
<div>
<NavLink to="/about">About</NavLink>
<NavLink to="/home">Home</NavLink>
<div className="content">
{element}
</div>
</div>
)
}

注意点:**useRoutes([])**,useRoutes根据路由表生成对应的路由规则。

9.2 第二次改进

src文件夹下新建子文件夹:routesroutes下新建文件:index.js 路由表独立成js文件:src/routes/index.js

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
routes/index.js
import { Navigate } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";

const routes = [
{
path:"/about",
element:<About/>
},
{
path:"/home",
element:<Home/>
},
{
path:"/",
element:<Navigate to="/about"/>
}
]

export default routes;
App.js
import React from 'react'
import {NavLink,useRoutes} from "react-router-dom";
import routes from "./routes";

export default function App() {
const element = useRoutes(routes);
return (
<div>
<NavLink to="/about">About</NavLink>
<NavLink to="/home">Home</NavLink>
<div className="content">
{element}
</div>
</div>
)
}

10.嵌套路由的实现

路由结构如下:

  • /about,About组件
  • /home,Home组件
    • /home/news,News组件
    • /home/message,Message组件

在pages文件夹下新建文件夹:News,News下新建文件:index.jsx,即News组件; 在pages文件夹下新建文件夹:Message,Message下新建文件:index.jsx,即Message组件。

路由表文件routes/index.js pages/Home/index.jsx,即Home组件 pages/News/index.jsx,即News组件 pages/Message/index.jsx,即Message组件

image-20221027122205526

1
routes/index.js

children 来嵌套路由。

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

const routes = [
{
path:"/about",
element:<About/>
},
{
path:"/home",
element:<Home/>,
children:[
{
path:"news",
element:<News/>
},
{
path:"message",
element:<Message/>
},
]
},
{
path:"/",
element:<Navigate to="/about"/>
}
]

export default routes;
Home/index.js
1
<Outlet>

作用:当<Route>产生嵌套时,渲染其对应的后续子路由。

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

export default function Home() {
return (
<div>
<h2>Home组件内容</h2>
<div>
<ul className="nav nav-tabs">
<li>
{/* <NavLink to="/home/news" className="list-group-item">News</NavLink> */}
{/* <NavLink to="./news" className="list-group-item">News</NavLink> */}
<NavLink to="news" className="list-group-item">News</NavLink>
</li>
<li>
<NavLink to="/home/message" className="list-group-item">Message</NavLink>
</li>
</ul>
<Outlet/>
</div>
</div>
)
}
  • 路由链接中的

    to

    属性值,可以是

    • **to="/home/news"**,即全路径(推荐这样写,不然直接看不知道是不是子路由)
    • **to="./news"**,即相对路径
    • to="news"

11.路由传递参数

11.1 传递 params 参数

需求描述:点击“消息1”,显示其id、title和content。

pages下新建子文件夹:Detail,Detail下新建文件:index.jsx。pages/Detail/index.jsx即Detail组件。

routes/index.js pages/Message/index.jsx,即Message组件 pages/Detail/index.jsx,即Detail组件

371844431d4c4553a1fbfd9d01fb140a

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

const routes = [
{
path:"/about",
element:<About/>
},
{
path:"/home",
element:<Home/>,
children:[
{
path:"news",
element:<News/>
},
{
path:"message",
element:<Message/>,
children:[
{
path:"detail/:id/:title/:content",
element:<Detail/>
}
]
},
]
},
{
path:"/",
element:<Navigate to="/about"/>
}
]

export default routes;

Message/index.jsx(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
import React,{useState} from 'react'
import { NavLink,Outlet } from 'react-router-dom'

export default function Message() {
const [message] = useState([
{id:"001",title:"消息1",content:"窗前明月光"},
{id:"002",title:"消息2",content:"疑是地上霜"},
{id:"003",title:"消息3",content:"举头望明月"},
{id:"004",title:"消息4",content:"低头思故乡"}
])

return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}/${msgObj.content}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}

Detail/index.jsx(Detail组件)

1
useParams()

作用:回当前匹配路由的params参数,类似于5.x中的match.params

1
useMatch()

作用:返回当前匹配信息,对标5.x中的路由组件的match属性。

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'
// import { useMatch } from 'react-router-dom'

export default function Detail() {
const {id,title,content} = useParams();
// const {params:{id,title,content}}= useMatch("/home/message/detail/:id/:title/:content");

return (
<ul>
<li>消息编号:{id}</li>
<li>消息标题:{title}</li>
<li>消息内容:{content}</li>
</ul>
)
}

image-20221027221627763

获取params参数有两种方式:

  1. 使用 useParamsconst {id,title,content} = useParams();
  2. 使用 useMatchconst {params:{id,title,content}}= useMatch("/home/message/detail/:id/:title/:content");

image-20221027221736759

11.2 传递 search 参数

演示的需求和上面params参数一样,所以只修改关键部分

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
routes/index.js
const routes = [
{
path:"/home",
element:<Home/>,
children:[
{
path:"news",
element:<News/>
},
{
path:"message",
element:<Message/>,
children:[
{
path:"detail",
element:<Detail/>
}
]
},
]
},
]

export default routes;

Message/index.jsx(Message组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function Message() {
return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<Link to={`detail?id=${msgObj.id}&title=${msgObj.title}&content=${msgObj.content}`}>{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}

Detail/index.jsx(Detail组件)

1
useSearchParams()

作用:用于读取和修改当前位置的 URL 中的查询字符串。 返回一个包含两个值的数组,内容分别为:当前的seaech参数、更新search的函数。

1
useLocation()

作用:获取当前 location 信息,对标5.x中的路由组件的location属性。

使用useSearchParams

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

export default function Detail() {
const [search,setSearch] = useSearchParams();
const id = search.get("id");
const title = search.get("title");
const content = search.get("content");

return (
<ul>
<li>
<button onClick={()=>setSearch('id=008&title=哈哈&content=嘻嘻')}>点我更新一下收到的search参数</button>
</li>
<li>消息编号:{id}</li>
<li>消息标题:{title}</li>
<li>消息内容:{content}</li>
</ul>
)
}

image-20221027222023399

点击按钮后

image-20221027222047406

使用useLocation

记得下载安装qs:npm install --save qs

nodejs官方说明querystring这个模块即将被废弃,推荐我们使用qs模块

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

export default function Detail() {
const {search} = useLocation();
const {id,title,content} = qs.parse(search.slice(1));

return (
<ul>
<li>消息编号:{id}</li>
<li>消息标题:{title}</li>
<li>消息内容:{content}</li>
</ul>
)
}

获取search参数,有两种写法:

  1. 使用useSearchParams
  2. 使用useLocation

11.3 传递 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
routes/index.js
const routes = [
{
path:"/home",
element:<Home/>,
children:[
{
path:"news",
element:<News/>
},
{
path:"message",
element:<Message/>,
children:[
{
path:"detail",
element:<Detail/>
}
]
},
]
},
]

export default routes;

Message/index.jsx(Message组件)

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

export default function Message() {

return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<Link to="detail" state={{ id: msgObj.id, title: msgObj.title, content: msgObj.content }} >{msgObj.title}</Link>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}

Detail/index.jsx(Detail组件)

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

export default function Detail() {
const {state:{id,title,content}} = useLocation();

return (
<ul>
<li>消息编号:{id}</li>
<li>消息标题:{title}</li>
<li>消息内容:{content}</li>
</ul>
)
}

刷新页面后对路由state参数的影响 在以前版本中,BrowserRouter没有任何影响,因为state保存在history对象中;HashRouter刷新后会导致路由state参数的丢失 但在V6版本中,HashRouter在页面刷新后不会导致路由state参数的丢失

但是现在网站基本也没看过路径有个#,所以我们使用BrowserRouter就行了。

12.编程式路由导航

案例还是和11.路由传递参数一样,只是换了种方式传参数

12.1 编程式导航下,路由传递params参数

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
pages/Message/index.jsx
import React,{useState} from 'react'
import { NavLink,Outlet,useNavigate } from 'react-router-dom'

export default function Message() {
const [message] = useState([
{id:"001",title:"消息1",content:"窗前明月光"},
{id:"002",title:"消息2",content:"疑是地上霜"},
{id:"003",title:"消息3",content:"举头望明月"},
{id:"004",title:"消息4",content:"低头思故乡"}
])

const navigate = useNavigate();

function handleClick(msgObj){
const {id,title,content} = msgObj
navigate(`detail/${id}/${title}/${content}`,{replace:false})
}

return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<NavLink to={`detail/${msgObj.id}/${msgObj.title}/${msgObj.content}`} >{msgObj.title}</NavLink>
<button onClick={() => handleClick(msgObj)}>查看消息详情</button>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}

12.2 编程式导航下,路由传递search参数

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
pages/Message/index.jsx
export default function Message() {

const navigate = useNavigate();

function handleClick(msgObj){
const {id,title,content} = msgObj
navigate(`detail?id=${id}&title=${title}&content=${content}`,{replace:false})
}

return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<NavLink to={`detail?id=${msgObj.id}&title=${msgObj.title}&content=${msgObj.content}`} >{msgObj.title}</NavLink>
<button onClick={() => handleClick(msgObj)}>查看消息详情</button>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}

12.3 编程式导航下,路由传递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
34
35
36
pages/Message/index.jsx
export default function Message() {

const navigate = useNavigate();

function handleClick(msgObj){
const {id,title,content} = msgObj
navigate("detail",{
replace:false,
state:{
id,
title,
content
}
})
}

return (
<div>
<ul>
{
message.map(msgObj => {
return (
<li key={msgObj.id}>
<NavLink to="detail" state={{ id: msgObj.id, title: msgObj.title, content: msgObj.content }} >{msgObj.title}</NavLink>
<button onClick={() => handleClick(msgObj)}>查看消息详情</button>
</li>
)
})
}
</ul>
<hr />
<Outlet/>
</div>
)
}

12.4 withRouter的替换者

这是5版本的时候

1
2
3
4
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
-this.prosp.history.goBack()
-this.prosp.history.goForward()
-this.prosp.history.go(1)

我们可以利用 react-router-dom 对象下的 withRouter 函数来对我们导出的 Header 组件进行包装,这样我们就能获得一个拥有 history 对象的一般组件

withRouter可以加工一般组件(即非路由组件),让一般组件具备路由组件所持有的API。但v6版本中已废除,可以直接用useNavigate实现。

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
import React from 'react';
import {useNavigate} from 'react-router-dom'

function Header(props) {

const navigate = new useNavigate()

const back = ()=>{
navigate(-1)
}

const forward = ()=>{
navigate(1)
}

const go = ()=>{
navigate(2)
}

return (
<div className="page-header">
<h2>React Router Demo</h2>
<button onClick={back}>回退</button>
<button onClick={forward}>前进</button>
<button onClick={go}>go</button>
</div>
);

}

export default Header;

17 【redux】

引言

我们现在开始学习了 Redux ,在我们之前写的案例当中,我们对于状态的管理,都是通过 state 来实现的,比如,我们在给兄弟组件传递数据时,需要先将数据传递给父组件,再由父组件转发 给它的子组件。这个过程十分的复杂,后来我们又学习了消息的发布订阅,我们通过 pubsub 库,实现了消息的转发,直接将数据发布,由兄弟组件订阅,实现了兄弟组件间的数据传递。但是,随着我们的需求不断地提升,我们需要进行更加复杂的数据传递,更多层次的数据交换。因此我们为何不可以将所有的数据交给一个中转站,这个中转站独立于所有的组件之外,由这个中转站来进行数据的分发,这样不管哪个组件需要数据,我们都可以很轻易的给他派发。

而有这么一个库就可以帮助我们来实现,那就是 Redux ,它可以帮助我们实现集中式状态管理

1.简介

Redux – 李立超 | lilichao.com

李立超老师的解释

A Predictable State Container for JS Apps是Redux官方对于Redux的描述,这句话可以这样翻译“一个专为JS应用设计的可预期的状态容器”,简单来说Redux是一个可预测的状态容器,什么玩意?这几个字单独拿出来都认识,连到一起后怎么就不像人话了?别急,我们一点一点看。1.1 状态(State)

state直译过来就是状态,使用React这么久了,对于state我们已经是非常的熟悉了。state不过就是一个变量,一个用来记录(组件)状态的变量。组件可以根据不同的状态值切换为不同的显示,比如,用户登录和没登录看到页面应该是不同的,那么用户的登录与否就应该是一个状态。再比如,数据加载与否,显示的界面也应该不同,那么数据本身就是一个状态。换句话说,状态控制了页面的如何显示。

但是需要注意的是,状态并不是React中或其他类似框架中独有的。所有的编程语言,都有状态,所有的编程语言都会根据不同的状态去执行不同的逻辑,这是一定的。所以状态是什么,状态就是一个变量,用以记录程序执行的情况。

1.2 容器(Container)

容器当然是用来装东西的,状态容器即用来存储状态的容器。状态多了,自然需要一个东西来存储,但是容器的功能却不是仅仅能存储状态,它实则是一个状态的管理器,除了存储状态外,它还可以用来对state进行查询、修改等所有操作。(编程语言中容器几乎都是这个意思,其作用无非就是对某个东西进行增删改查)

1.3 可预测(Predictable)

可预测指我们在对state进行各种操作时,其结果是一定的。即以相同的顺序对state执行相同的操作会得到相同的结果。简单来说,Redux中对状态所有的操作都封装到了容器内部,外部只能通过调用容器提供的方法来操作state,而不能直接修改state。这就意味着外部对state的操作都被容器所限制,对state的操作都在容器的掌控之中,也就是可预测。

总的来说,Redux是一个稳定、安全的状态管理器

2.为什么是Redux?

问:不对啊?React中不是已经有state了吗?为什么还要整出一个Redux来作为状态管理器呢?

答:state应付简单值还可以,如果值比较复杂的话并不是很方便。

问:复杂值可以用useReducer嘛!

答:的确可以啊!但无论是state还是useReducer,state在传递起来还是不方便,自上至下一层一层的传递并不方便啊!

问:那不是还有context吗?

答:的确使用context可以解决state的传递的问题,但依然是简单的数据尚可,如果数据结构过于复杂会使得context变得异常的庞大,不方便维护。

Redux可以理解为是reducer和context的结合体,使用Redux即可管理复杂的state,又可以在不同的组件间方便的共享传递state。当然,Redux主要使用场景依然是大型应用,大型应用中状态比较复杂,如果只是使用reducer和context,开发起来并不是那么的便利,此时一个有一个功能强大的状态管理器就变得尤为的重要。

3.什么情况使用 Redux

首先,我们先明晰 Redux 的作用 ,实现集中式状态管理。

Redux 适用于多交互、多数据源的场景。简单理解就是复杂

从组件角度去考虑的话,当我们有以下的应用场景时,我们可以尝试采用 Redux 来实现

  1. 某个组件的状态需要共享时
  2. 一个组件需要改变其他组件的状态时
  3. 一个组件需要改变全局的状态时

除此之外,还有很多情况都需要使用 Redux 来实现

image-20221030202337038

如上图所示,redux 通过将所有的 state 集中到组件顶部,能够灵活的将所有 state 各取所需地分发给所有的组件。

redux 的三大原则:

  • 整个应用的 state 都被存储在一棵 object tree 中,并且 object tree 只存在于唯一的 store 中(这并不意味使用 redux 就需要将所有的 state 存到 redux 上,组件还是可以维护自身的 state )。
  • state 是只读的。state 的变化,会导致视图(view)的变化。用户接触不到 state,只能接触到视图,唯一改变 state 的方式则是在视图中触发actionaction是一个用于描述已发生事件的普通对象。
  • 使用 reducers 来执行 state 的更新。 reducers 是一个纯函数,它接受 action 和当前 state 作为参数,通过计算返回一个新的 state ,从而实现视图的更新。

4.Redux 的工作流程

image-20221030202728807

如上图所示,redux 的工作流程大致如下:

  • 首先,用户在视图中通过 store.dispatch 方法发出 action
  • 然后,store 自动调用 reducers,并且传入两个参数:当前 state 和收到的 actionreducers 会返回新的 state
  • 最后,当store 监听到 state 的变化,就会调用监听函数,触发视图的重新渲染。

5.Redux API

5.1 store

  • tore 就是保存数据的地方,整个应用只能有一个 store
  • redux 提供 createStore 这个函数,用来创建一个 store 以存放整个应用的 state
1
2
import { createStore } from 'redux';
const store = createStore(reducer, [preloadedState], enhancer);

createStore用来创建一个Redux中的容器对象,它需要三个参数:reducerpreloadedStateenhancer

  • reducer是一个函数,是state操作的整合函数,每次修改state时都会触发该函数,它的返回值会成为新的state。
  • preloadedState就是state的初始值,可以在这里指定也可以在reducer中指定。
  • enhancer增强函数用来对state的功能进行扩展,暂时先不理它。

5.2 state

  • store 对象包含所有数据。如果想得到某个时点的数据,就要对 store 生成快照。这种时点的数据集合,就叫做 state
  • 如果要获取当前时刻的 state,可以通过 store.getState() 方法拿到:
1
2
3
4
import { createStore } from 'redux';
const store = createStore(reducer, [preloadedState], enhancer);

const state = store.getState();

5.3 action

  • state 的变化,会导致视图的变化。但是,用户接触不到 state,只能接触到视图。所以,state 的变化必须是由视图发起的。
  • action 就是视图发出的通知,通知store此时的 state 应该要发生变化了。
  • action 是一个对象。其中的 type 属性是必须的,表示 action 的名称。其他属性可以自由设置,社区有一个规范可以参考:
1
2
3
4
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux' // 可选属性
};

上面代码定义了一个名称为 ADD_TODOaction,它携带的数据信息是 Learn Redux

5.4 Action Creator

  • view 要发送多少种消息,就会有多少种 action,如果都手写,会很麻烦。
  • 可以定义一个函数来生成 action,这个函数就称作 Action Creator,如下面代码中的 addTodo 函数:
1
2
3
4
5
6
7
8
9
10
const ADD_TODO = '添加 TODO';

function addTodo(text) {
return {
type: ADD_TODO,
text
}
}

const action = addTodo('Learn Redux');
  • redux-actions 是一个实用的库,让编写 redux 状态管理变得简单起来。该库提供了 createAction 方法用于创建动作创建器:
1
2
3
4
import { createAction } from "redux-actions"

export const INCREMENT = 'INCREMENT'
export const increment = createAction(INCREMENT)
  • 上边代码定义一个动作 INCREMENT, 然后通过 createAction

    创建了对应 Action Creator

    • 调用 increment() 时就会返回 { type: 'INCREMENT' }
    • 调用increment(10)返回 { type: 'INCREMENT', payload: 10 }

.5 store.dispatch()

  • store.dispatch() 是视图发出 action 的唯一方法,该方法接受一个 action 对象作为参数:
1
2
3
4
5
6
7
import { createStore } from 'redux';
const store = createStore(reducer, [preloadedState], enhancer);

store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
  • 结合 Action Creator,这段代码可以改写如下:
1
2
3
4
5
6
7
8
import { createStore } from 'redux';
import { createAction } from "redux-actions"
const store = createStore(reducer, [preloadedState], enhancer);

const ADD_TODO = 'ADD_TODO';
const add_todo = createAction('ADD_TODO'); // 创建 Action Creator

store.dispatch(add_todo('Learn Redux'));

5.6 reducer

  • store 收到 action 以后,必须给出一个新的 state,这样视图才会进行更新。state 的计算(更新)过程则是通过 reducer 实现。
  • reducer 是一个函数,它接受 action 和当前 state 作为参数,返回一个新的 state
1
2
3
4
const reducer = function (state, action) {
// ...
return new_state;
};
  • 为了实现调用 store.dispatch 方法时自动执行 reducer 函数,需要在创建 store 时将将 reducer 传入 createStore 方法:
1
2
3
4
5
6
import { createStore } from 'redux';
const reducer = function (state, action) {
// ...
return new_state;
};
const store = createStore(reducer);
  • 上面代码中,createStore 方法接受 reducer 作为参数,生成一个新的 store。以后每当视图使用 store.dispatch 发送给 store 一个新的 action,就会自动调用 reducer函数,得到更新的 state
  • redux-actions 提供了 handleActions 方法用于处理多个 action
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用方法:
// handleActions(reducerMap, defaultState)

import { handleActions } from 'redux-actions';
const initialState = {
counter: 0
};

const reducer = handleActions(
{
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
},
initialState,
);

6.在网页中直接使用

我们先来在网页中使用以下Redux,在网页中使用Redux就像使用jQuery似的,直接在网页中引入Redux的库文件即可:

1
<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>

网页中我们实现一个简单的计数器功能,页面长成这样:

image-20221030210318059

1
2
3
<button id="btn01">减少</button>
<span id="counter">1</span>
<button id="btn02">增加</button>

我们要实现的功能很简单,点击减少数字变小,点击增加数字变大。如果用传统的DOM编写,可以创建一个变量用以记录数量,点击不同的按钮对变量做不同的修改并设置到span之中,代码像是这样:

不使用Redux:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const btn01 = document.getElementById('btn01');
const btn02 = document.getElementById('btn02');
const counterSpan = document.getElementById('counter');

let count = 1;

btn01.addEventListener('click', ()=>{
count--;
counterSpan.innerText = count;
});

btn02.addEventListener('click', ()=>{
count++;
counterSpan.innerText = count;
});

上述代码中count就是一个状态,只是这个状态没有专门的管理器,它的所有操作都在事件的响应函数中进行处理,这种状态就是不可预测的状态,因为在任何的函数中都可以对这个状态进行修改,没有任何安全限制。

使用Redux:

Redux是一个状态容器,所以使用Redux必须先创建容器对象,它的所有操作都是通过容器对象来进行的

1
Redux.createStore(reducer, [preloadedState], [enhancer])

三个参数中,只有reducer是必须的,来看一个Reducer的示例:

1
2
3
4
5
6
7
8
9
10
const countReducer = (state = {count:0}, action) => {
switch (action.type){
case 'ADD':
return {count:state.count+1};
case 'SUB':
return {count:state.count-1};
default:
return state
}
};

state = {count:0}这是在指定state的默认值,如果不指定,第一次调用时state的值会是undefined。也可以将该值指定为createStore()的第二个参数。action是一个普通对象,用来存储操作信息。

将reducer传递进createStore后,我们会得到一个store对象:

1
const store = Redux.createStore(countReducer);

store对象创建后,对state的所有操作都需要通过它来进行:

读取state:

1
store.getState()

修改state:

1
store.dispatch({type:'ADD'})

dipatch用来触发state的操作,可以将其理解为是想reducer发送任务的工具。它需要一个对象作为参数,这个对象将会成为reducer的第二个参数action,需要将操作信息设置到对象中传递给reducer。action中最重要的属性是type,type用来识别对state的不同的操作,上例中’ADD’表示增加操作,’SUB’表示减少的操作。

除了这些方法外,store还拥有一个subscribe方法,这个方法用来订阅state变化的信息。该方法需要一个回调函数作为参数,当store中存储的state发生变化时,回调函数会自动调用,我们可以在回调函数中定义state发生变化时所要触发的操作:

1
2
3
4
5
6
store.subscribe(()=>{
// store中state发生变化时触发
console.log('state变化了')
console.log(store.getState())
spanRef.current.innerText = store.getState().count
});

如此一来,刚刚的代码被修改成了这个样子:

修改后的代码相较于第一个版本要复杂一些,同时也解决了之前代码中存在的一些问题:

  1. 前一个版本的代码state就是一个变量,可以任意被修改。state不可预测,容易被修改为错误的值。新代码中使用了Redux,Redux中的对state的所有操作都封装到了reducer函数中,可以限制state的修改使state可预测,有效的避免了错误的state值。
  2. 前一个版本的代码,每次点击按钮修改state,就要手动的修改counterSpan的innerText,非常麻烦,这样一来我们如果再添加新的功能,依然不能忘记对其进行修改。新代码中,counterSpan的修改是在store.subscribe()的回调函数中进行的,state每次发生变化其值就会随之变化,不需要再手动修改。换句话说,state和DOM元素通过Redux绑定到了一起。

通过上例也不难看出,Redux中最最核心的东西就是这个store,只要拿到了这个store对象就相当于拿到了Redux中存储的数据。在加上Redux的核心思想中有一条叫做“单一数据源”,也就是所有的state都会存储到一课对象树中,并且这个对象树会存储到一个store中。所以到了React中,组件只需获取到store即可获取到Redux中存储的所有state。

7.React-Redux 基本使用

7.1 引言

在前面我们学习了 Redux ,我们在写案例的时候,也发现了它存在着一些问题,例如组件无法状态无法公用,每一个状态组件都需要通过订阅来监视,状态更新会影响到全部组件更新,面对着这些问题,React 官方在 redux 基础上提出了 React-Redux 库

在前面的案例中,我们如果把 store 直接写在了 React 应用的顶层 props 中,各个子组件,就能访问到顶层 props

1
2
3
<顶层组件 store={store}>
<App />
</顶层组件/>

这就类似于 React-Redux

7.2 开始使用 React-Redux

当我们需要在React中使用Redux时,我们除了需要引入Redux核心库外,还需要引入react-redux库,以使React和redux适配,可以通过npm或yarn安装:

1
npm install -S redux react-redux

接下来我们尝试在Redux,添加一些复杂的state,比如一个学生的信息:

1
{name:'孙悟空', age:18, gender:'男', address:'花果山'}

创建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
const reducer = (state = {
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山'
}, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.payload
};
case 'SET_AGE':
return {
...state,
age: action.payload
};
case 'SET_ADDRESS':
return {
...state,
address: action.payload
};
case 'SET_GENDER':
return {
...state,
gender: action.payload
};
default :
return state
}

};

reducer的编写和之前的案例并没有本质的区别,只是这次的数据和操作方法变得复杂了一些。以SET_NAME为例,当需要修改name属性时,dispatch需要传递一个有两个属性的action,action的type应该是字符串”SET_NAME”,payload应该是要修改的新名字,比如要将名字修改为猪八戒,则dispatch需要传递这样一个对象{type:'SET_NAME',payload:'猪八戒'}

创建store:

1
const store = createStore(reducer);

创建store和前例并无差异,传递reducer进行构建即可。

7.3 设置 provider

由于我们的状态可能会被很多组件使用,所以 React-Redux 给我们提供了一个 Provider 组件,可以全局注入 redux 中的 store ,只需要把 Provider 注册在根部组件即可

例如,当以下组件都需要使用 store 时,我们需要这么做,但是这样徒增了工作量,很不便利

1
2
3
4
5
6
7
<Count store={store}/>
{/* 示例 */}
<Demo1 store={store}/>
<Demo2 store={store}/>
<Demo3 store={store}/>
<Demo4 store={store}/>
<Demo5 store={store}/>

我们可以这么做:在 src 目录下的 main.jsx 文件中,引入 Provider ,直接用 Provider 标签包裹 App 组件,将 store 写在 Provider 中即可

1
2
3
4
5
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)

7.4 操作数据

访问数据:

1
const stu = useSelector(state => state);

react-redux还为我们提供一个钩子函数useSelector,用于获取Redux中存储的数据,它需要一个回调函数作为参数,回调函数的第一个参数就是当前的state,回调函数的返回值,会作为useSelector的返回值返回,所以state => state表示直接将整个state作为返回值返回。现在就可以通过stu来读取state中的数据了:

1
2
3
<p>
{stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
</p>

操作数据:

1
const dispatch = useDispatch();

useDispatch同样是react-redux提供的钩子函数,用来获取redux的派发器,对state的所有操作都需要通过派发器来进行。

通过派发器修改state:

1
2
3
4
dispatch({type:'SET_NAME', payload:'猪八戒'})
dispatch({type:'SET_AGE', payload:28})
dispatch({type:'SET_GENDER', payload:'女'})
dispatch({type:'SET_ADDRESS', payload:'高老庄'})

完整代码:

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
import ReactDOM from 'react-dom/client';
import {Provider, useDispatch, useSelector} from "react-redux";
import {createStore} from "redux";

const reducer = (state = {
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山'
}, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.payload
};
case 'SET_AGE':
return {
...state,
age: action.payload
};
case 'SET_ADDRESS':
return {
...state,
address: action.payload
};
case 'SET_GENDER':
return {
...state,
gender: action.payload
};
default :
return state
}

};

const store = createStore(reducer);

const App = () =>{
const stu = useSelector(state => state);
const dispatch = useDispatch();
return <div>
<p>
{stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
</p>
<div>
<button onClick={()=>{dispatch({type:'SET_NAME', payload:'猪八戒'})}}>改name</button>
<button onClick={()=>{dispatch({type:'SET_AGE', payload:28})}}>改age</button>
<button onClick={()=>{dispatch({type:'SET_GENDER', payload:'女'})}}>改gender</button>
<button onClick={()=>{dispatch({type:'SET_ADDRESS', payload:'高老庄'})}}>改address</button>
</div>
</div>
};

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

8.复杂的State

上例中的数据结构已经变得复杂,但是距离真实项目还有一定的差距。因为Redux的核心思想是所有的state都应该存储到同一个仓库中,所以只有一个学生数据确实显得有点单薄,现在将数据变得复杂一些,出来学生数据外,还增加了一个学校的信息,于是state的结构变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
{
stu:{
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山'
},
school:{
name:'花果山一小',
address:'花果山大街1号'
}
}

数据结构变得复杂了,我们需要对代码进行修改,首先看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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const reducer = (state = {
stu: {
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山'
},
school: {
name: '花果山一小',
address: '花果山大街1号'
}

}, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
stu: {
...state.stu,
name: action.payload
}
};
case 'SET_AGE':
return {
...state,
stu: {
...state.stu,
age: action.payload
}
};
case 'SET_ADDRESS':
return {
...state,
stu: {
...state.stu,
address: action.payload
}
};
case 'SET_GENDER':
return {
...state,
stu: {
...state.stu,
gender: action.payload
}
};
case 'SET_SCHOOL_NAME':
return {
...state,
school: {
...state.school,
name:action.payload
}
};
case 'SET_SCHOOL_ADDRESS':
return {
...state,
school: {
...state.school,
address: action.payload
}
}
default :
return state;
}

};

数据层次变多了,我们在操作数据时也变得复杂了,比如修改name的逻辑变成了这样:

1
2
3
4
5
6
7
8
case 'SET_NAME':
return {
...state,
stu: {
...state.stu,
name: action.payloadjs
}
};

同时数据加载的逻辑也要修改,之前我们是将整个state返回,现在我们需要根据不同情况获取state,比如获取学生信息要这么写:

1
const stu = useSelector(state => state.stu);

获取学校信息:

1
const school = useSelector(state => state.school);

完整代码:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
store/stuReducer.js
const stuReducer = (
state = {
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山',
},
action,
) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.payload,
}
case 'SET_AGE':
return {
...state,
age: action.payload,
}
case 'SET_ADDRESS':
return {
...state,
address: action.payload,
}
case 'SET_GENDER':
return {
...state,
gender: action.payload,
}
default:
return state
}
}

export default stuReducer
store/index.js
import { createStore } from 'redux'
import stuReducer from './stuReducer.js'

const store = createStore(stuReducer)

export default store
main.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>,
)
App.jsx
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'

export default function App() {
const stu = useSelector(state => state)
const dispatch = useDispatch()
return (
<div>
<p>
{stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
</p>
<div>
<button
onClick={() => {
dispatch({ type: 'SET_NAME', payload: '猪八戒' })
}}
>
改name
</button>
<button
onClick={() => {
dispatch({ type: 'SET_AGE', payload: 28 })
}}
>
改age
</button>
<button
onClick={() => {
dispatch({ type: 'SET_GENDER', payload: '女' })
}}
>
改gender
</button>
<button
onClick={() => {
dispatch({ type: 'SET_ADDRESS', payload: '高老庄' })
}}
>
改address
</button>
</div>
</div>
)
}

麻烦确实是麻烦了一些,但是还好功能实现了。

9.多个Reducer

上边的案例的写法存在一个非常严重的问题!将所有的代码都写到一个reducer中,会使得这个reducer变得无比庞大,现在只有学生和学校两个信息。如果数据在多一些,操作方法也会随之增多,reducer会越来越庞大变得难以维护。

Redux中是允许我们创建多个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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const stuReducer = (state = {
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山'
}, action) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.payload
};
case 'SET_AGE':
return {
...state,
age: action.payload
};
case 'SET_ADDRESS':
return {
...state,
address: action.payload
};
case 'SET_GENDER':
return {
...state,
gender: action.payload
};
default :
return state;
}

};

const schoolReducer = (state = {

name: '花果山一小',
address: '花果山大街1号'

}, action) => {
switch (action.type) {
case 'SET_SCHOOL_NAME':
return {
...state,
name: action.payload
};
case 'SET_SCHOOL_ADDRESS':
return {
...state,
address: action.payload
};
default :
return state;
}

};

修改后reducer被拆分为了stuReducer和schoolReducer,拆分后在编写每个reducer时,只需要考虑当前的state数据,不再需要对无关的数据进行复制等操作,简化了reducer的编写。于此同时将不同的功能编写到了不同的reducer中,降低了代码间的耦合,方便对代码进行维护。

拆分后,还需要使用Redux为我们提供的函数combineReducer将多个reducer进行合并,合并后才能传递进createStore来创建store。

1
2
3
4
5
6
const reducer = combineReducers({
stu:stuReducer,
school:schoolReducer
});

const store = createStore(reducer);

combineReducer需要一个对象作为参数,对象的属性名可以根据需要指定,比如我们有两种数据stu和school,属性名就命名为stu和school,stu指向stuReducer,school指向schoolReducer。读取数据时,直接通过state.stu读取学生数据,通过state.school读取学校数据

完整代码:

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
62
63
64
65
66
67
68
69
70
71
store/reducers.js
export const stuReducer = (
state = {
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山',
},
action,
) => {
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.payload,
}
case 'SET_AGE':
return {
...state,
age: action.payload,
}
case 'SET_ADDRESS':
return {
...state,
address: action.payload,
}
case 'SET_GENDER':
return {
...state,
gender: action.payload,
}
default:
return state
}
}

export const schoolReducer = (
state = {
name: '花果山一小',
address: '花果山大街1号',
},
action,
) => {
switch (action.type) {
case 'SET_SCHOOL_NAME':
return {
...state,
name: action.payload,
}
case 'SET_SCHOOL_ADDRESS':
return {
...state,
address: action.payload,
}
default:
return state
}
}
store/index.js
import { createStore, combineReducers } from 'redux'
import { stuReducer, schoolReducer } from './reduces.js'

const reducer = combineReducers({
stu: stuReducer,
school: schoolReducer,
})

const store = createStore(reducer)

export default store
main.js

8.复杂的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
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
62
63
64
65
66
67
68
69
App.jsx
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'

export default function App() {
const stu = useSelector(state => state.stu)
const school = useSelector(state => state.school)
const dispatch = useDispatch()

return (
<div>
<p>
{stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
</p>
<div>
<button
onClick={() => {
dispatch({ type: 'SET_NAME', payload: '猪八戒' })
}}
>
改name
</button>
<button
onClick={() => {
dispatch({ type: 'SET_AGE', payload: 28 })
}}
>
改age
</button>
<button
onClick={() => {
dispatch({ type: 'SET_GENDER', payload: '女' })
}}
>
改gender
</button>
<button
onClick={() => {
dispatch({ type: 'SET_ADDRESS', payload: '高老庄' })
}}
>
改address
</button>
</div>

<hr />

<p>
{school.name} -- {school.address}
</p>
<div>
<button
onClick={() => {
dispatch({ type: 'SET_SCHOOL_NAME', payload: '高老庄小学' })
}}
>
改学校name
</button>
<button
onClick={() => {
dispatch({ type: 'SET_SCHOOL_ADDRESS', payload: '高老庄中心大街15号' })
}}
>
改学校address
</button>
</div>
</div>
)
}

10.总结

数据流更新动画

Redux 确实有许多新的术语和概念需要记住。提醒一下,这是我们刚刚介绍的内容:

  • Redux 是一个管理全局应用状态的库
    • Redux 通常与 React-Redux 库一起使用,把 Redux 和 React 集成在一起
    • Redux Toolkit 是编写 Redux 逻辑的推荐方式
  • Redux 使用 “单向数据流”
    • State 描述了应用程序在某个时间点的状态,视图基于该 state 渲染
    • 当应用程序中发生某些事情时:
      • 视图 dispatch 一个 action
      • store 调用 reducer,随后根据发生的事情来更新 state
      • store 将 state 发生了变化的情况通知 UI
    • 视图基于新 state 重新渲染
  • Redux 有这几种类型的代码
    • Action 是有 type 字段的纯对象,描述发生了什么
    • Reducer 是纯函数,基于先前的 state 和 action 来计算新的 state
    • 每当 dispatch 一个 action 后,store 就会调用 root reducer

18 【Redux Toolkit】

上边的案例我们一直在使用Redux核心库来使用Redux,除了Redux核心库外Redux还为我们提供了一种使用Redux的方式——Redux Toolkit。它的名字起的非常直白,Redux工具包,简称RTK。RTK可以帮助我们处理使用Redux过程中的重复性工作,简化Redux中的各种操作。

1.Redux Toolkit 概览

1.1 Redux Toolkit 是什么?

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。 它包含我们对于构建 Redux 应用程序必不可少的包和函数。 Redux Toolkit 的构建简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。可以说 Redux Toolkit 就是目前 Redux 的最佳实践方式。

为了方便后面内容,之后 Redux Toolkit 简称 RTK

1.2 目的

Redux 核心库是故意设计成非定制化的样子(unopinionated)。怎么做完全取决于你,例如配置 store,你的 state 存什么东西,以及如何构建 reducer。

有些时候这样挺好,因为有很高的灵活性,但我们又不总是需要这么高的自由度。有时,我们只是想以最简单的方式上手,并想要一些良好的默认行为能够开箱即用。或者,也许你正在编写一个更大的应用程序并发现自己正在编写一些类似的代码,而你想减少必须手工编写的代码量。

Redux Toolkit 它最初是为了帮助解决有关 Redux 的三个常见问题而创建的:

  • “配置 Redux store 过于复杂”
  • “我必须添加很多软件包才能开始使用 Redux”
  • “Redux 有太多样板代码”

1.3 为什么需要使用 Redux Toolkit

通过遵循我们推荐的最佳实践,提供良好的默认行为,捕获错误并让你编写更简单的代码,React Toolkit 使得编写好的 Redux 应用程序以及加快开发速度变得更加容易。 Redux Toolkit 对所有 Redux 用户都有帮助,无论技能水平或者经验如何。可以在新项目开始时添加它,也可以在现有项目中将其用作增量迁移的一部分。

1.4 文档链接

学习的最佳方法我个人觉得还是看官方文档比较权威: 中文官方文档 英文官方文档

2.安装

安装,无论是RTK还是Redux,在React中使用时react-redux都是必不可少,所以使用RTK依然需要安装两个包:react-redux和@reduxjs/toolkit。

npm

1
npm install react-redux @reduxjs/toolkit -S

yarn

1
yarn add react-redux @reduxjs/toolkit

在官方文档中其实提供了完整的 RTK 项目创建命令,但咱们学习就从基础的搭建开始吧。

3.基础开发流程

安装完相关包以后开始编写基本的 RTK 程序

  • 创建一个store文件夹
  • 创建一个index.ts做为主入口
  • 创建一个festures文件夹用来装所有的store
  • 创建一个counterSlice.js文件,并导出简单的加减方法

3.1 创建 Redux State Slice

创建 slice 需要一个字符串名称来标识切片、一个初始 state 以及一个或多个定义了该如何更新 state 的 reducer 函数。slice 创建后 ,我们可以导出 slice 中生成的 Redux action creators 和 reducer 函数。

image-20221031123543763

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
store/features/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
value: 0,
}

// 创建一个Slice
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// 定义一个加的方法
increment: state => {
state.value += 1
},
// 定义一个减的方法
decrement: state => {
state.value -= 1
},
},
})
console.log('counterSlice', counterSlice)
console.log('counterSlice.actions', counterSlice.actions)

// 导出加减方法
export const { increment, decrement } = counterSlice.actions

// 暴露reducer
export default counterSlice.reducer

createSlice是一个全自动的创建reducer切片的方法,在它的内部调用就是createAction和createReducer,之所以先介绍那两个也是这个原因。createSlice需要一个对象作为参数,对象中通过不同的属性来指定reducer的配置信息。

1
createSlice(configuration object)

配置对象中的属性:

  • name —— reducer的名字,会作为action中type属性的前缀,不要重复
  • initialState —— state的初始值
  • reducers —— reducer的具体方法,需要一个对象作为参数,可以以方法的形式添加reducer,RTK会自动生成action对象。

总的来说,使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer,action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改。reducer需要传递给configureStore以使其在仓库中生效。

我们可以看看counterSlicecounterSlice.actions是什么样子

image-20221031124548096

3.2 将 Slice Reducers 添加到 Store 中

下一步,我们需要从计数切片中引入 reducer 函数,并将它添加到我们的 store 中。通过在 reducer 参数中定义一个字段,我们告诉 store 使用这个 slice reducer 函数来处理对该状态的所有更新。

我们以前直接用redux是这样的

1
2
3
4
5
6
const reducer = combineReducers({
counter:counterReducers
});

const store = createStore(reducer);
store/index.js

切片的reducer属性是切片根据我们传递的方法自动创建生成的reducer,需要将其作为reducer传递进configureStore的配置对象中以使其生效:

1
2
3
4
5
6
7
8
9
10
11
12
import { configureStore } from '@reduxjs/toolkit'
import counterSlice from './features/counterSlice'

// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
counter: counterSlice,
},
})

export default store
  • configureStore需要一个对象作为参数,在这个对象中可以通过不同的属性来对store进行设置,比如:reducer属性用来设置store中关联到的reducer,preloadedState用来指定state的初始值等,还有一些值我们会放到后边讲解。
  • reducer属性可以直接传递一个reducer,也可以传递一个对象作为值。如果只传递一个reducer,则意味着store中只有一个reducer。若传递一个对象作为参数,对象的每个属性都可以执行一个reducer,在方法内部它会自动对这些reducer进行合并。

3.3 store加到全局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
main.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

// redux toolkit
import { Provider } from 'react-redux'
import store from './store'

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

3.4 在 React 组件中使用 Redux 状态和操作

现在我们可以使用 React-Redux 钩子让 React 组件与 Redux store 交互。我们可以使用 useSelector 从 store 中读取数据,使用 useDispatch dispatch actions。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
App.jsx
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
// 引入对应的方法
import { increment, decrement } from './store/features/counterSlice'

export default function App() {
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()

return (
<div style={{ width: 100, margin: '100px auto' }}>
<button onClick={() => dispatch(increment())}>+</button>
<span>{count}</span>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
)
}

image-20221031125215129

现在,每当你点击”递增“和“递减”按钮。

  • 会 dispatch 对应的 Redux action 到 store
  • 在计数器切片对应的 reducer 中将看到 action 并更新其状态
  • <App>组件将从 store 中看到新的状态,并使用新数据重新渲染组件

3.5 小总结

这是关于如何通过 React 设置和使用 Redux Toolkit 的简要概述。 回顾细节:

  • 使用configureStore创建 Redux store
    • configureStore 接受 reducer 函数作为命名参数
    • configureStore 使用的好用的默认设置自动设置 store
  • 为 React 应用程序组件提供 Redux store
    • 使用 React-Redux <Provider> 组件包裹你的 <App />
    • 传递 Redux store 如 <Provider store={store}>
  • 使用 createSlice 创建 Redux “slice” reducer
    • 使用字符串名称、初始状态和命名的 reducer 函数调用“createSlice”
    • Reducer 函数可以使用 Immer 来“改变”状态
    • 导出生成的 slice reducer 和 action creators
  • 在 React 组件中使用 React-Redux useSelector/useDispatch 钩子
    • 使用 useSelector 钩子从 store 中读取数据
    • 使用 useDispatch 钩子获取 dispatch 函数,并根据需要 dispatch actions

4.补充解析上面计数器案例

这个工具帮我们封装好了很多操作,虽然很方便,但是刚使用很多地方不是那么习惯。

每个文件的代码就不贴了,和上面一样的,可以复制到文本结合看

4 .1 创建 Slice Reducer 和 Action

store/features/counterSlice.js

早些时候,我们看到单击视图中的不同按钮会 dispatch 三种不同类型的 Redux action:

  • {type: "counter/increment"}
  • {type: "counter/decrement"}
  • {type: "counter/incrementByAmount"}

我们知道 action 是带有 type 字段的普通对象,type 字段总是一个字符串,并且我们通常有 action creator 函数来创建和返回 action 对象。那么在哪里定义 action 对象、类型字符串和 action creator 呢?

我们可以每次都手写。但是,那会很乏味。此外,Redux 中真正重要的是 reducer 函数,以及其中计算新状态的逻辑。

Redux Toolkit 有一个名为 createSlice 的函数,它负责生成 action 类型字符串、action creator 函数和 action 对象的工作。你所要做的就是为这个 slice 定义一个名称,编写一个包含 reducer 函数的对象,它会自动生成相应的 action 代码。name 选项的字符串用作每个 action 类型的第一部分,每个 reducer 函数的键名用作第二部分。因此,"counter" 名称 + "increment" reducer 函数生成了一个 action 类型 {type: "counter/increment"}。(毕竟,如果计算机可以为我们做,为什么要手写!)

除了 name 字段,createSlice 还需要我们为 reducer 传入初始状态值,以便在第一次调用时就有一个 state。在这种情况下,我们提供了一个对象,它有一个从 0 开始的 value 字段。

我们可以看到这里有三个 reducer 函数,它们对应于通过单击不同按钮 dispatch 的三种不同的 action 类型。

createSlice 会自动生成与我们编写的 reducer 函数同名的 action creator。我们可以通过调用其中一个来检查它并查看它返回的内容:

1
2
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}

它还生成知道如何响应所有这些 action 类型的 slice reducer 函数:

1
2
3
4
5
6
const newState = counterSlice.reducer(
{ value: 10 },
counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}

4.2 Reducer 的规则

Reducer 必需符合以下规则:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

但为什么这些规则很重要?有几个不同的原因:

  • Redux 的目标之一是使你的代码可预测。当函数的输出仅根据输入参数计算时,更容易理解该代码的工作原理并对其进行测试。
  • 另一方面,如果一个函数依赖于自身之外的变量,或者行为随机,你永远不知道运行它时会发生什么。

“不可变更新(Immutable Updates)” 这个规则尤其重要,值得进一步讨论。

4.3 Reducer 与 Immutable 更新

前面讲过 “mutation”(更新已有对象/数组的值)与 “immutability”(认为值是不可以改变的)

在 Redux 中,*永远* 不允许在 reducer 中更改 state 的原始对象!

1
2
// ❌ 非法 - 默认情况下,这将更改 state!
state.value = 123

不能在 Redux 中更改 state 有几个原因:

  • 它会导致 bug,例如视图未正确更新以显示最新值
  • 更难理解状态更新的原因和方式
  • 编写测试变得更加困难
  • 它违背了 Redux 的预期精神和使用模式

所以如果我们不能更改原件,我们如何返回更新的状态呢?

Reducer 中必需要先创建原始值的副本,然后可以改变副本。

1
2
3
4
5
// ✅ 这样操作是安全的,因为创建了副本
return {
...state,
value: 123
}

我们已经看到我们可以手动编写 immutable 更新open in new window 。但是,手动编写不可变的更新逻辑确实繁琐,而且在 reducer 中意外改变状态是 Redux 用户最常犯的一个错误。

这就是为什么 Redux Toolkit 的 createSlice 函数可以让你以更简单的方式编写不可变更新!

createSlice 内部使用了一个名为 Immeropen in new window 的库。 Immer 使用一种称为 “Proxy” 的特殊 JS 工具来包装你提供的数据,当你尝试 ”mutate“ 这些数据的时候,奇迹发生了,Immer 会跟踪你尝试进行的所有更改,然后使用该更改列表返回一个安全的、不可变的更新值,就好像你手动编写了所有不可变的更新逻辑一样。

所以,下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}

可以变成这样:

1
2
3
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}

变得非常易读!

但,还有一些非常重要的规则要记住:

警告

你只能在 Redux Toolkit 的 createSlicecreateReducer 中编写 “mutation” 逻辑,因为它们在内部使用 Immer!如果你在没有 Immer 的 reducer 中编写 mutation 逻辑,它将改变状态并导致错误!

5.传递参数

上面的项目中固定的加一减一,那如果我们想加多少就能动态加多少,那就需要传参。那如何传参呢?

5.1 定义接受参数

接收参数的方式和 redux 一样,我们可以通过 action 来接收参数,如下:

store/features/counterSlice.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createSlice } from '@reduxjs/toolkit'

// 创建一个Slice
export const counterSlice = createSlice({
// ...
reducers: {
incrementByAmount: (state, action) => {
// action 里面有 type 和 payload 两个属性,所有的传参都在payload里面
console.log(action)
state.value += action.payload
},
},
})

// 导出加减方法
export const { increment, decrement, incrementByAmount } = counterSlice.actions

// 暴露reducer
export default counterSlice.reducer

incrementByAmountaction参数

image-20221031135743580

5.2 传递参数

redux 的传参一样,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
// 引入对应的方法
import { incrementByAmount } from './store/features/counterSlice'

export default function App() {
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()
const [amount, setAmount] = useState(1)

return (
<div style={{ width: 500, margin: '100px auto' }}>
<input type="text" value={amount} onChange={e => setAmount(e.target.value)} />
<button onClick={() => dispatch(incrementByAmount(Number(amount) || 0))}> Add Amount </button>
<span>{count}</span>
</div>
)
}

image-20221031135809294

注意这里reducer的action中如果要传入参数,只能是一个payload,如果是多个参数的情况,那就需要封装成一个payload的对象。

5.3 Action Payloads

以一个常见的todo案例来讲解

store/features/todoSlice.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { createSlice, nanoid } from '@reduxjs/toolkit'

const initialState = {
todoList: [],
}

// 创建一个Slice
export const todoSlice = createSlice({
name: 'todo',
initialState,
reducers: {
addTodo: (state, action) => {}
},
},
})

// 导出加减方法
export const { addTodo } = todoSlice.actions

// 暴露reducer
export default todoSlice.reducer
store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterSlice from './features/counterSlice'
import todoSlice from './features/todoSlice'

// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
counter: counterSlice,
todo: todoSlice,
},
})

export default store

Todo.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
// 引入对应的方法
import { addTodo } from '../store/features/todoSlice'

export default function Todo() {
const todoList = useSelector(state => state.todo.todoList)

const dispatch = useDispatch()

return (
<div>
<p>任务列表</p>
<ul>
{todoList.map(todo => (
<li key={todo.id}>
<input type="checkbox" defaultChecked={todo.completed} /> {todo.content}
</li>
))}
</ul>
<button onClick={() => dispatch(addTodo('敲代码'))}>增加一个todo</button>
</div>
)
}

我们刚刚看到 createSlice 中的 action creator 通常期望一个参数,它变成了 action.payload。这简化了最常见的使用模式,但有时我们需要做更多的工作来准备 action 对象的内容。 在我们的 postAdded 操作的情况下,我们需要为新todo生成一个唯一的 ID,我们还需要确保有效 payload 是一个看起来像 {id, content, completed} 的对象。

现在,我们正在 React 组件中生成 ID 并创建有效 payload 对象,并将有效 payload 对象传递给 addTodo。 但是,如果我们需要从不同的组件 dispatch 相同的 action,或者准备 payload 的逻辑很复杂怎么办? 每次我们想要 dispatch action 时,我们都必须复制该逻辑,并且我们强制组件确切地知道此 action 的有效 payload 应该是什么样子。

# 注意

如果 action 需要包含唯一 ID 或其他一些随机值,请始终先生成该随机值并将其放入 action 对象中。 Reducer 中永远不应该计算随机值,因为这会使结果不可预测。

幸运的是,createSlice 允许我们在编写 reducer 时定义一个 prepare 函数。 prepare 函数可以接受多个参数,生成诸如唯一 ID 之类的随机值,并运行需要的任何其他同步逻辑来决定哪些值进入 action 对象。然后它应该返回一个包含 payload 字段的对象。(返回对象还可能包含一个 meta 字段,可用于向 action 添加额外的描述性值,以及一个 error 字段,该字段应该是一个布尔值,指示此 action 是否表示某种错误。)

rtk还提供了一个nanoid方法,用于生成一个固定长度的随机字符串,类似uuid功能。

可以打印dispatch(addTodo(’敲代码‘))的结果看到,返回了一个带有payload字段的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
import { createSlice, nanoid } from '@reduxjs/toolkit'

// 创建一个Slice
export const todoSlice = createSlice({
name: 'todo',
initialState,
reducers: {
addTodo: {
// 这个函数就是我们平时直接写在这的函数( addTodo: (state, action) => {})
reducer(state, aciton) {
console.log('addTodo-reducer执行')
const { id, content } = aciton.payload
state.todoList.push({ id, content, completed: false })
},
// 预处理函数,返回值就是reducer函数接收的pyload值, 必须返回一个带有payload字段的对象
prepare(content) {
console.log('prepare参数', content)
return {
payload: {
id: nanoid(),
content,
},
}
},
},
},
})

image-20221031151023678

6.异步逻辑与数据请求

6.1 Thunks 与异步逻辑

就其本身而言,Redux store 对异步逻辑一无所知。它只知道如何同步 dispatch action,通过调用 root reducer 函数更新状态,并通知 UI 某些事情发生了变化。任何异步都必须发生在 store 之外。

但是,如果你希望通过调度或检查当前 store 状态来使异步逻辑与 store 交互,该怎么办? 这就是 Redux middleware

的用武之地。它们扩展了 store,并允许你:

  • dispatch action 时执行额外的逻辑(例如打印 action 的日志和状态)
  • 暂停、修改、延迟、替换或停止 dispatch 的 action
  • 编写可以访问 dispatchgetState 的额外代码
  • dispatch 如何接受除普通 action 对象之外的其他值,例如函数和 promise,通过拦截它们并 dispatch 实际 action 对象来代替

Redux 有多种异步 middleware,每一种都允许你使用不同的语法编写逻辑。最常见的异步 middleware 是 redux-thunk它可以让你编写可能直接包含异步逻辑的普通函数。Redux Toolkit 的 configureStore 功能默认自动设置 thunk middleware 我们推荐使用 thunk 作为 Redux 开发异步逻辑的标准方式

6.2 Thunk 函数

thunk最重要的思想,就是可以接受一个返回函数的action creator。如果这个action creator 返回的是一个函数,就执行它,如果不是,就按照原来的action执行。

正因为这个action creator可以返回一个函数,那么就可以在这个函数中执行一些异步的操作。

Thunks 通常还可以使用 action creator 再次 dispatch 普通的 action,比如 dispatch(increment())

为了与 dispatch 普通 action 对象保持一致,我们通常将它们写为 thunk action creators,它返回 thunk 函数。这些 action creator 可以接受可以在 thunk 中使用的参数。

1
2
3
4
5
6
7
const incrementAsync = amount => {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
}

incrementAsync函数就返回了一个函数,将dispatch作为函数的第一个参数传递进去,在函数内进行异步操作就可以了。

Thunk 通常写在 “slice” 文件中。createSlice 本身对定义 thunk 没有任何特殊支持,因此你应该将它们作为单独的函数编写在同一个 slice 文件中。这样,他们就可以访问该 slice 的普通 action creator,并且很容易找到 thunk 的位置。

6.3 改写之前的计数器案例

增加一个延时器

store/features/counterSlice.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
value: 0,
}

// 创建一个Slice
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
incrementByAmount: (state, action) => {
// action 里面有 type 和 payload 两个属性,所有的传参都在payload里面
state.value += action.payload
},
},
})

const {incrementByAmount } = counterSlice.actions

export const incrementAsync = amount => {
return (dispatch, getState) => {

const stateBefore = getState()
console.log('Counter before:', stateBefore.counter)

setTimeout(() => {
dispatch(incrementByAmount(amount))
const stateAfter = getState()
console.log('Counter after:', stateAfter.counter)
}, 1000)
}
}

// 暴露reducer
export default counterSlice.reducer

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
// 引入对应的方法
import { incrementAsync } from './store/features/counterSlice'

export default function App() {
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()
const [amount, setAmount] = useState(1)

return (
<div style={{ width: 500, margin: '100px auto' }}>
<input type="text" value={amount} onChange={e => setAmount(e.target.value)} />
<button onClick={() => dispatch(incrementAsync(Number(amount) || 0))}> Add Async </button>
<span>{count}</span>
</div>
)
}

image-20221031171739218

6.4 编写异步 Thunks

Thunk 内部可能有异步逻辑,例如 setTimeoutPromiseasync/await。这使它们成为使用 AJAX 发起 API 请求的好地方。

Redux 的数据请求逻辑通常遵循以下可预测的模式:

  • 在请求之前 dispatch 请求“开始”的 action,以指示请求正在进行中。这可用于跟踪加载状态以允许跳过重复请求或在 UI 中显示加载中提示。
  • 发出异步请求
  • 根据请求结果,异步逻辑 dispatch 包含结果数据的“成功” action 或包含错误详细信息的 “失败” action。在这两种情况下,reducer 逻辑都会清除加载状态,并且要么展示成功案例的结果数据,要么保存错误值并在需要的地方展示。

这些步骤不是 必需的,而是常用的。(如果你只关心一个成功的结果,你可以在请求完成时发送一个“成功” action ,并跳过“开始”和“失败” action 。)

Redux Toolkit 提供了一个 createAsyncThunk API 来实现这些 action 的创建和 dispatch,我们很快就会看看如何使用它。

如果我们手动编写一个典型的 async thunk 的代码,它可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const getRepoDetailsStarted = () => ({
type: 'repoDetails/fetchStarted'
})
const getRepoDetailsSuccess = repoDetails => ({
type: 'repoDetails/fetchSucceeded',
payload: repoDetails
})
const getRepoDetailsFailed = error => ({
type: 'repoDetails/fetchFailed',
error
})
const fetchIssuesCount = (org, repo) => async dispatch => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}

但是,使用这种方法编写代码很乏味。每个单独的请求类型都需要重复类似的实现:

  • 需要为三种不同的情况定义独特的 action 类型
  • 每种 action 类型通常都有相应的 action creator 功能
  • 必须编写一个 thunk 以正确的顺序发送正确的 action

createAsyncThunk 实现了这套模式:通过生成 action type 和 action creator 并生成一个自动 dispatch 这些 action 的 thunk。你提供一个回调函数来进行异步调用,并把结果数据返回成 Promise。

6.5 使用 createAsyncThunk 请求数据

Redux Toolkit 的 createAsyncThunk API 生成 thunk,为你自动 dispatch 那些 “start/success/failure” action。

让我们从添加一个 thunk 开始,该 thunk 将进行 AJAX 调用。

store/features/counterSlice.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// 请求电影列表
const reqMovieListApi = () =>
fetch(
'https://pcw-api.iqiyi.com/search/recommend/list?channel_id=1&data_type=1&mode=24&page_id=1&ret_num=48',
).then(res => res.json())

const initialState = {
status: 'idel',
list: [],
totals: 0,
}

// thunk函数允许执行异步逻辑, 通常用于发出异步请求。
// createAsyncThunk 创建一个异步action,方法触发的时候会有三种状态:
// pending(进行中)、fulfilled(成功)、rejected(失败)
export const getMovieData = createAsyncThunk('movie/getMovie', async () => {
const res = await reqMovieListApi()
return res.data
})

createAsyncThunk 接收 2 个参数:

  • 将用作生成的 action 类型的前缀的字符串
  • 一个 “payload creator” 回调函数,它应该返回一个包含一些数据的 Promise,或者一个被拒绝的带有错误的 Promise

Payload creator 通常会进行某种 AJAX 调用,并且可以直接从 AJAX 调用返回 Promise,或者从 API 响应中提取一些数据并返回。我们通常使用 JS async/await 语法来编写它,这让我们可以编写使用 Promise 的函数,同时使用标准的 try/catch 逻辑而不是 somePromise.then() 链式调用。

在这种情况下,我们传入 'movie/getMovie' 作为 action 类型的前缀。我们的 payload 创建回调等待 API 调用返回响应。响应对象的格式为 {data: []},我们希望我们 dispatch 的 Redux action 有一个 payload,也就是电影列表的数组。所以,我们提取 response.data,并从回调中返回它。

当调用 dispatch(getMovieData()) 的时候,getMovieData 这个 thunk 会首先 dispatch 一个 action 类型为'movie/getMovie/pending'

image-20221031180756586

我们可以在我们的 reducer 中监听这个 action 并将请求状态标记为 “loading 正在加载”。

一旦 Promise 成功,getMovieData thunk 会接受我们从回调中返回的 response.data ,并 dispatch 一个 action,action 的 payload 为 接口返回的数据(response.data ),action 的 类型为 'movie/getMovie/fulfilled'

image-20221031180934282****

6.6 使用 extraReducers

有时 slice 的 reducer 需要响应 没有 定义到该 slice 的 reducers 字段中的 action。这个时候就需要使用 slice 中的 extraReducers 字段。

extraReducers 选项是一个接收名为 builder 的参数的函数。builder 对象提供了一些方法,让我们可以定义额外的 case reducer,这些 reducer 将响应在 slice 之外定义的 action。我们将使用 builder.addCase(actionCreator, reducer) 来处理异步 thunk dispatch 的每个 action。

在这个例子中,我们需要监听我们 getMovieData thunk dispatch 的 “pending” 和 “fulfilled” action 类型。这些 action creator 附加到我们实际的 getMovieData 函数中,我们可以将它们传递给 extraReducers 来监听这些 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
const initialState = {
status: 'idel',
list: [],
totals: 0,
}

export const getMovieData = createAsyncThunk('movie/getMovie', async () => {
const res = await reqMovieListApi()
return res.data
})

// 创建一个 Slice
export const movieSlice = createSlice({
name: 'movie',
initialState,
// extraReducers 字段让 slice 处理在别处定义的 actions,
// 包括由 createAsyncThunk 或其他slice生成的actions。
extraReducers(builder) {
builder
.addCase(getMovieData.pending, state => {
console.log('🚀 ~ 进行中!')
state.status = 'pending'
})
.addCase(getMovieData.fulfilled, (state, action) => {
console.log('🚀 ~ fulfilled', action.payload)
state.status = 'pending'
state.list = state.list.concat(action.payload.list)
state.totals = action.payload.list.length
})
.addCase(getMovieData.rejected, (state, action) => {
console.log('🚀 ~ rejected', action)
state.status = 'pending'
state.error = action.error.message
})
},
})

// 默认导出
export default movieSlice.reducer

我们将根据返回的 Promise 处理可以由 thunk dispatch 的三种 action 类型:

  • 当请求开始时,我们将 status 枚举设置为 'pending'
  • 如果请求成功,我们将 status 标记为 'pending',并将获取的电影列表添加到 state.list
  • 如果请求失败,我们会将 status 标记为 'pending',并将任何错误消息保存到状态中以便我们可以显示它

6.7 完善案例

store/index.js

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

// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
counter: counterSlice,
movie: movieSlice,
},
})

export default store

Movie.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 引入相关的hooks
import { useSelector, useDispatch } from 'react-redux'
// 引入对应的方法
import { getMovieData } from '../store/features/movieSlice'
function Movie() {
// 通过useSelector直接拿到store中定义的list
const movieList = useSelector(store => store.movie.list)
// 通过useDispatch 派发事件
const dispatch = useDispatch()

return (
<div>
<button
onClick={() => {
dispatch(getMovieData())
}}
>
获取数据
</button>
<ul>
{movieList.map(movie => {
return <li key={movie.tvId}> {movie.name}</li>
})}
</ul>
</div>
)
}

export default Movie

image-20221031182248330

createAsyncThunk 可以写在任何一个slice的extraReducers中,它接收2个参数,

  • 生成actiontype值,这里type是要自己定义,不像是createSlice自动生成type,这就要注意避免命名冲突问题了(如果createSlice定义了相当的name和方法,也是会冲突的)
  • 包含数据处理的promise,首先会dispatch一个action类型为movie/getMovie/pending,当异步请求完成后,根据结果成功或是失败,决定dispatch出action的类型为movie/getMovie/fulfilledmovie/getMovie/rejected,这三个action可以在sliceextraReducers中进行处理。这个promise也只接收2个参数,分别是payload和包含了dispatchgetStatethunkAPI对象,所以除了在sliceextraReducers中处理之外,createAsyncThunk中也可以调用任意的action,这样就很像原本thunk的写法了,并不推荐

7.数据持久化

7.1 概念

一般是指页面刷新后,数据仍然能够保持原来的状态。

一般在前端当中,数据持久化,可以通过将数据存储到localstorage或Cookie中存起来,用到的时

候直接从本地存储中获取数据。而redux-persist是把redux中的数据在localstorage中存起来,起到持久化的效果。

7.2 使用

1
npm i redux-persist --save

store/index.js

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
import { configureStore, combineReducers } from '@reduxjs/toolkit'
// --- 新增 ---
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
// --- 新增 ---
import counterSlice from './features/counterSlice'
import movieSlice from './features/movieSlice'

// --- 新增 ---
const persistConfig = {
key: 'root',
storage,
// 指定哪些reducer数据持久化
whitelist: ['movie'],
}

const persistedReducer = persistReducer(
persistConfig,
combineReducers({
counter: counterSlice,
movie: movieSlice,
}),
)
// --- 新增 ---

// 这里照着我这样配中间件就行,getDefaultMiddleware不要直接导入了,已经内置了
export const store = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})

export const persistor = persistStore(store)

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import Movie from './components/Movie'
import { Provider } from 'react-redux'

import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './store'

ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Movie />
</PersistGate>
</Provider>,
)

然后就可以直接使用了。

最终效果:

image-20221105211826950

7.3 让每一个仓库单独存储

以前使用过pinia-plugin-persist,我觉得这个pinia这个插件使用比redux-persist方便

这里的方法是我自己想出来的,不知道对不对

store/index.js

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
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import counterSlice from './features/counterSlice'
import movieSlice from './features/movieSlice'

const rootPersistConfig = {
key: 'root',
storage,
whitelist: [],
}

const moviePersistConfig = {
key: 'movie',
storage,
}

const counterPersistConfig = {
key: 'counter',
storage,
}

const persistedReducer = persistReducer(
rootPersistConfig,
combineReducers({
counter: persistReducer(counterPersistConfig, counterSlice),
movie: persistReducer(moviePersistConfig, movieSlice),
}),
)

// configureStore创建一个redux数据
export const store = configureStore({
// 合并多个Slice
reducer: persistedReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
export const persistor = persistStore(store)

效果:

image-20221105212117068

19 【RTK Query】

1.目前前端常见的发起 ajax 请求的方式

  • 1、使用原生的ajax请求
  • 2、使用jquery封装好的ajax请求
  • 3、使用fetch发起请求
  • 4、第三方的比如axios请求
  • 5、angular中自带的HttpClient

就目前前端框架开发中来说我们在开发vuereact的时候一般都是使用fetchaxios自己封装一层来与后端数据交互,至于angular肯定是用自带的HttpClient请求方式,但是依然存在几个致命的弱点,

  • 1、对当前请求数据不能缓存,
  • 2、一个页面上由多个组件组成,但是刚好有遇到复用相同组件的时候,那么就会发起多次ajax请求

📢 针对同一个接口发起多次请求的解决方法,目前常见的解决方案

  • 1、使用axios的取消发起请求,参考文档
  • 2、vue中还没看到比较好的方法
  • 3、在rect中可以借用类似react-query 工具对请求包装一层
  • 4、对于angular中直接使用rxjs的操作符shareReplay

2.RTK Query 概述

RTK不仅帮助我们解决了state的问题,同时,它还为我们提供了RTK Query用来帮助我们处理数据加载的问题。RTK Query是一个强大的数据获取和缓存工具。在它的帮助下,Web应用中的加载变得十分简单,它使我们不再需要自己编写获取数据和缓存数据的逻辑。

rtk-queryredux-toolkit里面的一个分之,专门用来优化前端接口请求,目前也只支持在react中使用。

RTK Query 是一个强大的数据获取和缓存工具。它旨在简化在 Web 应用程序中加载数据的常见情况,无需自己手动编写数据获取和缓存逻辑

RTK Query 是一个包含在 Redux Toolkit 包中的可选插件,其功能构建在 Redux Toolkit 中的其他 API 之上。

Web应用中加载数据时需要处理的问题:

  1. 根据不同的加载状态显示不同UI组件
  2. 减少对相同数据重复发送请求
  3. 使用乐观更新,提升用户体验
  4. 在用户与UI交互时,管理缓存的生命周期

这些问题,RTKQ都可以帮助我们处理。首先,可以直接通过RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。其次,RTKQ在发送请求时会根据请求不同的状态返回不同的值,我们可以通过这些值来监视请求发送的过程并随时中止。

我们将 createAsyncThunkcreateSlice 一起使用,在发出请求和管理加载状态方面仍然需要进行大量手动工作。我们必须创建异步 thunk,发出实际请求,从响应中提取相关字段,添加加载状态字段,在 extraReducers 中添加处理程序以处理 pending/fulfilled/rejected 情况,并实际编写正确的状态更新。

在过去的几年里,React 社区已经意识到 “数据获取和缓存” 实际上是一组不同于 “状态管理” 的关注点。虽然你可以使用 Redux 之类的状态管理库来缓存数据,但用例差异较大,因此值得使用专门为数据获取用例构建的工具。

RTK Query 在其 API 设计中添加了独特的方法:

  • 数据获取和缓存逻辑构建在 Redux Toolkit 的 createSlicecreateAsyncThunk API 之上
  • 由于 Redux Toolkit 与 UI 无关,因此 RTK Query 的功能可以与任何 UI 层一起使用
  • API 请求接口是提前定义的,包括如何从参数生成查询参数和转换响应以进行缓存
  • RTK Query 还可以生成封装整个数据获取过程的 React hooks ,为组件提供 dataisFetching 字段,并在组件挂载和卸载时管理缓存数据的生命周期
  • RTK Query 提供“缓存数据项生命周期函数”选项,支持在获取初始数据后通过 websocket 消息流式传输缓存更新等用例
  • 我们有从 OpenAPI 和 GraphQL 模式生成 API slice 代码的早期工作示例
  • 最后,RTK Query 完全用 TypeScript 编写,旨在提供出色的 TS 使用体验

📢 rtk-query的使用环境,必须是react版本大于 17,可以使用hooks的版本,因为使用rtk-query的查询都是hooks的方式,如果你项目简单redux都未使用到,本人不建议你用rtk-query,可能直接使用axios请求更加的简单方便。

3.基础开发流程

后面这些案例后端接口返回格式都是

1
2
3
4
{
"code":200,
"data":[]
}
  • 创建一个store文件夹
  • 创建一个index.ts做为主入口
  • 创建一个festures/api文件夹用来装所有的API Slice
  • 创建一个sudentApiSlice.js文件,并导出简单的加减方法

3.1 定义 API Slice

使用 RTK Query,管理缓存数据的逻辑被集中到每个应用程序的单个“API Slice”中。就像每个应用程序只有一个 Redux 存储一样,我们现在有一个Slice 用于 所有 我们的缓存数据。

我们将从定义一个新的 sudentApiSlice.js 文件开始。由于这不是特定于我们已经编写的任何其他“功能”,我们将添加一个新的 features/api/ 文件夹并将 sudentApiSlice.js 放在那里。让我们填写 API 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
27
28
29
30
31
features/api/sudentApiSlice.js
// 从特定于 React 的入口点导入 RTK Query 方法
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'
// 上面这个引入的会自动创建钩子
// import { createApi } from '@reduxjs/toolkit/query'

// 定义我们的单个 API Slice 对象
//createApi() 用来创建RTKQ中的API对象
// RTKQ的所有功能都需要通过该对象来进行
// createApi() 需要一个对象作为参数
export const sudentApiSlice = createApi({
reducerPath: 'studentApi', // Api的标识,不能和其他的Api或reducer重复
// 指定查询的基础信息,发送请求使用的工具
baseQuery: fetchBaseQuery({
// 我们所有的请求都有以 “/api 开头的 URL
baseUrl: 'http://localhost:8080/api',
}),
// “endpoints” 代表对该服务器的操作和请求
endpoints: builder => ({
// `getStudents` endpoint 是一个返回数据的 “Query” 操作
getStudents: builder.query({
// 请求的 URL 是“/api/all/student”
query: () => '/all/student',
}),
}),
})

// Api对象创建后,对象中会根据各种方法自动的生成对应的钩子函数
// 通过这些钩子函数,可以来向服务器发送请求
// 钩子函数的命名规则 getStudents --> useGetStudentsQuery
export const { useGetStudentsQuery } = sudentApiSlice

上例是一个比较简单的Api对象的例子,我们来分析一下,首先我们需要调用createApi()来创建Api对象。这个方法在RTK中存在两个版本,一个位于@reduxjs/toolkit/dist/query下,一个位于@reduxjs/toolkit/dist/query/react下。react目录下的版本会自动生成一个钩子,方便我们使用Api。如果不要钩子,可以引入query下的版本,当然我不建议你这么做。

createApi()需要一个配置对象作为参数,配置对象中的属性繁多,我们暂时介绍案例中用到的属性:

reducerPath

用来设置reducer的唯一标识,主要用来在创建store时指定action的type属性,如果不指定默认为api。

baseQuery

用来设置发送请求的工具,就是你是用什么发请求,RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装,很方便,如果你不喜欢可以改用其他工具,这里暂时不做讨论。

fetchBaseQuery

简单封装过的fetch调用后会返回一个封装后的工具函数。需要一个配置对象作为参数,baseUrl表示Api请求的基本路径,指定后请求将会以该路径为基本路径。配置对象中其他属性暂不讨论。

endpoints

Api对象封装了一类功能,比如学生的增删改查,我们会统一封装到一个对象中。一类功能中的每一个具体功能我们可以称它是一个端点。endpoints用来对请求中的端点进行配置。

endpoints是一个回调函数,可以用普通方法的形式指定,也可以用箭头函数。回调函数中会收到一个build对象,使用build对象对点进行映射。回调函数的返回值是一个对象,Api对象中的所有端点都要在该对象中进行配置。

对象中属性名就是要实现的功能名,比如获取所有学生可以命名为getStudents,根据id获取学生可以命名为getStudentById。属性值要通过build对象创建,分两种情况:

查询:build.query({})

增删改:build.mutation({})

例如:

1
2
3
4
getStudents: builder.query({
// 请求的 URL 是“/api/all/student”
query: () => '/all/student',
}),

先说query,query也需要一个配置对象作为参数。配置对象里同样有n多个属性,现在直说一个,query方法。注意不要搞混两个query,一个是build的query方法,一个是query方法配置对象中的属性,这个方法需要返回一个子路径,这个子路径将会和baseUrl拼接为一个完整的请求路径。比如:getStudets的最终请求地址是:

1
http://localhost:8080/api + /all/student = http://localhost:8080/api/all/student

可算是介绍完了,但是注意了这个只是最基本的配置。RTKQ功能非常强大,但是配置也比较麻烦。不过,熟了就好了。

上例中,我们创建一个Api对象studentApi,并且在对象中定义了一个getStudents方法用来查询所有的学生信息。如果我们使用react下的createApi,则其创建的Api对象中会自动生成钩子函数,钩子函数名字为useXxxQuery或useXxxMutation,中间的Xxx就是方法名,查询方法的后缀为Query,修改方法的后缀为Mutation。所以上例中,Api对象中会自动生成一个名为useGetStudentsQuery的钩子,我们可以获取并将钩子向外部暴露。

1
export const {useGetStudentsQuery} = studentApi;

3.2 配置 Store

我们现在需要将 API Slice 连接到我们的 Redux 存储。我们可以修改现有的 store.js 文件,将 API slice 的 cache reducer 添加到状态中。此外,API slice 会生成需要添加到 store 的自定义 middleware。这个 middleware 必须 被添加——它管理缓存的生命周期和控制是否过期。

store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14

import { configureStore } from '@reduxjs/toolkit'
import { sudentApiSlice } from './features/api/sudentApiSlice'

// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
[sudentApiSlice.reducerPath]: sudentApiSlice.reducer,
},
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(sudentApiSlice.middleware),
})

export default store

我们可以在 reducer 参数中重用 sudentApiSlice.reducerPath 字段作为计算键,以确保在正确的位置添加缓存 reducer。

我们需要在 store 设置中保留所有现有的标准 middleware,例如“redux-thunk”,而 API slice 的 middleware 通常会在这些 middleware 之后使用。我们可以通过向 configureStore 提供 middleware 参数,调用提供的 getDefaultMiddleware() 方法,并在返回的 middleware 数组的末尾添加 sudentApiSlice.middleware 来做到这一点。

store创建完毕同样要设置Provider标签,这里不再展示。

3.3 在组件中使用 Query Hooks

接下来,我们来看看如果通过studentApi发送请求。由于我们已经将studentApi中的钩子函数向外部导出了,所以我们只需通过钩子函数即可自动加载到所有的学生信息。比如,现在在App.js中加载信息可以这样编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react'
import { useGetStudentsQuery } from './store/features/api/sudentApiSlice'

export default function App() {
const { data:studentsRes, isLoading, isSuccess, isError, error } = useGetStudentsQuery()

let content
if (isLoading) {
content = '正在加载中'
} else if (isSuccess) {
content = studentsRes.data.map(stu => (
<p key={stu._id}>
{stu.name} ---
{stu.age} ---
{stu.sex}
</p>
))
} else if (isError) {
content = error.toString()
}

return <div>{content}</div>
}

我们能够用对 useGetStudentsQuery() 的单个调用来替换多个 useSelector 调用和 useEffect 调度。

直接调用useGetStudentsQuery()它会自动向服务器发送请求加载数据,每个生成的 Query hooks 都会返回一个包含多个字段的“结果”对象,包括:

  1. data – 来自服务器的实际响应内容。 在收到响应之前,该字段将是 “undefined”
  2. currentData – 当前的数据
  3. isUninitialized – 如果为true则表示查询还没开始
  4. data:来自服务器的实际响应内容。 在收到响应之前,该字段将是 “undefined”
  5. isLoading: 一个 boolean,指示此 hooks 当前是否正在向服务器发出 第一次 请求。(请注意,如果参数更改以请求不同的数据,isLoading 将保持为 false。)
  6. isFetching: 一个 boolean,指示 hooks 当前是否正在向服务器发出 any 请求
  7. isSuccess: 一个 boolean,指示 hooks 是否已成功请求并有可用的缓存数据(即,现在应该定义 data)
  8. isError: 一个 boolean,指示最后一个请求是否有错误
  9. error: 一个 serialized 错误对象
  10. refetch 函数,用来重新加载数据

从结果对象中解构字段是很常见的,并且可能将 data 重命名为更具体的变量,例如 studentRes 来描述它包含的内容。然后我们可以使用状态 boolean 和 data/error 字段来呈现我们想要的 UI。 但是,如果你使用的是 TypeScript,你可能需要保持原始对象不变,并在条件检查中将标志引用为 result.isSuccess,以便 TS 可以正确推断 data 是有效的。

Snipaste_2022-11-04_22-57-20

这是最终效果:

image-20221104231936997

4.传递参数

4.1 定义接收参数

1
features/api/sudentApiSlice.js

这里定义了一个新的接口,通过id获取学生信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 从特定于 React 的入口点导入 RTK Query 方法
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'

export const sudentApiSlice = createApi({
reducerPath: 'studentApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
endpoints: builder => ({
getStudentById: builder.query({
// 从query方法这里接收参数
query: sutId => `/student/${sutId}`,
}),
}),
})

export const { useGetStudentsQuery, useGetStudentByIdQuery } = sudentApiSlice

4.2 传递参数

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import React from 'react'
import { useGetStudentByIdQuery } from './store/features/api/sudentApiSlice'

export default function App() {
let stuId = '63652d2c03155b63eea7b9f5'
const {
data: studentRes,
isLoading,
isSuccess,
isError,
error,
} = useGetStudentByIdQuery(stuId)

let content
if (isLoading) {
content = '正在加载中'
} else if (isSuccess) {
content = (
<p>
{studentRes.data.name} ---{studentRes.data.age} ---{studentRes.data.sex}
</p>
)
} else if (isError) {
content = error.toString()
}

return <div>{content}</div>
}

useGetPostQuery这个钩子接收的第一个参数传递到query方法,使用起来很简单

image-20221105135329712

5.转换响应

请求接口可以定义一个 transformResponse 处理程序,该处理程序可以在缓存之前提取或修改从服务器接收到的数据。我们可以有 transformResponse: (responseData) => responseData.data,它只会缓存实际的 student 对象,而不是整个响应体。

1
2
3
4
5
6
7
8
features/api/sudentApiSlice.js
getStudentById: builder.query({
query: sutId => `/student/${sutId}`,
transformResponse:(responseData, meta, arg)=>{
console.log(responseData);
return responseData.data
}
}),

对于上一个案例中通过id获取学生信息的接口,加一个transformResponse方法,我们来看看他接受到的参数responseData是什么

image-20221105140454626

可以看到responseData这个参数就是我们的响应体

在使用的过程中,发现这个方法类似于响应拦截器。

我们在App.jsx中看看useGetStudentByIdQuery这个钩子函数返回的data有什么变化

image-20221105140911763

6.RTK Query 缓存简单介绍

后面在介绍缓存的灵活使用

6.1 什么是相同查询

RTK Query 会将查询查询参数序列化为字符串,并将相同钩子、相同参数的查询视为相同的查询,他们将共享一个请求与缓存数据。

因此,下面两个调用返回结果相同(即使在不同的组件中):

1
2
3
useGetXXXQuery({ a: 1, b: 2 }) // 订阅 + 1
useGetXXXQuery({ b: 2, a: 1 }) // 订阅 + 1
// ...

这是因为:

  • 他们使用相同的查询:GetXXX
  • 查询参数的序列化结果相同:'{"a":1,"b":2}'

你不需要担心嵌套或是字段顺序,或是不同对象不同引用会被认为是不同的查询,因为 RTK Query 已经在默认的序列化函数中处理了相关用例。同时,你也可以提供自己的序列化函数。

6.2 引用计数与垃圾回收

当在组件中使用某个查询时,该查询的引用计数会 + 1,当该组件被卸载时,引用计数会 -1。当一个查询的引用计数为 0 时,说明没有任何组件在使用这个查询。此时,经过 keepUnusedDataFor(默认为 30 )秒后,如果缓存仍为被使用过,那么他将被从缓存中移除。

6.3 缓存初体验

缓存的配置

1
2
3
4
5
6
7
8
9
10
11
store/index.js
// configureStore创建一个redux数据
const store = configureStore({
// 合并多个Slice
reducer: {
[sudentApiSlice.reducerPath]: sudentApiSlice.reducer,
},
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(sudentApiSlice.middleware),
})

export default store

其实就是在这个store配置这个中间件,一开始就配好了。

来看看实际效果

先改写下上面的案例

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { useState } from 'react'
import StudentA from './StudentA'
import StudentB from './StudentB'

export default function App() {
const [tab, setTab] = useState(0)

let content
switch (tab) {
case 0:
content = '首页'
break
case 1:
content = <StudentA />
break
case 2:
content = <StudentB />
break
}

return (
<div>
<p>
<button onClick={() => setTab(0)}>首页</button>
<button onClick={() => setTab(1)}>学生A</button>
<button onClick={() => setTab(2)}>学生B</button>
</p>
{content}
</div>
)
}

StudentA.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react'
import { useEffect } from 'react'
import { useGetStudentByIdQuery } from './store/features/api/sudentApiSlice'

export default function Student() {
let stuId = '63652d2c03155b63eea7b9f5'
const { data: studentRes, isLoading, isSuccess, isError, error } = useGetStudentByIdQuery(stuId)

let content
if (isLoading) {
content = '正在加载中'
} else if (isSuccess) {
content = (
<p>
{studentRes.data.name} ---{studentRes.data.age} ---{studentRes.data.sex}
</p>
)
} else if (isError) {
content = error.toString()
}

useEffect(() => {
console.log('渲染了')
}, [])

return (
<>
<p>组件StudentA</p>
{content}
</>
)
}

StudentB.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react'
import { useEffect } from 'react'
import { useGetStudentByIdQuery } from './store/features/api/sudentApiSlice'

export default function Student() {
let stuId = '63652d2c03155b63eea7b9f5'
const { data: studentRes, isLoading, isSuccess, isError, error } = useGetStudentByIdQuery(stuId)

let content
if (isLoading) {
content = '正在加载中'
} else if (isSuccess) {
content = (
<p>
{studentRes.data.name} ---{studentRes.data.age} ---{studentRes.data.sex}
</p>
)
} else if (isError) {
content = error.toString()
}

useEffect(() => {
console.log('渲染了')
}, [])

return (
<>
<p>组件StudentB</p>
{content}
</>
)
}

我们把学生信息抽离成两个组件,里面除了标题都是一样的,在App.jsx中设置了个三个按钮控制显示隐藏

切换到StudentA组件

image-20221105151052394

切换到StudentB组件

image-20221105151112395

可以看到切换到StudentB组件并没有重新发起请求,这就是缓存生效了。

RTK Query 允许多个组件订阅相同的数据,并且将确保每个唯一的数据集只获取一次。 在内部,RTK Query 为每个请求接口 + 缓存键组合保留一个 action 订阅的引用计数器。如果组件 A 调用 useGetStudentByIdQuery(stuId),则将获取该数据。如果组件 B 挂载并调用 useGetStudentByIdQuery(stuId),则请求的数据完全相同。两种钩子用法将返回完全相同的结果,包括获取的 “data” 和加载状态标志。

当活跃订阅数下降到 0 时,RTK Query 会启动一个内部计时器。 如果在添加任何新的数据订阅之前计时器到期,RTK Query 将自动从缓存中删除该数据,因为应用程序不再需要该数据。但是,如果在计时器到期之前添加了新订阅,则取消计时器,并使用已缓存的数据而无需重新获取它。

在这种情况下,我们的 <StudentA> 挂载并通过 ID 请求。当我们“切换” 时,<StudentA> 组件被路由器卸载,并且活动订阅由于卸载而被删除。RTK Query 立即启动 “remove this post data” 计时器。但是,<StudentB> 组件会立即挂载并使用相同的缓存键订阅相同的 student 数据。因此,RTK Query 取消了计时器并继续使用相同的缓存数据,而不是从服务器获取数据。

默认情况下,未使用的数据会在 60 秒后从缓存中删除,但这可以在根 API Slice 定义中进行配置,也可以使用 keepUnusedDataFor 标志在各个请求接口定义中覆盖,该标志指定缓存生存期 秒。

features/api/sudentApiSlice.js

1
2
3
4
5
getStudentById: builder.query({
// 从query方法这里接收参数
query: sutId => `/student/${sutId}`,
keepUnusedDataFor: 60, // 设置数据缓存的时间,单位秒 默认60s
}),

7.mutation 请求接口

我们已经看到了如何通过定义查询请求接口从服务器获取数据,但是向服务器发送更新呢?

RTK Query 让我们定义 mutation 请求接口 来更新服务器上的数据。让我们添加一个可以让我们添加新学生的 Mutation。

7.1 添加新的 Mutations 后请求接口

添加 Mutation 请求接口与添加查询请求接口非常相似。 最大的不同是我们使用 builder.mutation() 而不是 builder.query() 来定义请求接口。 此外,我们现在需要将 HTTP 方法更改为“POST”请求,并且我们还必须提供请求的正文。

features/api/sudentApiSlice.js

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 { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'

export const sudentApiSlice = createApi({
reducerPath: 'studentApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
endpoints: builder => ({
getStudents: builder.query({
query: () => '/all/student',
}),
getStudentById: builder.query({
query: sutId => `/student/${sutId}`
}),
addNewStudent: builder.mutation({
query: student => ({
url: '/student',
method: 'POST',
// 将整个post对象作为请求的主体
body: student,
}),
}),
}),
})

export const { useGetStudentsQuery, useGetStudentByIdQuery, useAddNewStudentMutation } =
sudentApiSlice

这里我们的 query 选项返回一个包含 {url, method, body} 的对象。 由于我们使用 fetchBaseQuery 来发出请求,body 字段将自动为我们进行 JSON 序列化。

与查询请求接口一样,API slice 会自动为 Mutation 请求接口生成一个 React hooks - 在本例中为 useAddNewPostMutation

7.2 在组件中使用 Mutation Hooks

每当我们单击“添加”按钮时,我们以前得调度了一个异步 thunk 来添加。 为此,它必须导入 useDispatchaddNewPost thunk。 Mutation hooks 取代了这两者,并且使用模式非常相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, { useState } from 'react'
import { useAddNewStudentMutation } from './store/features/api/sudentApiSlice'

export default function Home() {
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const [sex, setSex] = useState('')

// 获取添加的钩子,useMutation的钩子返回的是一个数组
// 数组中有两个东西,第一个是操作的触发器,第二个是结果集
const [addNewStudent, { isLoading }] = useAddNewStudentMutation()

const canSubmit = [name, age, sex].every(() => true) && !isLoading

const onAddStuClicked = async () => {
if (!canSubmit) return
try {
await addNewStudent({ name, age, sex }).unwrap()
setName('')
setAge(0)
setSex('')
} catch (err) {
console.error('Failed to add student: ', err)
}
}

return (
<div>
<h2>首页</h2>
<p>
<button onClick={onAddStuClicked}>添加学生</button>
</p>
<form>
姓名:
<input type="text" value={name} onChange={e => setName(e.target.value)} /> <br />
年龄:
<input type="number" value={age} onChange={e => setAge(+e.target.value)} /> <br />
性别:
<input type="text" value={sex} onChange={e => setSex(e.target.value)} />
</form>
</div>
)
}

Mutation hooks 返回一个包含两个值的数组:

  • 第一个值是触发函数。当被调用时,它会使用你提供的任何参数向服务器发出请求。这实际上就像一个已经被包装以立即调度自身的 thunk。
  • 第二个值是一个对象,其中包含有关当前正在进行的请求(如果有)的元数据。这包括一个 isLoading 标志以指示请求是否正在进行中。

我们可以用 useAddNewStudentMutation hooks 中的触发函数和 isLoading 标志替换现有的 thunk 调度和组件加载状态,组件的其余部分保持不变。

与 thunk 调度一样,我们使用初始 post 对象调用 addNewStudent。 这会返回一个带有.unwrap()方法的特殊 Promise ,我们可以 await addNewStudent().unwrap() 使用标准的 try/catch 块来处理任何潜在的错误。

image-20221105193209910

8.useQuery Hook 参数

查询钩子需要两个参数:(queryArg?, queryOptions?)

参数将被传递到底层回调以生成URL。queryArg query

该对象接受几个可用于控制数据获取行为的附加参数:queryOptions

  • skip - 允许查询“跳过”为该渲染运行。默认为false
  • pollingInterval - 允许查询按提供的时间间隔(以毫秒为单位指定)自动重新获取。默认为(关闭)0
  • selectFromResult - 允许更改钩子的返回值以获取结果的子集,针对返回的子集进行渲染优化。
  • refetchOnMountOrArgChange - 允许强制查询始终在挂载时重新取回迁(何时提供)。允许在自上次查询同一缓存(当设置为true)以来已经过去了足够的时间(以秒为单位)时强制查询重新获取。默认为true number false
  • refetchOnFocus - 允许在浏览器窗口重新获得焦点时强制查询重新获取。默认为false
  • refetchOnReconnect - 允许在重新获得网络连接时强制查询重新获取。默认为false

8.1 条件提取

默认为false。一旦挂载组件,查询钩子就会自动开始获取数据。但是,在某些用例中,您可能希望延迟获取数据,直到某些条件变为真。RTK 查询支持条件提取以启用该行为。

如果要阻止查询自动运行,可以在钩子中使用参数skip

跳过示例

1
2
3
4
5
6
7
8
9
10
11
const Pokemon = ({ name, skip }) => {
const { data, error, status } = useGetPokemonByNameQuery(name, {
skip,
});

return (
<div>
{name} - {status}
</div>
);
};
  • 如果查询缓存了数据:
    • 缓存的数据将不会在初始加载时使用,并且将忽略来自任何相同查询的更新,直到删除条件skip
    • 查询的状态为uninitialized
    • 初始加载后设置的 ifis,将使用缓存结果skip: false
  • 如果查询没有缓存数据:
    • 查询的状态为uninitialized
    • 使用开发工具查看查询时,查询将不存在于该状态
    • 查询不会在装载时自动获取
    • 当添加具有相同查询的其他组件时,查询不会自动运行

这里我想演示的例子是如果我们给钩子函数传递的参数是一个undefined,这个时候发起请求是会报错的,我们可以使用skip来来跳过这次无法进行的请求。

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react'
import {useGetStudentByIdQuery} from "./store/features/api/sudentApiSlice"

const StudentForm = (props) => {
// 调用钩子来加载数据
const {data:stuData, isSuccess, isFetching} = useGetStudentByIdQuery(props.stuId, {
skip:!props.stuId
})
...
}

export default StudentForm;

这里如果父组件传过来的stuId是个undefined,那么这次就不会发起请求了。

8.2 轮询

默认值为0。轮询使您能够通过使查询按指定的时间间隔运行来产生“实时”效果。若要为查询启用轮询,请以毫秒为单位的间隔将值传递给钩子

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'

export const Pokemon = ({ name }: { name: string }) => {
// 每过3s会自动调用一次这个钩子函数
const { data, status, error, refetch } = useGetPokemonByNameQuery(name, {
pollingInterval: 3000,
})

return <div>{data}</div>
}

8.3 从查询结果中选择数据

selectFromResult允许您以高性能方式从查询结果中获取特定段。使用此功能时,除非所选项的基础数据已更改,否则组件不会重新呈现。如果所选项是较大集合中的一个元素,它将忽略对同一集合中元素的更改。

AllStudent.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react'
import { useGetStudentsQuery } from './store/features/api/sudentApiSlice'

export default function App() {
const {
data: studentsRes,
isLoading,
isSuccess,
isError,
error,
} = useGetStudentsQuery(null, {
selectFromResult: result => {
console.log(result)
return result
},
})

let content
if (isLoading) {
content = '正在加载中'
} else if (isSuccess) {
content = studentsRes.data.map(stu => (
<p key={stu._id}>
{stu.name} ---
{stu.age} ---
{stu.sex}
</p>
))
} else if (isError) {
content = error.toString()
}

return <div>{content}</div>
}

先看这个selectFromResult方法的参数是什么

image-20221105195834361

这里我们可以对学生数据进行过滤

1
2
3
4
5
6
7
8
9
10
selectFromResult: result => {
let res = result.data
if (res) {
result.data = {
...res,
data: res.data.filter(stu => stu.age > 20),
}
}
return result
},

image-20221105201840326

8.4 refetchOnMountOrArgChange

默认为false。此设置允许您控制缓存结果是否已经可用 RTK 查询将仅提供缓存的结果,或者是否应该设置为 或 自上次成功查询结果以来已经过去了足够的时间。

  • false- 除非查询尚不存在,否则不会导致执行查询。
  • true- 在添加查询的新订阅者时,将始终重新获取。行为与调用回调或传入操作创建者相同。
  • number - 值以秒为单位。如果提供了一个数字,并且缓存中存在现有查询,它将比较当前时间与上次实现的时间戳,并且仅在经过足够时间时才重新获取。

如果同时指定此选项skip: true,则在 false 之前不会对其进行计算

1
2
3
4
5
6
7
8
9
const {
data: studentsRes,
isLoading,
isSuccess,
isError,
error,
} = useGetStudentsQuery(null, {
refetchOnMountOrArgChange:false
})

注意

fetchBaseQuery |Redux Toolkit (redux-toolkit.js.org) 您可以在createApi中全局设置此项refetchOnMountOrArgChange,但您也可以覆盖默认值,并通过传递给每个单独的钩子调用或类似地通过 passingwhen 调度启动open in new window操作来获得更精细的控制。createApi

8.5 refetchOnFocus

默认值为false。此设置允许您控制 RTK 查询是否在应用程序窗口重新获得焦点后尝试重新获取所有订阅的查询。

如果同时指定此选项skip: true,则在 false 之前不会对其进行计算

注意:要求已调用安装程序侦听器

注意

fetchBaseQuery |Redux Toolkit (redux-toolkit.js.org)

您可以在createApi中全局设置中此项refetchOnFocus,但也可以覆盖默认值,并通过传递给每个单独的钩子调用或在调度启动open in new window操作时进行更精细的控制。

如果您指定手动分派查询的时间,RTK 查询将无法自动为您重新获取。

想使用还得为store添加一个配置才行

store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'

// configureStore创建一个redux数据
const store = configureStore({
...
})

// 设置以后,将会支持 refetchOnFocus refetchOnReconnect
setupListeners(store.dispatch)

export default store

然后我们看下效果

image-20221105203424540

devtool回来点一下网页会重新发一次请求,然后从别的网站点回来也会重新发起请求。

8.6 refetchOnReconnect

默认值为false,此设置允许您控制 RTK 查询在重新获得网络连接后是否尝试重新获取所有订阅的查询。

如果同时指定此选项skip: true,则在 false 之前不会对其进行计算

注意:要求已调用安装程序侦听器

注意

您可以在createApi中全局设置此项refetchOnReconnect,但也可以覆盖默认值,并通过传递给每个单独的钩子调用或在调度启动操作时进行更精细的控制。

如果您指定手动分派查询的时间,RTK 查询将无法自动为您重新获取。track: false

9.刷新缓存数据

当我们点击添加学生时,我们可以在浏览器 DevTools 中查看 Network 选项卡,确认 HTTP POST 请求成功。 但是,如果我们回到所有学生组件,新的学生信息并不会被展示出来。我们在内存中仍然有相同的缓存数据。

我们需要告诉 RTK Query 刷新其缓存的学生列表,以便我们可以看到我们刚刚添加的新学生信息。

9.1 手动刷新

第一个选项是手动强制 RTK Query 重新获取给定请求接口的数据。Query hooks 结果对象包含一个 “refetch” 函数,我们可以调用它来强制重新获取。 我们可以暂时将“重新获取学生列表”按钮添加到<AllStudent>,并在添加新学生后单击该按钮。

AllStudent.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react'
import { useGetStudentsQuery } from './store/features/api/sudentApiSlice'

export default function App() {
const {
data: studentsRes,
isLoading,
isSuccess,
isError,
error,
refetch,
} = useGetStudentsQuery()

let content
if (isLoading) {
content = '正在加载中'
} else if (isSuccess) {
content = studentsRes.data.map(stu => (
<p key={stu._id}>
{stu.name} ---
{stu.age} ---
{stu.sex}
</p>
))
} else if (isError) {
content = error.toString()
}

return (
<div>
<p>
<button onClick={refetch}>重新获取学生列表</button>
</p>
{content}
</div>
)
}

首先先从首页添加一个学生数据,然后回到所有学生组件

image-20221106161045089

这个时候由于有缓存,用的还是之前的数据,我们使用reFetch方法来强制刷新数据

image-20221106161736244

9.2 缓存失效自动刷新-数据标签

有时需要让用户手动单击以重新获取数据,但对于正常使用而言绝对不是一个好的解决方案。

我们知道我们的服务器拥有所有帖子的完整列表,包括我们刚刚添加的帖子。 理想情况下,我们希望我们的应用程序在 Mutation 请求完成后自动重新获取更新的帖子列表。 这样我们就知道我们的客户端缓存数据与服务器所拥有的数据是同步的。

RTK Query 让我们定义查询和 mutations 之间的关系,以启用自动数据重新获取,使用标签。标签是一个字符串或小对象,可让你命名某些类型的数据和缓存的 无效 部分。当缓存标签失效时,RTK Query 将自动重新获取标记有该标签的请求接口。

基本标签使用需要向我们的 API slice 添加三条信息:

  • API slice 对象中的根 tagTypes 字段,声明数据类型的字符串标签名称数组,例如 'student'
  • 查询请求接口中的 “providesTags” 数组,列出了一组描述该查询中数据的标签
  • Mutation 请求接口中的“invalidatesTags”数组,列出了每次 Mutation 运行时失效的一组标签

我们可以在 API slice 中添加一个名为 'student' 的标签,让我们在添加新帖子时自动重新获取 getStudents 请求接口:

features/api/sudentApiSlice.js

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 { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'

export const sudentApiSlice = createApi({
reducerPath: 'studentApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
tagTypes: ['student'],
endpoints: builder => ({
getStudents: builder.query({
query: () => '/all/student',
providesTags: [{ type: 'student', id: 'LIST' }],
}),
addNewStudent: builder.mutation({
query: student => ({
url: '/student',
method: 'POST',
// 将整个post对象作为请求的主体
body: student,
}),
invalidatesTags: [{ type: 'student', id: 'LIST' }],
}),
}),
})

export const { useGetStudentsQuery,useAddNewStudentMutation } = sudentApiSlice

这就是我们所需要的! 现在,如果我们单击添加学生,然后回到AllStudent组件重新发起请求,渲染新的数据

请注意,这里的文字字符串 'student' 没有什么特别之处。 我们可以称它为“Fred”、“qwerty”或其他任何名称。 它只需要在每个字段中使用相同的字符串,以便 RTK Query 知道“当发生这种 Mutation 时,使列出相同标签字符串的所有请求接口无效”。

10.RTKQ 结合 Axios

先来看看一个简单的案例:

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
import React from "react"
import ReactDOM from "react-dom/client"
import { Provider } from "react-redux"
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"
import { configureStore } from "@reduxjs/toolkit"
import { setupListeners } from "@reduxjs/toolkit/dist/query"

const productApi = createApi({
reducerPath: "productApi",
baseQuery: fetchBaseQuery({
baseUrl:
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store",
}),
endpoints(build) {
return {
getProducts: build.query({
query() {
return {
url: "/products.json",
}
},
}),
}
},
})

const { useGetProductsQuery } = productApi

const store = configureStore({
reducer: {
[productApi.reducerPath]: productApi.reducer,
},

middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(productApi.middleware),
})

setupListeners(store.dispatch)

const App = () => {
const { data, isSuccess } = useGetProductsQuery()

return (
<div>
App
<hr />
{isSuccess && JSON.stringify(data)}
</div>
)
}

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

上例中productApi用来调用product数据,定义api时的baseQuery属性用来指定我们要使用的发送请求的工具,其中的fetchBaseQuery是RTKQ中为我们提供的工具,它对Fetch进行了包装,设置后RTKQ中将会使用Fetch做为发送请求的工具。

10.1 BaseQuery

要设置通过Axios发送请求,关键就在于BaseQuery。只需要使用Axios的BaseQuery替换掉fetchBaseQuery即可。但是可惜的是RTKQ中并没有为我们提供Axios的BaseQuery,所以我们需要自定义一个BaseQuery才能达到目的。

BaseQuery本身就是一个函数,定义BaseQuery直接定义一个函数即可,可以通过函数的参数来指定查询中要使用的默认参数,比如baseUrl,参数可以根据自己的实际需要指定:

1
2
3
const myBaseQuery = ({baseUrl} = {baseUrl:""}) => {

}

BaseQuery需要一个函数作为返回值,这个函数将会成为最终的发送请求的工具,且函数的返回值将会作为执行结果返回。我们可以将发送请求的逻辑编写到函数中,并且根据不同的情况设置返回值。

先看看返回值的格式,返回值的格式有两种,一种是请求成功返回的数据,一种是请求失败返回的数据:

1
2
return { data: YourData } // 请求成功返回的数据
return { error: YourError } // 请求失败返回的数据

我们先尝试定义一个简单的BaseQuery:

1
2
3
4
5
6
7
8
9
10
11
12
13
const myBaseQuery = () => {
return () => {
if(Math.random() > .5){
return {
data:{name:"孙悟空"}
}
}else{
return {
error:{message:"出错了"}
}
}
}
}

这个BaseQuery不会真的去加载数据,而是根据随机数返回不同的数据。随机数大于0.5时会返回成功的数据,否则返回错误的数据。接下来修改Api的代码,将fetchBaseQuery修改为,myBaseQuery:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const productApi = createApi({
reducerPath: "productApi",
baseQuery: myBaseQuery(),
endpoints(build) {
return {
getProducts: build.query({
query() {
return {
url: "/products.json",
}
},
}),
}
},
})

10.2 AxiosBaseQuery

如果你能理解myBaseQuery,下边我们尝试编写一个axiosBaseQuery:

1
2
3
4
5
6
7
8
9
10
const axiosBaseQuery = ({baseUrl} = {baseUrl:""}) => {
return ({url, method, data, params}) => {
return axios({
url: baseUrl + url,
method,
data,
params
})
}
}

直接使用axiosBaseQuery替换掉之前的BaseQuery,即可在RTKQ中使用Axios来发送请求了,同时我们也可以根据需要在BaseQuery中对axios做一些更详细的配置。

11.小总结

  • RTK Query 是 Redux Toolkit 中包含的数据获取和缓存解决方案

    • RTK Query 为你抽象了管理缓存服务器数据的过程,无需编写加载状态、存储结果和发出请求的逻辑
    • RTK Query 建立在 Redux 中使用的相同模式之上,例如异步 thunk
  • RTK Query 对每个应用程序使用单个 “API slice”,使用

    1
    createApi

    定义

    • RTK Query 提供与 UI 无关和特定于 React 的 createApi 版本
    • API slice 为不同的服务器操作定义了多个“请求接口”
    • 如果使用 React 集成,API slice 包括自动生成的 React hooks
  • 查询请求接口允许从服务器获取和缓存数据

    • Query Hooks 返回一个 “data” 值,以及加载状态标志
    • 查询可以手动重新获取,或者使用标签自动重新获取缓存失效
  • Mutation 请求接口允许更新服务器上的数据

    • Mutation hooks 返回一个发送更新请求的“触发”函数,以及加载状态
    • 触发函数返回一个可以解包并等待的 Promise

20 【react中使用ts】

官方文档:React TypeScript Cheatsheets | React TypeScript Cheatsheets (react-typescript-cheatsheet.netlify.app)

1.创建一个组件

下面我们将要创建一个Hello组件。 这个组件接收任意一个我们想对之打招呼的名字(我们把它叫做name),并且有一个可选数量的感叹号做为结尾(通过enthusiasmLevel)。

若我们这样写<Hello name="Daniel" enthusiasmLevel={3} />,这个组件大至会渲染成<div>Hello Daniel!!!</div>。 如果没指定enthusiasmLevel,组件将默认显示一个感叹号。 若enthusiasmLevel0或负值将抛出一个错误。

下面来写一下Hello.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/components/Hello.tsx

import * as React from 'react';

export interface Props {
name: string;
enthusiasmLevel?: number;
}

function Hello({ name, enthusiasmLevel = 1 }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}

return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}

export default Hello;

// helpers

function getExclamationMarks(numChars: number) {
return Array(numChars + 1).join('!');
}

注意我们定义了一个类型Props,它指定了我们组件要用到的属性。 name是必需的且为string类型,同时enthusiasmLevel是可选的且为number类型(你可以通过名字后面加?为指定可选参数)。

我们创建了一个无状态的函数式组件(Stateless Functional Components,SFC)Hello。 具体来讲,Hello是一个函数,接收一个Props对象并拆解它。 如果Props对象里没有设置enthusiasmLevel,默认值为1

使用函数是React中定义组件的两种方式 之一。 如果你喜欢的话,也可以通过类的方式定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Hello extends React.Component<Props, object> {
render() {
const { name, enthusiasmLevel = 1 } = this.props;

if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}

return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}
}

当我们的组件具有某些状态 的时候,使用类的方式是很有用处的。 但在这个例子里我们不需要考虑状态 - 事实上,在React.Component<Props, object>我们把状态指定为了object,因此使用SFC更简洁。 当在创建可重用的通用UI组件的时候,在表现层使用组件局部状态比较适合。 针对我们应用的生命周期,我们会审视应用是如何通过Redux轻松地管理普通状态的。

现在我们已经写好了组件,让我们仔细看看index.tsx,把<App />替换成<Hello ... />

首先我们在文件头部导入它:

1
import Hello from './components/Hello.tsx';

然后修改render调用:

1
2
3
4
ReactDOM.render(
<Hello name="TypeScript" enthusiasmLevel={10} />,
document.getElementById('root') as HTMLElement
);

这里还有一点要指出,就是最后一行document.getElementById('root') as HTMLElement。 这个语法叫做类型断言,有时也叫做转换。 当你比类型检查器更清楚一个表达式的类型的时候,你可以通过这种方式通知TypeScript。

这里,我们之所以这么做是因为getElementById的返回值类型是HTMLElement | null。 简单地说,getElementById返回null是当无法找对对应id元素的时候。 我们假设getElementById总是成功的,因此我们要使用as语法告诉TypeScript这点。

TypeScript还有一种感叹号(!)结尾的语法,它会从前面的表达式里移除nullundefined。 所以我们也可以写成document.getElementById('root')!,但在这里我们想写的更清楚些。

2.React中内置函数

React中内置函数由很多,我们就挑几个常用的来学习一下。

2.1 React.FC< P >

React.FC<>是函数式组件在TypeScript使用的一个泛型,FC就是FunctionComponent的缩写,事实上React.FC可以写成React.FunctionComponent。

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
import React from 'react';

interface demo1PropsInterface {
attr1: string,
attr2 ?: string,
attr3 ?: 'w' | 'ww' | 'ww'
};

// 函数组件,其也是类型别名
// type FC<P = {}> = FunctionComponent<P>;
// FunctionComponent<T>是一个接口,里面包含其函数定义和对应返回的属性
// interface FunctionComponent<P = {}> {
// // 接口可以表示函数类型,通过给接口定义一个调用签名实现
// (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
// propTypes?: WeakValidationMap<P> | undefined;
// contextTypes?: ValidationMap<any> | undefined;
// defaultProps?: Partial<P> | undefined;
// displayName?: string | undefined;
// }
const Demo1: React.FC<demo1PropsInterface> = ({
attr1,
attr2,
attr3
}) => {
return (
<div>hello demo1 {attr1}</div>
);
};

export default Demo1;

2.2 React.Component< P, S >

React.Component< P, S > 是定义class组件的一个泛型,第一个参数是props、第二个参数是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
import React from "react";

// props的接口
interface demo2PropsInterface {
props1: string
};

// state的接口
interface demo2StateInterface {
state1: string
};

class Demo2 extends React.Component<demo2PropsInterface, demo2StateInterface> {
constructor(props: demo2PropsInterface) {
super(props);
this.state = {
state1: 'state1'
}
}

render() {
return (
<div>{this.state.state1 + this.props.props1}</div>
);
}
}

export default Demo2;

2.3 React.Reducer<S, A>

useState的替代方案,接收一个形如(state, action) => newState的reducer,并返回当前state以及其配套的dispatch方法。语法如下所示:const [state, dispatch] = useReducer(reducer, initialArg, init);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, {useReducer, useContext} from "react";

interface stateInterface {
count: number
};

interface actionInterface {
type: string,
data: {
[propName: string]: any
}
};

const initialState = {
count: 0
};

// React.Reducer其实是类型别名,其实质上是type Reducer<S, A> = (prevState: S, action: A) => S;
// 因为reducer是一个函数,其接受两个泛型参数S和A,返回S类型
const reducer: React.Reducer<stateInterface, actionInterface> = (state, action) => {
const {type, data} = action;
switch (type) {
case 'increment': {
return {
...state,
count: state.count + data.count
};
}
case 'decrement': {
return {
...state,
count: state.count - data.count
};
}
default: {
return state;
}
}
}

2.4 React.Context<T>

  1. React.createContext

其会创建一个Context对象,当React渲染一个订阅了这个Context对象的组件,这个组件会从组件树中离自身最近的那个匹配的Provider中读取到当前的context值。【注:只要当组件所处的树没有匹配到Provider时,其defaultValue参数参会生效】

1
2
3
4
5
6
7
8
9
const MyContext = React.createContext(defaultValue);

const Demo = () => {
return (
// 注:每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化。
<MyContext.Provider value={xxxxxx}>
// ……
</MyContext.Provider>
);
  1. useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。语法如下所示:const value = useContext(MyContext);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, {useContext} from "react";
const MyContext = React.createContext('');

const Demo3Child: React.FC<{}> = () => {
const context = useContext(MyContext);
return (
<div>
{context}
</div>
);
}

const Demo3: React.FC<{}> = () => {

return (
<MyContext.Provider value={'222222'}>
<MyContext.Provider value={'33333'}>
<Demo3Child />
</MyContext.Provider>
</MyContext.Provider>
);
};
  1. 使用
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import React, {useReducer, useContext} from "react";

interface stateInterface {
count: number
};

interface actionInterface {
type: string,
data: {
[propName: string]: any
}
};

const initialState = {
count: 0
};

const reducer: React.Reducer<stateInterface, actionInterface> = (state, action) => {
const {type, data} = action;
switch (type) {
case 'increment': {
return {
...state,
count: state.count + data.count
};
}
case 'decrement': {
return {
...state,
count: state.count - data.count
};
}
default: {
return state;
}
}
}

// React.createContext返回的是一个对象,对象接口用接口表示
// 传入的为泛型参数,作为整个接口的一个参数
// interface Context<T> {
// Provider: Provider<T>;
// Consumer: Consumer<T>;
// displayName?: string | undefined;
// }
const MyContext: React.Context<{
state: stateInterface,
dispatch ?: React.Dispatch<actionInterface>
}> = React.createContext({
state: initialState
});

const Demo3Child: React.FC<{}> = () => {
const {state, dispatch} = useContext(MyContext);
const handleClick = () => {
if (dispatch) {
dispatch({
type: 'increment',
data: {
count: 10
}
})
}
};
return (
<div>
{state.count}
<button onClick={handleClick}>增加</button>
</div>
);
}

const Demo3: React.FC<{}> = () => {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<MyContext.Provider value={{state, dispatch}}>
<Demo3Child />
</MyContext.Provider>
);
};

export default Demo3;

3.React中事件处理函数

React中的事件是我们在编码中经常用的,例如onClick、onChange、onMouseMove等,那么应该如何用呢?

3.1 不带event参数

当对应的事件处理函数不带event参数时,这个时候用起来很简单,如下所示:

1
2
3
4
5
6
7
8
9
10
const Test: React.FC<{}> = () => {
const handleClick = () => {
// 做一系列处理
};
return (
<div>
<button onClick={handleClick}>按钮</button>
</div>
);
};

3.2 带event参数

  1. 带上event参数,报错
1
2
3
4
5
6
7
8
9
10
11
12
const Test: React.FC<{}> = () => {
// 报错了,注意不要这么写……
const handleClick = event => {
// 做一系列处理
event.preventDefault();
};
return (
<div>
<button onClick={handleClick}>按钮</button>
</div>
);
};
  1. 点击onClick参数,跳转到index.d.ts文件
1
2
3
4
5
6
7
8
9
10
11
12
// onClick是MouseEventHandler类型
onClick?: MouseEventHandler<T> | undefined;

// 那MouseEventHandler<T>又是啥?原来是个类型别名,泛型是Element类型
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;

// 那么泛型Element又是什么呢?其是一个接口,通过继承该接口实现了很多其它接口
interface Element { }
interface HTMLElement extends Element { }
interface HTMLButtonElement extends HTMLElement { }
interface HTMLInputElement extends HTMLElement { }
// ……

至此,就知道该位置应该怎么实现了

1
2
3
4
5
6
7
8
9
10
11
const Test: React.FC<{}> = () => {
const handleClick: React.MouseEventHandler<HTMLButtonElement> = event => {
// 做一系列处理
event.preventDefault();
};
return (
<div>
<button onClick={handleClick}>按钮</button>
</div>
);
};

3.3 表单事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 如果不考虑性能的话,可以使用内联处理,注解将自动正确生成
const el = (
<button onClick=(e=>{
//...
})/>
)
// 如果需要在外部定义类型
handlerChange = (e: React.FormEvent<HTMLInputElement>): void => {
//
}
// 如果在=号的左边进行注解
handlerChange: React.ChangeEventHandler<HTMLInputElement> = e => {
//
}
// 如果在form里onSubmit的事件,React.SyntheticEvent,如果有自定义类型,可以使用类型断言
<form
ref={formRef}
onSubmit={(e: React.SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
email: { value: string };
password: { value: string };
};
const email = target.email.value; // typechecks!
// etc...
}}
>
<div>
<label>
Email:
<input type="email" name="email" />
</label>
</div>
<div>
<input type="submit" value="Log in" />
</div>
</form>

3.4 事件类型列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AnimationEvent : css动画事件
ChangeEvent:<input>, <select>和<textarea>元素的change事件
ClipboardEvent: 复制,粘贴,剪切事件
CompositionEvent:由于用户间接输入文本而发生的事件(例如,根据浏览器和PC的设置,如果你想在美国键盘上输入日文,可能会出现一个带有额外字符的弹出窗口)
DragEvent:在设备上拖放和交互的事件
FocusEvent: 元素获得焦点的事件
FormEvent: 当表单元素得失焦点/value改变/表单提交的事件
InvalidEvent: 当输入的有效性限制失败时触发(例如<input type="number" max="10">,有人将插入数字20)
KeyboardEvent: 键盘键入事件
MouseEvent: 鼠标移动事件
PointerEvent: 鼠标、笔/触控笔、触摸屏)的交互而发生的事件
TouchEvent: 用户与触摸设备交互而发生的事件
TransitionEvent: CSS Transition,浏览器支持度不高
UIEvent:鼠标、触摸和指针事件的基础事件。
WheelEvent: 在鼠标滚轮或类似的输入设备上滚动
SyntheticEvent:所有上述事件的基础事件。是否应该在不确定事件类型时使用
// 因为InputEvent在各个浏览器支持度不一样,所以可以使用KeyboardEvent代替

4.普通函数

  1. 一个具体类型的输入输出函数
1
2
3
4
// 参数输入为number类型,通过类型判断直接知道输出也为number
function testFun1 (count: number) {
return count * 2;
}
  1. 一个不确定类型的输入、输出函数,但是输入、输出函数类型一致
1
2
3
4
// 用泛型
function testFun2<T> (arg: T): T {
return arg;
}
  1. async函数,返回的为Promise对象,可以使用then方法添加回调函数,Promise是一个泛型函数,T泛型变量用于确定then方法时接收的第一个回调函数的参数类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用接口
interface PResponse<T> {
result: T,
status: string
};

// 除了用接口外,还可以用对象
// type PResponse<T> = {
// result: T,
// status: string
// };

async function testFun3(): Promise<PResponse<number>> {
return {
status: 'success',
result: 10
}
}

5.React Prop 类型

  • 如果你有配置 Eslint 等一些代码检查时,一般函数组件需要你定义返回的类型,或传入一些 React 相关的类型属性。 这时了解一些 React 自定义暴露出的类型就很有必要了。例如常用的 React.ReactNode
1
2
3
4
5
6
7
8
9
10
11
12
export declare interface AppProps {
children1: JSX.Element; // ❌ bad, 没有考虑数组类型
children2: JSX.Element | JSX.Element[]; // ❌ 没考虑字符类型
children3: React.ReactChildren; // ❌ 名字唬人,工具类型,慎用
children4: React.ReactChild[]; // better, 但没考虑 null
children: React.ReactNode; // ✅ best, 最佳接收所有 children 类型
functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点

style?: React.CSSProperties; // React style

onChange?: React.FormEventHandler<HTMLInputElement>; // 表单事件! 泛型参数即 `event.target` 的类型
}

defaultProps 默认值问题。

1
2
3
4
5
6
7
8
9
type GreetProps = { age: number } & typeof defaultProps;
const defaultProps = {
age: 21,
};

const Greet = (props: GreetProps) => {
// etc
};
Greet.defaultProps = defaultProps;
  • 你可能不需要 defaultProps
1
2
3
4
5
type GreetProps = { age?: number };

const Greet = ({ age = 21 }: GreetProps) => {
// etc
};

6.Hooks

项目基本上都是使用函数式组件和 React Hooks。 接下来介绍常用的用 TS 编写 Hooks 的方法。

6.1 useState

  • 给定初始化值情况下可以直接使用
1
2
3
4
5
import { useState } from 'react';
// ...
const [val, toggle] = useState(false);
// val 被推断为 boolean 类型
// toggle 只能处理 boolean 类型
  • 没有初始值(undefined)或初始 null
1
2
3
4
5
6
type AppProps = { message: string };
const App = () => {
const [data] = useState<AppProps | null>(null);
// const [data] = useState<AppProps | undefined>();
return <div>{data && data.message}</div>;
};
  • 更优雅,链式判断
1
2
// data && data.message
data?.message

6.2 useEffect

  • 使用 useEffect 时传入的函数简写要小心,它接收一个无返回值函数或一个清除函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;

useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs]
);
// ❌ bad example! setTimeout 会返回一个记录定时器的 number 类型
// 因为简写,箭头函数的主体没有用大括号括起来。
return null;
}
  • 看看 useEffect接收的第一个参数的类型定义。
1
2
3
4
// 1. 是一个函数
// 2. 无参数
// 3. 无返回值 或 返回一个清理函数,该函数类型无参数、无返回值 。
type EffectCallback = () => (void | (() => void | undefined));
  • 了解了定义后,只需注意加层大括号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;

useEffect(() => {
const timer = setTimeout(() => {
/* do stuff */
}, timerMs);

// 可选
return () => clearTimeout(timer);
}, [timerMs]);
// ✅ 确保函数返回 void 或一个返回 void|undefined 的清理函数
return null;
}
  • 同理,async 处理异步请求,类似传入一个 () => Promise<void>EffectCallback 不匹配。
1
2
3
4
5
// ❌ bad
useEffect(async () => {
const { data } = await ajax(params);
// todo
}, [params]);
  • 异步请求,处理方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ better
useEffect(() => {
(async () => {
const { data } = await ajax(params);
// todo
})();
}, [params]);

// 或者 then 也是可以的
useEffect(() => {
ajax(params).then(({ data }) => {
// todo
});
}, [params]);

6.3 useRef

useRef 一般用于两种场景

  1. 引用 DOM 元素;
  2. 不想作为其他 hooks 的依赖项,因为 ref 的值引用是不会变的,变的只是 ref.current
  • 使用 useRef ,可能会有两种方式。
1
2
const ref1 = useRef<HTMLElement>(null!);
const ref2 = useRef<HTMLElement | null>(null);
  • 非 null 断言 null!。断言之后的表达式非 null、undefined
1
2
3
4
5
6
7
8
function MyComponent() {
const ref1 = useRef<HTMLElement>(null!);
useEffect(() => {
doSomethingWith(ref1.current);
// 跳过 TS null 检查。e.g. ref1 && ref1.current
});
return <div ref={ref1}> etc </div>;
}
  • 不建议使用 !,存在隐患,Eslint 默认禁掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function TextInputWithFocusButton() {
// 初始化为 null, 但告知 TS 是希望 HTMLInputElement 类型
// inputEl 只能用于 input elements
const inputEl = React.useRef<HTMLInputElement>(null);
const onButtonClick = () => {
// TS 会检查 inputEl 类型,初始化 null 是没有 current 上是没有 focus 属性的
// 你需要自定义判断!
if (inputEl && inputEl.current) {
inputEl.current.focus();
}
// ✅ best
inputEl.current?.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

6.4 useReducer

使用 useReducer 时,多多利用 Discriminated Unions 来精确辨识、收窄确定的 typepayload 类型。 一般也需要定义 reducer 的返回类型,不然 TS 会自动推导。

  • 又是一个联合类型收窄和避免拼写错误的精妙例子。
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
const initialState = { count: 0 };

// ❌ bad,可能传入未定义的 type 类型,或码错单词,而且还需要针对不同的 type 来兼容 payload
// type ACTIONTYPE = { type: string; payload?: number | string };

// ✅ good
type ACTIONTYPE =
| { type: 'increment'; payload: number }
| { type: 'decrement'; payload: string }
| { type: 'initial' };

function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - Number(action.payload) };
case 'initial':
return { count: initialState.count };
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
</>
);
}

6.5 useContext

一般 useContextuseReducer 结合使用,来管理全局的数据流。

  • 例子
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
interface AppContextInterface {
state: typeof initialState;
dispatch: React.Dispatch<ACTIONTYPE>;
}

const AppCtx = React.createContext<AppContextInterface>({
state: initialState,
dispatch: (action) => action,
});
const App = (): React.ReactNode => {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<AppCtx.Provider value={{ state, dispatch }}>
<Counter />
</AppCtx.Provider>
);
};

// 消费 context
function Counter() {
const { state, dispatch } = React.useContext(AppCtx);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
</>
);
}

6.6 useImperativeHandle, forwardRef

推荐使用一个自定义的 innerRef 来代替原生的 ref,否则要用到 forwardRef 会搞的类型很复杂。

1
2
3
4
5
6
7
8
9
10
type ListProps = {
innerRef?: React.Ref<{ scrollToTop(): void }>
}

function List(props: ListProps) {
useImperativeHandle(props.innerRef, () => ({
scrollToTop() { }
}))
return null
}

结合刚刚 useRef 的知识,使用是这样的:

1
2
3
4
5
6
7
8
9
10
11
function Use() {
const listRef = useRef<{ scrollToTop(): void }>(null!)

useEffect(() => {
listRef.current.scrollToTop()
}, [])

return (
<List innerRef={listRef} />
)
}

很完美,是不是?

可以在线调试 useImperativeHandle 的例子

6.6 自定义 Hooks

Hooks 的美妙之处不只有减小代码行的功效,重点在于能够做到逻辑与 UI 分离。做纯粹的逻辑层复用。

  • 例子:当你自定义 Hooks 时,返回的数组中的元素是确定的类型,而不是联合类型。可以使用 const-assertions 。
1
2
3
4
5
6
7
8
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // 推断出 [boolean, typeof load],而不是联合类型 (boolean | typeof load)[]
}
  • 也可以断言成 tuple type 元组类型。
1
2
3
4
5
6
7
8
9
10
11
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as [
boolean,
(aPromise: Promise<any>) => Promise<any>
];
}
  • 如果对这种需求比较多,每个都写一遍比较麻烦,可以利用泛型定义一个辅助函数,且利用 TS 自动推断能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function tuplify<T extends any[]>(...elements: T) {
return elements;
}

function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // type is (number | (() => void))[]
}

function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {
}).current;
return tuplify(numberValue, functionValue); // type is [number, () => void]
}

21 【styled-components的使用】

1.为什么要用这个

我们都知道,我们从最开始学css的时候,为了避免写的样式影响到另外的地方。所以我们这样来写的。

1
2
3
#userConten .userBtn button{
font-size: 18px;
}

首先给一个元素写了一个唯一id | class,然后在这个里面写对应的样式,就可以避免影响到其它地方的代码。但是,如果项目是多人协作,那就可能存在命名冲突了,所以我们想要一种技术来让整个项目起的类名都是唯一的id。避免样式冲突等问题。所以css in js 就来了。

简单来说CSS-in-JS就是将应用的CSS样式写在JavaScript文件里面,而不是独立为一些.css,.scss或者less之类的文件,这样你就可以在CSS中使用一些属于JS的诸如模块声明,变量定义,函数调用和条件判断等语言特性来提供灵活的可扩展的样式定义

使用这个技术写的库有很多,react中火的是styled-components,vue中css scope也是这个思想,每个组件都有它的scopeId,样式进行绑定,css modules也是同样的。react中css in js为什么火,框架本身就是html css js 写在一个组件混着写,虽然有些违背一些主流说法,但这就是它的特点,毕竟本身就就可以说html in js,再来一个css in js也很正常。

实现这个的库有很多,在react中最火的就是styled-components。

2.简介

styled-components 是作者对于如何增强 React 组件中 CSS 表现这个问题的思考结果 通过聚焦于单个用例,设法优化了开发者的体验和面向终端用户的输出.

Styled Components 的官方网站 将其优点归结为:

  • Automatic critical CSSstyled-components 持续跟踪页面上渲染的组件,并自动注入样式。结合使用代码拆分, 可以实现仅加载所需的最少代码。
  • 解决了 class name 冲突styled-components 为样式生成唯一的 class name,开发者不必再担心 class name 重复、覆盖以及拼写的问题。(CSS Modules 通过哈希编码局部类名实现这一点)
  • CSS 更容易移除:使用 styled-components 可以很轻松地知道代码中某个 class 在哪儿用到,因为每个样式都有其关联的组件。如果检测到某个组件未使用并且被删除,则其所有的样式也都被删除。
  • 简单的动态样式:可以很简单直观的实现根据组件的 props 或者全局主题适配样式,无需手动管理多个 classes。(这一点很赞)
  • 无痛维护:无需搜索不同的文件来查找影响组件的样式,无论代码多庞大,维护起来都是小菜一碟。
  • 自动提供前缀:按照当前标准写 CSS,其余的交给 styled-components 处理。

因为 styled-components 做的只是在 runtime 把 CSS 附加到对应的 HTML 元素或者组件上,它完美地支持所有 CSS。 媒体查询、伪选择器,甚至嵌套都可以工作。但是要注意,styled-componentsReact 下的 CSS-in-JS 的实践,因此下面的所有例子的技术栈都是 React

3.安装

安装样式化组件只需要一个命令

1
2
npm install --save styled-components
yarn add styled-components

如果使用像 yarn 这样支持 “resolution” package.json 字段的包管理器,还要添加一个与主要版本范围对应的条目。这有助于避免因项目中安装的多个版本的样式化组件而引起的一整类问题。

package.json:

1
2
3
4
5
{
"resolutions": {
"styled-components": "^5"
}
}

注意

强烈推荐使用 styled-components 的 babel 插件open in new window (当然这不是必须的).它提供了许多益处,比如更清晰的类名,SSR 兼容性,更小的包等等.

.babelrc

1
2
3
4
5
{
"plugins": [
"babel-plugin-styled-components"
]
}

如果没有使用模块管理工具或者包管理工具,也可以使用官方托管在 unpkg CDN 上的构建版本.只需在HTML文件底部添加以下<script>标签:

1
<script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>

添加 styled-components 之后就可以访问全局的 window.styled 变量.

1
2
3
const Component = window.styled.div`
color: red;
`

注意

这用使用方式需要页面在 styled-components script 之前引入 react CDN bundles

VsCode 有一款插件 vscode-styled-components 能识别 styled-components ,并能自动进行 CSS 高亮、补全、纠正等。

image-20221211221654403

4.基本使用

样式化组件利用标记的模板文本来设置组件的样式。

它删除了组件和样式之间的映射。这意味着当你定义你的样式时,你实际上是在创建一个普通的 React 组件,它附加了你的样式。

以下的例子创建了两个简单的附加了样式的组件, 一个Wrapper和一个Title:

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 styled from 'styled-components'

/*
创建一个Title组件,
将render一个带有样式的h1标签
*/
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;

/*
创建一个Wrapper组件,
将render一个带有样式的section标签
*/
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;

// 使用 Title and Wrapper 得到下面效果图
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);

image-20221214220428483

值得注意的是styled-components创建的组件首字母必须以大写开头。

几乎所有基础的HTML标签styled都支持,比如divh1span

styled.xxx后面的.xxx代表的是最终解析后的标签,如果是styled.a那么解析出来就是a标签,styled.div解析出来就是div标签。

注意

styled-components 会为我们自动创建 CSS 前缀

5.基于props动态实现

我们可以将 props 以插值的方式传递给styled component,以调整组件样式.

下面这个 Button 组件持有一个可以改变colorprimary属性. 将其设置为 ture 时,组件的background-colorcolor会交换.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Button = styled.button`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;

render(
<>
<Button>Normal</Button>
<Button primary>Primary</Button>
</>
);

image-20221214221122731

对于react开发者来说,这个还是比较香的。有人说用了这个之后,检查元素无法定位元素,其实它本身name是可以展示的,dev开发时候有一个插件配一下即可styled-components: Tooling

6.样式继承

可能我们希望某个经常使用的组件,在特定场景下可以稍微更改其样式.当然我们可以通过 props 传递插值的方式来实现,但是对于某个只需要重载一次的样式来说这样做的成本还是有点高.

创建一个继承其它组件样式的新组件,最简单的方式就是用构造函数styled()包裹被继承的组件.下面的示例就是通过继承上一节创建的按钮从而实现一些颜色相关样式的扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 上一节创建的没有插值的 Button 组件
const Button = styled.button`
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;

// 一个继承 Button 的新组件, 重载了一部分样式
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;

render(
<div>
<Button>Normal Button</Button>
<TomatoButton>Tomato Button</TomatoButton>
</div>
);

image-20221211224440580

可以看到,新的TomatoButton仍然和Button类似,我们只是添加了两条规则.

在某些情况下,您可能需要更改样式化组件渲染的标签或组件。这在构建导航栏时很常见,例如导航栏中同时存在链接和按钮,但是它们的样式应该相同.

在这种情况下,我们也有替代办法(escape hatch). 我们可以使用多态 “as” polymorphic prop 动态的在不改变样式的情况下改变元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Button = styled.button`
display: inline-block;
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;

const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;

render(
<div>
<Button>Normal Button</Button>
<Button as="a" href="/">Link with Button styles</Button>
<TomatoButton as="a" href="/">Link with Tomato Button styles</TomatoButton>
</div>
);

这也完美适用于自定义组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Button = styled.button`
display: inline-block;
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;

const ReversedButton = props => <button {...props} children={props.children.split('').reverse()} />

render(
<div>
<Button>Normal Button</Button>
<Button as={ReversedButton}>Custom Button with Normal Button styles</Button>
</div>
);

比如: styled("div"),styled.tagname的方式就是 styled(tagname)`的别名.

7.条件渲染

styled-components最核心的一点,我个人认为也是这一点,让styled-components变得如此火热,我们直接先看下代码:

字符串前面那个css可加可不加,不加也是能够正常进行渲染的,但是还是推荐加,如果你不加的话在编辑器中就会失去提示的功能,编辑器会把它当作字符串而不是CSS样式。

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";
import styled, { css } from "styled-components";

const Box = styled.div`
${(props) =>
props?.small
? css`
width: 100px;
height: 100px;
`
: css`
width: 200px;
height: 200px;
`}

background-color: red;
`;

export default function App() {
const [small, setSmall] = useState(true);

return (
<div>
<Box small={small} />
<button onClick={() => setSmall(!small)}>切换</button>
</div>
);
}

img

可以看到,使用styled-components编写组件样式的过程会变得异常的简单,如果你用的是CSS,那么你是无法通过React的Props进行更改CSS中的属性,你只能通过Props动态更改dom上绑定的类名,就如同下面的代码一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState } from "react";
import "./styles.css";

export default function App() {
const [small, setSmall] = useState(true);

return (
<div>
<div className={small ? "box-small" : "box"} />
<button onClick={() => setSmall(!small)}>切换</button>
</div>
);
}

这样看起来styled-components没有什么特别的,甚至上面的写法还比较麻烦?其实styled-components的威力不止于此,我们看一下下面的例子:

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

const Box = styled.div`
${(props) => css`
width: ${props?.size}px;
height: ${props?.size}px;
`}
background-color: red;
`;

export default function App() {
const [size, setSize] = useState(100);

return (
<div>
<Box size={size} />
<button onClick={() => setSize(size + 2)}>变大</button>
</div>
);
}

渲染如下:

img

如果是通过CSS属性就非常难以实现这种效果,只有靠React官方提供的style-in-js方案,直接编写行内属性:

1
2
3
4
5
6
7
8
9
10
11
12
import { useState } from "react";

export default function App() {
const [size, setSize] = useState(100);

return (
<div>
<div style={{ width: size, height: size, backgroundColor: "red" }} />
<button onClick={() => setSize(size + 2)}>变大</button>
</div>
);
}

8.普通样式

如果使用过Vue的同学应该很清楚,在.vue文件中有个style标签,你只需要加上了scoped就可以进行样式隔离,而styled-components其实完全具有Vue的style标签的能力,你只需要在最外面包一层,然后就可以实现Vue中样式隔离的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import styled from 'styled-components'

const AppStyle = styled.div`
.box {
width: 100px;
height: 100px;
background-color: red;
}
`
const Div = styled.div``

export default function App() {
return (
<AppStyle>
<Div className="box"></Div>
</AppStyle>
)
}

image-20221215201355737

甚至还可以配合上面的条件渲染进行使用,也非常的方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState } from "react";
import styled, { css } from "styled-components";

const AppStyle = styled.div`
${({ change }) =>
change
? css`
.box {
width: 200px;
height: 200px;
background-color: blue;
}
`
: css`
.box {
width: 100px;
height: 100px;
background-color: red;
}
`}
`;

export default function App() {
const [change, setChange] = useState(false);

return (
<AppStyle change={change}>
<div className="box" />
<button
onClick={() => {
setChange(true);
}}
>
更换
</button>
</AppStyle>
);
}

渲染效果如下图所示:

img

9.attrs

为了避免仅为传递一些props来渲染组件或元素而使用不必要的wrapper, 可以使用 .attrs constructor. 通过它可以添加额外的 props 或 attributes 到组件.

在一些HTML标签中是有一些属性的,比如input标签中,有type这个属性,我们就可以使用attrs给上一个默认值,还可以实现不传对应的属性则给一个默认值,如果传入对应的属性则使用传入的那个属性值。

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 styled from "styled-components";

const Input = styled.input.attrs((props) => ({
// 直接指定一个值
type: "text",

// 给定一个默认值,可以传入Props进行修改
size: props.size || "1em"
}))`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;

margin: ${(props) => props.size};
padding: ${(props) => props.size};
`;

export default function App() {
return (
<div>
<Input placeholder="A small text input" />
<br />
<Input placeholder="A bigger text input" size="2em" />
</div>
);
}

渲染效果:

image-20221215202119951

有继承的话,以继承后的组件中的属性为准

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
const Input = styled.input.attrs((props) => ({
type: "text",
size: props.size || "1em"
}))`
border: 2px solid palevioletred;
margin: ${(props) => props.size};
padding: ${(props) => props.size};
`;

// 有继承的话,以继承后的组件中的属性为准
const PasswordInput = styled(Input).attrs({
type: "password"
})`
border: 2px solid aqua;
`;

export default function App() {
return (
<div>
<Input placeholder="A bigger text input" size="2em" />
<br />
<PasswordInput placeholder="A bigger password input" size="2em" />
</div>
);
}

最后渲染结果:

image-20221215202344333

10.动画

虽然使用@keyframes的 CSS 动画不限于单个组件,但我们仍希望它们不是全局的(以避免冲突). 这就是为什么 styled-components 导出 keyframes helper 的原因: 它将生成一个可以在 APP 应用的唯一实例。

动画需要使用keyframes进行声明,如下所示:

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 styled, { keyframes } from "styled-components";

// 通过keyframes创建动画
const rotate = keyframes`
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
`;

// 创建动画的组件
const Rotate = styled.span`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 1.2rem;
`;

export default function App() {
return (
<div>
<Rotate>&lt; &gt;</Rotate>
</div>
);
}

渲染结果:

img

11.Coming from CSS

11.1 styled-components 如何在组件中工作?

如果你熟悉在组件中导入 CSS(例如 CSSModules),那么下面的写法你一定不陌生:

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

export default class Counter extends React.Component {
state = { count: 0 }

increment = () => this.setState({ count: this.state.count + 1 })
decrement = () => this.setState({ count: this.state.count - 1 })

render() {
return (
<div className={styles.counter}>
<p className={styles.paragraph}>{this.state.count}</p>
<button className={styles.button} onClick={this.increment}>
+
</button>
<button className={styles.button} onClick={this.decrement}>
-
</button>
</div>
)
}
}

由于 Styled Component 是 HTML 元素和作用在元素上的样式规则的组合, 我们可以这样编写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
28
29
import React from 'react'
import styled from 'styled-components'

const StyledCounter = styled.div`
/* ... */
`
const Paragraph = styled.p`
/* ... */
`
const Button = styled.button`
/* ... */
`

export default class Counter extends React.Component {
state = { count: 0 }

increment = () => this.setState({ count: this.state.count + 1 })
decrement = () => this.setState({ count: this.state.count - 1 })

render() {
return (
<StyledCounter>
<Paragraph>{this.state.count}</Paragraph>
<Button onClick={this.increment}>+</Button>
<Button onClick={this.decrement}>-</Button>
</StyledCounter>
)
}
}

注意,我们在StyledCounter添加了”Styled”前缀,这样组件CounterStyledCounter 不会明明冲突,而且可以在 React Developer Tools 和 Web Inspector 中轻松识别.

11.2 使用伪元素、选择器、嵌套语法

由于 styled-components 采用 stylis 作为预处理器,因此提供了对伪元素、伪选择器以及嵌套写法的支持(跟 Les 很类似)。其中,& 指向组件本身:

1
2
3
const Thing = styled.div`
color: blue;
`

伪元素和伪类无需进一步细化,而是自动附加到了组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Thing = styled.button`
color: blue;

::before {
content: '🚀';
}

:hover {
color: red;
}
`

render(
<Thing>Hello world!</Thing>
)

对于更复杂的选择器,可以使用与号(&)来指向主组件.以下是一些示例:

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
const ScDiv = styled.div`
color: blue;

&:hover {
color: red; // 被 hover 时的样式
}

& ~ & {
background: tomato; // ScDiv 作为 ScDiv 的 sibling
}

& + & {
background: lime; // 与 ScDiv 相邻的 ScDiv
}

&.something {
background: orange; // 带有 class .something 的 ScDiv
}

.something-child & {
border: 1px solid; // 不带有 & 时指向子元素,因此这里表示在带有 class .something-child 之内的 ScDiv
`;

render(
<React.Fragment>
<ScDiv>Hello world!</ScDiv>
<ScDiv>How ya doing?</ScDiv>
<ScDiv className="something">The sun is shining...</ScDiv>
<ScDiv>Pretty nice day today.</ScDiv>
<ScDiv>Don't you think?</ScDiv>
<div className="something-else">
<ScDiv>Splendid.</ScDiv>
</div>
</React.Fragment>
)
复制代码

渲染的结果如图所示:

image-20221212205623181

如果只写选择器而不带&,则指向组件的子节点.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Thing = styled.div`
color: blue;

.something {
border: 1px solid; // an element labeled ".something" inside <Thing>
display: block;
}
`

render(
<Thing>
<label htmlFor="foo-button" className="something">Mystery button</label>
<button id="foo-button">What do I do?</button>
</Thing>
)

最后,&可以用于增加组件的差异性;在处理混用 styled-components 和纯 CSS 导致的样式冲突时这将会非常有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Thing = styled.div`
&& {
color: blue;
}
`

const GlobalStyle = createGlobalStyle`
div${Thing} {
color: red;
}
`

render(
<React.Fragment>
<GlobalStyle />
<Thing>
I'm blue, da ba dee da ba daa
</Thing>
</React.Fragment>
)

12.媒体查询

开发响应式 web app 时媒体查询是不可或缺的工具.

以下是一个非常简单的示例,展示了当屏宽小于700px时,组件如何改变背景色:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Content = styled.div`
background: papayawhip;
height: 3em;
width: 3em;

@media (max-width: 700px) {
background: palevioletred;
}
`;

render(
<Content />
);

由于媒体查询很长,并且常常在应用中重复出现,因此有必要为其创建模板.

由于 JavaScript 的函数式特性,我们可以轻松的定义自己的标记模板字符串用于包装媒体查询中的样式.我们重写一下上个例子来试试:

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
const sizes = {
desktop: 992,
tablet: 768,
phone: 576,
}

// Iterate through the sizes and create a media template
const media = Object.keys(sizes).reduce((acc, label) => {
acc[label] = (...args) => css`
@media (max-width: ${sizes[label] / 16}em) {
${css(...args)}
}
`

return acc
}, {})

const Content = styled.div`
height: 3em;
width: 3em;
background: papayawhip;

/* Now we have our methods on media and can use them instead of raw queries */
${media.desktop`background: dodgerblue;`}
${media.tablet`background: mediumseagreen;`}
${media.phone`background: palevioletred;`}
`;

render(
<Content />
);

13.asprop

as - 转变组件类型,比如将一个div转变为button

1
2
3
4
5
6
7
8
9
10
11
12
const Component = styled.div`
color: red;
`;

render(
<Component
as="button"
onClick={() => alert('It works!')}
>
Hello World!
</Component>
)
1
2
3
4
5
6
7
8
9
export default () => {
return (
// as(可以是组件名,也可以是普通标签名): 表示要渲染出来的标签或组件
// 这个例子表示: 继承了 ScExtendedButton 样式的 a 标签
<ScExtendedButton as="a" href="#">
Extends Link with Button styles
</ScExtendedButton>
)
}

14.样式化任意组件

14.1 样式化组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Link = ({ className, children }) => (
// className 属性附加到 DOM 元素上
<a className={className}>
{children}
</a>
)

const StyledLink = styled(Link)`
color: red;
font-weight: bold;
`

render(
<div>
<Link>Unstyled Link</Link>
<StyledLink>Styled Link</StyledLink>
</div>
)

14.2 样式化第三方组件

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

const ScButton = styled(Button)`
margin-top: 12px;
color: green;
`

render(
<div>
<ScButton>Styled Fusion Button</ScButton>
</div>
)

15.主题切换

15.1 基本使用

styled-components 通过导出 <ThemeProvider> 组件从而能支持主题切换。 <ThemeProvider>是基于 React 的 Context API 实现的,可以为其下面的所有 React 组件提供一个主题。在渲染树中,任何层次的所有样式组件都可以访问提供的主题。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import styled, {ThemeProvider} from "styled-components";

// 通过使用 props.theme 可以访问到 ThemeProvider 传递下来的对象
const Button = styled.button`
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border-radius: 3px;
color: ${props => props.theme.main};
border: 2px solid ${props => props.theme.main};
`;

// 为 Button 指定默认的主题
Button.defaultProps = {
theme: {
main: "palevioletred"
}
}

const theme = {
main: "mediumseagreen"
};

render(
<div>
<Button>Normal</Button>
// 采用了 ThemeProvider 提供的主题的 Button
<ThemeProvider theme={theme}>
<Button>Themed</Button>
</ThemeProvider>
</div>
);

image-20221213202527317

15.2 函数主题

ThemeProvidertheme除了可以接受对象之外,还可以接受函数。函数的参数是父级的 theme对象。此外,还可以通过使用 theme prop 来处理 ThemeProvider 未定义的情况(这跟上面的 defaultProps是一样的效果),或覆盖 ThemeProvider的 theme。例如:

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
const ScButton = styled.button`
color: ${props => props.theme.fg};
border: 2px solid ${props => props.theme.fg};
background: ${props => props.theme.bg};
`;

const theme = {
fg: "palevioletred",
bg: "white"
};

const invertTheme = ({ fg, bg }) => ({
fg: bg,
bg: fg
});

render(
// ThemeProvider 未定义的情况
<ScButton theme={{
fg: 'red',
bg: 'white'
}}>Default Theme</ScButton>
<ThemeProvider theme={theme}>
<div>
<ScButton>Default Theme</ScButton>
// theme 接收的是一个函数,函数的参数是父级的 theme
<ThemeProvider theme={invertTheme}>
<ScButton>Inverted Theme</ScButton>
</ThemeProvider>
// 覆盖 ThemeProvider的 theme
<ScButton theme={{
fg: 'red',
bg: 'white'
}}>Override Theme</ScButton>
</div>
</ThemeProvider>
);

image-20221213202602311

15.3 在 styled-components 外使用主题

如果需要在styled-components外使用主题,可以使用高阶组件withTheme:

1
2
3
4
5
6
7
8
9
10
import { withTheme } from 'styled-components'

class MyComponent extends React.Component {
render() {
console.log('Current theme: ', this.props.theme)
// ...
}
}

export default withTheme(MyComponent)

通过useContext React hook

使用React Hooks时,还可以使用useContext访问样式化组件之外的当前主题。

1
2
3
4
5
6
7
8
9
import { useContext } from 'react'
import { ThemeContext } from 'styled-components'

const MyComponent = () => {
const themeContext = useContext(ThemeContext)

console.log('Current theme: ', themeContext)
// ...
}

通过useTheme自定义挂钩

使用React Hooks时,您还可以使用useTheme访问样式组件之外的当前主题。

1
2
3
4
5
6
7
8
import { useTheme } from 'styled-components'

const MyComponent = () => {
const theme = useTheme()

console.log('Current theme: ', theme)
// ...
}

15.4 theme prop

主题可以通过theme prop传递给组件.通过使用theme prop可以绕过或重写ThemeProvider所提供的主题.

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
// Define our button
const Button = styled.button`
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border-radius: 3px;

/* Color the border and text with theme.main */
color: ${props => props.theme.main};
border: 2px solid ${props => props.theme.main};
`;

// Define what main theme will look like
const theme = {
main: "mediumseagreen"
};

render(
<div>
<Button theme={{ main: "royalblue" }}>Ad hoc theme</Button>
<ThemeProvider theme={theme}>
<div>
<Button>Themed</Button>
<Button theme={{ main: "darkorange" }}>Overridden</Button>
</div>
</ThemeProvider>
</div>
);

image-20221213202829952

16.Refs

通过传递ref prop给 styled component 将获得:

  • 底层 DOM 节点 (如果 styled 的对象是基本元素如 div)
  • React 组件实例 (如果 styled 的对象是 React Component)
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
const Input = styled.input`
padding: 0.5em;
margin: 0.5em;
color: palevioletred;
background: papayawhip;
border: none;
border-radius: 3px;
`;

class Form extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}

render() {
return (
<Input
ref={this.inputRef}
placeholder="Hover to focus!"
onMouseEnter={() => {
this.inputRef.current.focus()
}}
/>
);
}
}

render(
<Form />
);

image-20221213203049891

注意

v3 或更低的版本请使用 innerRef prop instead.

17.样式对象

styled-components 支持将 CSS 写成 JavaScript 对象.对于已存在的样式对象,可以很轻松的将其迁移到 styled-components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Static object
const Box = styled.div({
background: 'palevioletred',
height: '50px',
width: '50px'
});

// Adapting based on props
const PropsBox = styled.div(props => ({
background: props.background,
height: '50px',
width: '50px'
}));

render(
<div>
<Box />
<PropsBox background="blue" />
</div>
);

image-20221213203538145

18.CSS Prop实现内联样式

避免创建新的组件,直接应用样式,需要用到 styled-components 提供的 babel-plugin: styled-components.com/docs/toolin…

1
2
3
4
5
6
7
8
<div
css={`
background: papayawhip;
color: ${props => props.theme.colors.text};
`}
/>

<MyComponent css="padding: 0.5em 1em;"/>

参考:https://styled-components.com/docs/tooling#babel-plugin

19.mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import styled, { css } from 'styled-components';
import { Button as FusionButton } from 'antd';

const mixinCommonCSS = css`
margin-top: 12px;
border: 1px solid grey;
borde-radius: 4px;
`;

const ScButton = styled.button`
${mixinCommonCSS}
color: yellow;
`;

const ScFusionButton = styled(FusionButton)`
${mixinCommonCSS}
color: blue;
`;

20.性能问题

Styled-Components 定义的组件一定要放在组件函数定义之外(对于 Class 类型的组件,不要放在 render 方法内 )。因为在 react 组件的 render 方法中声明样式化的组件,会导致每次渲染都会创建一个新组建。 这意味着 React 将不得不在每个后续渲染中丢弃并重新计算 DOM 子树的那部分,而不是仅仅计算它们之间变化的差异,从而导致性能瓶颈和不可预测的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ❌ 绝对不要这样写
const Header = () => {
const Title = styled.h1`
font-size: 10px;
`

return (
<div>
<Title />
</div>
)
}

// ✅应该要这样写
const Title = styled.h1`
font-size: 10px;
`

const Header = () => {
return (
<div>
<Title />
</div>
)
}

此外,如果 styled-components 的目标是一个简单的 HTML 元素(例如 styled.div),那么 styled-components 将传递所有原生的 HTML AttributesDOM。如果是自定义 React 组件(例如 styled(MyComponent)),则 styled-components 会传递所有的 props

21.配合TypeScript

React+TypeScript一直是神组合,React可以完美的搭配TypeScript。

但在TypeScript中使用得先安装@types/styled-components类型声明库:

1
npm install @types/styled-components -D

如在是要在TypeScript中,那么需要对styled-components组件的属性类型进行声明,不然会报错,虽然不会影响最终的编译结果:

image-20221211230301186

下面的组件类型就需要进行声明:

image-20221211230316835

下面例子展示了一个样式化的 Button 接收 primary 属性,并根据该属性调整背景颜色 background 以及 color

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, {
ButtonHTMLAttributes
} from 'react';
import styled from 'styled-components';

interface IScButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
primary?: boolean;
}

const ScWrapper = styled.div`
margin-top: 12px;
`;

const ScButton = styled.button<IScButtonProps> `
background: ${props => props.primary ? "blue" : "white"};
color: ${props => props.primary ? "white" : "blue"};
border: 2px solid palevioletred;
border-radius: 3px;
padding: 0.25em 1em;
`;

export default () => {
return (
<ScWrapper>
<ScButton>Normal</ScButton>
<ScButton primary>Primary</ScButton>
</ScWrapper>
);
};

22 【在react中使用Emotion】

1.CSS in JS 的优点

CSS in JS 已逐渐发展为 React 应用中写样式的一个主流的方案,著名组件库 material-ui 也已经使用 CSS in JS 来实现。 CSS in JS 的实现方式有两种: 唯一CSS选择器和内联样式。因此

  1. 不用关心繁琐的 Class 命名规则
  2. 不用担心样式被覆盖
  3. 便利的样式复用(样式都是 js 对象或字符串)
  4. 减少冗余的 CSS 代码,极致的样式按需加载

Emotion 是 CSS in JS 的众多实现方案中的其中一个,下面介绍一下它的使用。

说明:以下的介绍都来自于Emotion官方文档

安装

1
npm i @emotion/styled @emotion/react

使用

Emotion 有两种写 CSS 的方式:css-prop Styled Components

2.Css Prop

添加预设或将杂注设置为注释后,React.createElement编译后的 jsx 代码将使用 emotion 的函数而不是 .jsx

2.1 Babel Preset

此方法不适用于创建 React App 或其他不允许自定义 Babel 配置的项目。 请改用 JSX 注释方法

.babelrc

1
2
3
{
"presets": ["@emotion/babel-preset-css-prop"]
}

完整的@emotion/babel-preset-css-prop 文档

If you are using the compatible React version (>=16.14.0) then you can opt into using the new JSX runtimes by using such configuration:

如果 React 版本 >=16.14.0 , 可以使用如下的配置来使用新的 jsx 运行时。

1
2
3
4
5
6
7
8
9
{
"presets": [
[
"@babel/preset-react",
{ "runtime": "automatic", "importSource": "@emotion/react" }
]
],
"plugins": ["@emotion/babel-plugin"]
}

2.2 JSX 注释

jsx 注释设置在使用道具的源文件的顶部。 此选项最适合测试 prop 功能或在 babel 配置不可配置的项目(create-react-app、codesandbox 等)中。

1
2
/** @jsx jsx */
import { jsx } from '@emotion/react'

/** @jsx jsx */ 不生效的时候可以改为 /** @jsxImportSource @emotion/react */ 来尝试。

2.3 tsconfig.json

这里指的是使用 babel 编译 typescript 时的配置

1
2
3
4
5
6
7
8
{
"compilerOptions": {
...
// "jsx": "react",
"jsxImportSource": "@emotion/react",
...
}
}

2.4 Object Styles 和 String Styles

Emotion 支持 js 对象js 字符串两种形式的样式定义。

Object Styles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** @jsx jsx */
import { jsx } from '@emotion/react'

render(
<div
css={{
backgroundColor: 'hotpink',
'&:hover': {
color: 'lightgreen'
}
}}
>
This has a hotpink background.
</div>
)

image-20221213124704208

Object Style Documentation

要传递字符串样式,您必须使用 @emotion/react导出的css ,它可以用作标记模板文字 ,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/react'

const color = 'darkgreen'

render(
<div
css={css`
background-color: hotpink;
&:hover {
color: ${color};
}
`}
>
This has a hotpink background.
</div>
)

image-20221213124738854

无论是Object Styles还是String Styles,我们都可以直接在定义样式的时候读取上下文的 js 变量,这个可以让我们很方便地更改样式。

3.Styled Components

Styled Components 基础用法 Styled Components 导出了一些带有 html 标签的内置组件。

3.1 写一个带样式的组件

styledcss非常相似,除了你用 html 标签或 React 组件调用它,然后用字符串样式的模板文字或对象样式的常规函数调用来调用它。

语法:styled.元素名样式

1
2
3
4
5
import styled from '@emotion/styled'
const Button = styled.button`
color: turquoise;
`
render(<Button>This my button component.</Button>)

image-20221213205055196

3.2 通过参数控制样式

Styled Components 的 Props Styled Components 生成的组件也可以根据传入的 Props 来更改样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import styled from '@emotion/styled'

const Button = styled.button`
color: ${props => (props.primary ? 'hotpink' : 'turquoise')};
`

const Container = styled.div(props => ({
display: 'flex',
flexDirection: props.column && 'column'
}))

render(
<Container column>
<Button>This is a regular button.</Button>
<Button primary>This is a primary button.</Button>
</Container>
)

image-20221213211843757

3.3 通过传className创建组件

语法:

1
2
styled( ({className}) => (<p className={className}>text</
p>) )`样式`

分析:相当于把样式通过className传递给了元素

1
2
3
4
5
6
7
8
import styled from '@emotion/styled'
const Basic = ({ className }) => (
<div className={className}>Some text</div>
)
const Fancy = styled(Basic)`
color: hotpink;
`
render(<Fancy />)

image-20221213212041581

3.4 创建与某个组件相同的样式

有时您想使用一个组件创建一些样式,然后再次将这些样式用于另一个组件,该方法可用于此目的。

语法:样式组件.withComponent('元素')

1
2
3
4
5
6
7
8
9
10
11
12
13
import styled from '@emotion/styled'
const Section = styled.section`
background: #333;
color: #fff;
`
// Aside样式跟Section样式相同
const Aside = Section.withComponent('aside')
render(
<div>
<Section>This is a section</Section>
<Aside>This is an aside</Aside>
</div>
)

image-20221213212523562

3.5 嵌套写法

3.5.1 $

styled-components 类似,当使用@emotion/babel-plugin 时,emotion允许emotion components像常规CSS选择器一样被嵌套。

语法:父组件 = styled.元素${子组件} {样式}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import styled from '@emotion/styled'

const Child = styled.div`
color: red;
`

const Parent = styled.div`
${Child} {
color: green;
}
`

render(
<div>
<Parent>
<Child>Green because I am inside a Parent</Child>
</Parent>
<Child>Red because I am not inside a Parent</Child>
</div>
)

image-20221213212915098

3.5.2 对象(键值对)

组件选择器也可以与对象样式一起使用

语法:

1
2
3
4
5
父组件 = styled.元素(
{
[子组件]: {样式}
}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import styled from '@emotion/styled'

const Child = styled.div({
color: 'red'
})

const Parent = styled.div({
[Child]: {
color: 'green'
}
})

render(
<div>
<Parent>
<Child>green</Child>
</Parent>
<Child>red</Child>
</div>
)

image-20221213213342914

3.6 对象样式

1
2
3
4
5
6
7
8
import styled from '@emotion/styled'
const H1 = styled.h1(
{
fontSize: 20
},
props => ({ color: props.color, width:props.width })
)
render(<H1 color="lightgreen" width="200px">This is lightgreen.</H1>)

image-20221213214851893

3.7 自定义 prop 转发

默认情况下,Emotion 会将所有 props(theme除外)传递给自定义组件,并且仅传递作为字符串标签的有效 html 属性的 prop。可以通过传递自定义函数来自定义此设置。您还可以使用shouldForwardProp来过滤掉无效的 html 属性。

1
2
3
4
5
6
7
8
9
10
import isPropValid from '@emotion/is-prop-valid'
import styled from '@emotion/styled'

const H1 = styled('h1', {
shouldForwardProp: prop => isPropValid(prop) && prop !== 'color'
})(props => ({
color: props.color
}))

render(<H1 color="lightgreen">This is lightgreen.</H1>)

image-20221213215311231

3.8 动态样式

您可以创建基于 props 的动态样式,并在样式中使用它们。

1
2
3
4
5
6
7
8
9
10
11
12
import styled from '@emotion/styled'
import { css } from '@emotion/react'

const dynamicStyle = props =>
css`
color: ${props.color};
`

const Container = styled.div`
${dynamicStyle};
`
render(<Container color="lightgreen">This is lightgreen.</Container>)

image-20221213220140864

3.9 as prop

要使用样式化组件中的样式但要更改呈现的元素,可以使用as prop。

1
2
3
4
5
6
7
8
9
10
11
import styled from '@emotion/styled'

const Button = styled.button`
color: hotpink;
`

render(
<Button as="a" href="https://github.com/emotion-js/emotion">
Emotion on GitHub
</Button>
)

image-20221213220134546

3.10 嵌套元素样式写法

我们可以使用以下方法嵌套选择器:&

1
2
3
4
5
6
7
8
9
10
11
12
import styled from '@emotion/styled'
const Example = styled('span')`
color: lightgreen;
& > a {
color: hotpink;
}
`
render(
<Example>
This is <a>nested</a>.
</Example>
)

image-20221213220109366

4.Composition

组合是emotion中最强大、最有用的模式之一。您可以通过在另一个样式块中插入从css返回的值来组合样式。

4.1 样式复用

在 Emotion 中,我们可以把通用样式用变量声明,然后在不同的组件中共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { css } from '@emotion/react'

const base = css`
color: hotpink;
`

render(
<div
css={css`
${base};
background-color: #eee;
`}
>
This is hotpink.
</div>
)

上面的 base 样式在 render 时被使用。如果我们有其它的组件用到 base 样式,我们也可以导入 base 这个变量来使用。

4.2 样式优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { css } from '@emotion/react'

const danger = css`
color: red;
`

const base = css`
background-color: darkgreen;
color: turquoise;
`

render(
<div>
<div css={base}>This will be turquoise</div>
<div css={[danger, base]}>
This will be also be turquoise since the base styles overwrite the danger
styles.
</div>
<div css={[base, danger]}>This will be red</div>
</div>
)

image-20221213221609976

写样式的时候难免会需要覆盖样式的情况,这时候我们可以像上面一样调整 basedanger 的先后顺序来覆盖(后面的样式优先级较高)。

5.Object Styles

带对象的写作风格是一种直接构建在emotion核心的强大模式。您可以使用camelCase来编写css属性,而不是像普通css那样使用kebab-case大小写,例如背景色将是backgroundColor。对象样式对于css属性特别有用,因为您不需要像字符串样式那样的css调用,但是对象样式也可以与样式一起使用。

5.1 使用 css props

1
2
3
4
5
6
7
8
9
10
render(
<div
css={{
color: 'darkorchid',
backgroundColor: 'lightgray'
}}
>
This is darkorchid.
</div>
)

image-20221213223856868

5.2 使用styled

1
2
3
4
5
6
7
8
9
10
11
12
import styled from '@emotion/styled'

const Button = styled.button(
{
color: 'darkorchid'
},
props => ({
fontSize: props.fontSize
})
)

render(<Button fontSize={16}>This is a darkorchid button.</Button>)

image-20221213224008883

5.3 子选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
render(
<div
css={{
color: 'darkorchid',
'& .name': {
color: 'orange'
}
}}
>
This is darkorchid.
<div className="name">This is orange</div>
</div>
)

image-20221213224107609

5.4 媒体查询

1
2
3
4
5
6
7
8
9
10
11
12
render(
<div
css={{
color: 'darkorchid',
'@media(min-width: 420px)': {
color: 'orange'
}
}}
>
This is orange on a big screen and darkorchid on a small screen.
</div>
)

image-20221213224217334

5.5 Numbers

1
2
3
4
5
6
7
8
9
10
render(
<div
css={{
padding: 8,
zIndex: 200
}}
>
This has 8px of padding and a z-index of 200.
</div>
)

image-20221213224256993

5.6 Arrays

嵌套数组被展平

1
2
3
4
5
6
7
8
9
10
11
render(
<div
css={[
{ color: 'darkorchid' },
{ backgroundColor: 'hotpink' },
{ padding: 8 }
]}
>
This is darkorchid with a hotpink background and 8px of padding.
</div>
)

image-20221213224346539

5.7 用css

您也可以将css与对象样式一起使用。

1
2
3
4
5
6
7
8
9
10
11
import { css } from '@emotion/react'

const hotpink = css({
color: 'hotpink'
})

render(
<div>
<p css={hotpink}>This is hotpink</p>
</div>
)

image-20221213224458835

5.8 Composition - 样式复用

Learn more composition in Emotion

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 { css } from '@emotion/react'

const hotpink = css({
color: 'hotpink'
})

const hotpinkHoverOrFocus = css({
'&:hover,&:focus': hotpink
})

const hotpinkWithBlackBackground = css(
{
backgroundColor: 'black',
color: 'green'
},
hotpink
)

render(
<div>
<p css={hotpink}>This is hotpink</p>
<button css={hotpinkHoverOrFocus}>This is hotpink on hover or focus</button>
<p css={hotpinkWithBlackBackground}>
This has a black background and is hotpink. Try moving where hotpink is in
the css call and see if the color changes.
</p>
</div>
)

image-20221213224550509

当组件是子组件时,使用 & 来选择自己并设置样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { css } from '@emotion/react'

const paragraph = css`
color: turquoise;

header & {
color: green;
}
`
render(
<div>
<header>
<p css={paragraph}>This is green since it's inside a header</p>
</header>
<p css={paragraph}>This is turquoise since it's not inside a header.</p>
</div>
)

image-20221213224813608

7.Media Queries

emotion中使用媒体查询就像在常规 css 中使用媒体查询一样,只是您不必在块内指定选择器,您可以将 css 直接放在 css 块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { css } from '@emotion/react'

render(
<p
css={css`
font-size: 30px;
@media (min-width: 420px) {
font-size: 50px;
}
`}
>
Some text!
</p>
)

image-20221213225036540

8.Global Styles

有时您可能希望插入全局 css,例如resets 或 font faces。您可以使用该Global组件来执行此操作。它接受一个 stylesprop,该 prop 接受与css prop 相同的值,除了全局插入样式。当样式更改或全局组件卸载时,也会删除全局样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Global, css } from '@emotion/react'

render(
<div>
<Global
styles={css`
.some-class {
color: hotpink !important;
}
`}
/>
<Global
styles={{
'.some-class': {
fontSize: 50,
textAlign: 'center'
}
}}
/>
<div className="some-class">This is hotpink now!</div>
</div>
)

image-20221213225321584

9.Keyframes

您可以使用@emotive/react中的keyframes来定义动画。keyframe接受css关键帧定义,并返回一个可以在样式中使用的对象。您可以像css一样使用字符串或对象。

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 { css, keyframes } from '@emotion/react'

const bounce = keyframes`
from, 20%, 53%, 80%, to {
transform: translate3d(0,0,0);
}

40%, 43% {
transform: translate3d(0, -30px, 0);
}

70% {
transform: translate3d(0, -15px, 0);
}

90% {
transform: translate3d(0,-4px,0);
}
`

render(
<div
css={css`
animation: ${bounce} 1s ease infinite;
`}
>
some bouncing text!
</div>
)

image-20221213225454209

10.Attaching Props - 附加额外的属性

一些 css-in-js 库提供了将 props 附加到组件的 API,而不是让我们自己的 API 来做到这一点,我们建议创建一个常规的 react 组件,使用 css prop 并像附加任何其他 React 组件一样附加 props。

请注意,如果 css 是通过 props 传递下来的,它将优先于组件中的 css。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { css } from '@emotion/react'

const pinkInput = css`
background-color: pink;
`
const RedPasswordInput = props => (
<input
type="password"
css={css`
background-color: red;
display: block;
`}
{...props}
/>
)

render(
<div>
<RedPasswordInput placeholder="red" />
<RedPasswordInput placeholder="pink" css={pinkInput} />
</div>
)

image-20221213230119759

11.Theming

主题包含在@emotion/react中。 将ThemeProvider添加到应用程序的顶层,并在样式组件中使用props.theme访问主题,或者提供一个接受主题作为css属性的函数。

11.1 css prop

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ThemeProvider } from '@emotion/react'

const theme = {
colors: {
primary: 'hotpink'
}
}

render(
<ThemeProvider theme={theme}>
<div css={theme => ({ color: theme.colors.primary })}>some other text</div>
</ThemeProvider>
)

image-20221213230310767

11.2 styled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ThemeProvider } from '@emotion/react'
import styled from '@emotion/styled'

const theme = {
colors: {
primary: 'hotpink'
}
}

const SomeText = styled.div`
color: ${props => props.theme.colors.primary};
`

render(
<ThemeProvider theme={theme}>
<SomeText>some text</SomeText>
</ThemeProvider>
)

image-20221213230337193

11.3 useTheme hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ThemeProvider, useTheme } from '@emotion/react'

const theme = {
colors: {
primary: 'hotpink'
}
}

function SomeText(props) {
const theme = useTheme()
return <div css={{ color: theme.colors.primary }} {...props} />
}

render(
<ThemeProvider theme={theme}>
<SomeText>some text</SomeText>
</ThemeProvider>
)

image-20221213230505784

12.1 HTML/SVG elements

1
2
3
4
5
6
7
8
9
10
11
import styled from '@emotion/styled'

const Link = styled('a')`
color: red;
`

const Icon = styled('svg')`
stroke: green;
`

const App = () => <Link href="#">Click me</Link>
1
2
3
4
5
6
7
8
9
10
import styled from '@emotion/styled';

const NotALink = styled('div')`
color: red;
`;

const App = () => (
<NotALink href="#">Click me</NotALink>
^^^^^^^^ Property 'href' does not exist [...]
);

withComponent

1
2
3
4
5
6
7
8
9
10
11
import styled from '@emotion/styled'

const NotALink = styled('div')`
color: red;
`

const Link = NotALink.withComponent('a')

const App = () => <Link href="#">Click me</Link>

// No errors!

12.2 定义 props 类型

您可以定义styled components 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
import styled from '@emotion/styled'

type ImageProps = {
src: string
width: number
}

// Using a css block
const Image0 = styled.div<ImageProps>`
width: ${props => props.width};
background: url(${props => props.src}) center center;
background-size: contain;
`
const Image0 = styled('div')<ImageProps>`
width: ${props => props.width};
background: url(${props => props.src}) center center;
background-size: contain;
`

// Or with object styles
const Image1 = styled('div')<ImageProps>(
{
backgroundSize: 'contain'
},
props => ({
width: props.width,
background: `url(${props.src}) center center`
})
)

12.3 React Components

Emotion 还可以设置React组件的样式,并根据预期推断组件 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
import React, { FC } from 'react'
import styled from '@emotion/styled'

interface ComponentProps {
className?: string
label: string
}

const Component: FC<ComponentProps> = ({ label, className }) => (
<div className={className}>{label}</div>
)

const StyledComponent0 = styled(Component)`
color: ${props => (props.label === 'Important' ? 'red' : 'green')};
`

const StyledComponent1 = styled(Component)({
color: 'red'
})

const App = () => (
<div>
<StyledComponent0 label="Important" />
<StyledComponent1 label="Yea! No need to re-type this label prop." />
</div>
)

23 【UmiJS入门】

1.Umi 介绍

Umi

1.1 Umi 是什么?

Umi,中文发音为「乌米」,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

Umi 有很多非常有意思的特性,比如。

1、企业级,在安全性、稳定性、最佳实践、约束能力方面会考虑更多 2、插件化,啥都能改,Umi 本身也是由插件构成 3、MFSU,比 Vite 还快的 Webpack 打包方案 4、基于 React Router 6 的完备路由 5、默认最快的请求 6、SSR & SSG 7、稳定白盒性能好的 ESLint 和 Jest 8、React 18 的框架级接入 9、Monorepo 最佳实践 …

1.2 什么时候不用 Umi?

如果你的项目,

1、需要支持 IE 8 或更低版本的浏览器 2、需要支持 React 16.8.0 以下的 React 3、需要跑在 Node 14 以下的环境中 4、有很强的 webpack 自定义需求和主观意愿 5、需要选择不同的路由方案 …

Umi 可能不适合你。

1.3 为什么不是?

  1. create-react-app

    create-react-app 是脚手架,和 Umi、next.js、remix、ice、modern.js 等元框架不是同一类型。脚手架可以让我们快速启动项目,对于单一的项目够用,但对于团队而言却不够。因为使用脚手架像泼出去的水,一旦启动,无法迭代。同时脚手架所能做的封装和抽象都非常有限。

  2. next.js

    如果要做 SSR,next.js 是非常好的选择(当然,Umi 也支持 SSR);而如果只做 CSR,Umi 会是更好的选择。相比之下,Umi 的扩展性会更好;并且 Umi 做了很多更贴地气的功能,比如配置式路由、补丁方案、antd 的接入、微前端、国际化、权限等;同时 Umi 会更稳定,因为他锁了能锁的全部依赖,定期主动更新,某一个子版本的 Umi,不会因为重装依赖之后而跑不起来。

  3. remix

    Remix 是我非常喜欢的框架,Umi 4 从中抄(学)了不少东西。但 Remix 是 Server 框架,其内置的 loader 和 action 都是跑在 server 端的,所以会对部署环境会有一定要求。Umi 将 loader、action 以及 remix 的请求机制同时运用到 client 和 server 侧,不仅 server 请求快,纯 CSR 的项目请求也可达到理论的最快值。同时 Remix 基于 esbuild 做打包,可能不适用于对兼容性有要求或者依赖尺寸特别大的项目。

1.4 插件和插件集

Umi 通过提供插件和插件集的机制来满足不同场景和业务的需求。插件是为了扩展一个功能,而插件集是为了扩展一类业务。比如要支持 vue,我们可以有 @umijs/preset-vue,包含 vue 相关的构建和运行时;比如要支持 h5 的应用类型,可以有 @umijs/preset-h5,把 h5 相关的功能集合到一起。

如果要类比,插件集和 babel 的 preset,以及 eslint 的 config 都类似。

1.5 import all from umi

很多人可能都第一次听到。import all from umi 意思是所有 import 都来自 umi。比如 dva 不是 import { connect } from 'dva',而是 import { connect } from 'umi',从 umi 中导出。导出的方法不仅来自 umi 自身,还来自 umi 插件。

这是两年前 Umi 3 加的功能,最近发现 Remix、prisma、vitekit 等框架和工具都有类似实现。

1
2
// 大量插件为 umi 提供额外导出内容
import { connect, useModel, useIntl, useRequest, MicroApp, ... } from 'umi';

这带来的好处是。通过 Umi 将大量依赖管理起来,用户无需手动安装;同时开发者在代码中也会少很多 import 语句。

2.快速上手

2.1 环境准备

首先得有 node,并确保 node 版本是 14 或以上。(推荐用 nvm 来管理 node 版本,windows 下推荐用 nvm-windows

然后需要包管理工具。node 默认包含 npm,但也可以选择其他方案,

2.2 创建项目

先找个地方建个空目录。

1
mkdir myapp && cd myapp

通过官方工具创建项目,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pnpm dlx create-umi@latest
✔ Install the following package: create-umi? (Y/n) · true
✔ Pick Npm Client › pnpm
✔ Pick Npm Registry › taobao
Write: .gitignore
Write: .npmrc
Write: .umirc.ts
Copy: layouts/index.tsx
Write: package.json
Copy: pages/index.tsx
Copy: pages/users.tsx
Copy: pages/users/foo.tsx
> @ postinstall /private/tmp/sorrycc-vylwuW
> umi setup
info - generate files

也可以使用yarn和npm

1
2
$ npx create-umi@latest
$ yarn create umi

国内建议选 pnpm + taobao 源,速度提升明显。这一步会自动安装依赖,同时安装成功后会自动执行 umi setup 做一些文件预处理等工作。

选择后会自动生成一个最基本的 Umi 项目,并根据选中的客户端和镜像源安装依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── package.json
├── pnpm-lock.yaml
├── src
│ ├── assets
│ │ └── yay.jpg
│ ├── layouts
│ │ ├── index.less
│ │ └── index.tsx
│ └── pages
│ ├── docs.tsx
│ └── index.tsx
├── tsconfig.json
└── typings.d.ts

这样就一键完成 Umi 项目的初始化了。

2.3 参数选项

使用 create-umi 创建项目时,可用的参数如下:

option description
--no-git 创建项目,但不初始化 Git
--no-install 创建项目,但不自动安装依赖

3.运行时配置

运行时配置和配置的区别是他跑在浏览器端,基于此,我们可以在这里写函数、tsx、import 浏览器端依赖等等,注意不要引入 node 依赖。

3.1 配置方式

约定 src/app.tsx 为运行时配置。

3.2 配置

Umi 在 .umirc.tsconfig/config.ts 中配置项目和插件,支持 es6。一份常见的配置如下,

1
2
3
4
5
6
7
8
export default {
base: '/docs/',
publicPath: '/public/',
hash: true,
history: {
type: 'hash',
},
}

.3 配置文件

如果项目的配置不复杂,推荐在 .umirc.ts 中写配置; 如果项目的配置比较复杂,可以将配置写在 config/config.ts 中,并把配置的一部分拆分出去,比如路由配置可以拆分成单独的 routes.ts

1
2
3
4
5
// config/routes.ts

export default [
{ exact: true, path: '/', component: 'index' },
];
1
2
3
4
5
6
7
8
// config/config.ts

import { defineConfig } from 'umi';
import routes from './routes';

export default defineConfig({
routes: routes,
});

推荐两种配置方式二选一,.umirc.ts 优先级更高。

3.4 TypeScript 提示

如果你想在写配置时也有提示,可以通过 umi 的 defineConfig 方法定义配置,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { defineApp } from 'umi';
export default defineApp({
layout: () => {
return {
title: 'umi',
};
},
});

// or
import { RuntimeConfig } from 'umi';
export const layout: RuntimeConfig['layout'] = () => {
return {
title: 'umi',
};
};

3.5 本地临时配置

可以新建 .umirc.local.ts,这份配置会和 .umirc.ts 做 deep merge 后形成最终配置。

注:.umirc.local.ts 仅在 umi dev 时有效。umi build 时不会被加载。

比如,

1
2
3
4
5
// .umirc.ts 或者 config/config.ts
export default { a: 1, b: 2 };

// .umirc.local.ts 或者 config/config.local.ts
export default { c: 'local' };

拿到的配置是:

1
2
3
4
5
{
a: 1,
b: 2,
c: 'local',
}

注意:

  • config/config.ts 对应的是 config/config.local.ts
  • .local.ts 是本地验证使用的临时配置,请将其添加到 .gitignore务必不要提交到 git 仓库中
  • .local.ts 配置的优先级最高,比 UMI_ENV 指定的配置更高

4.目录结构

这里罗列了 Umi 项目中约定(或推荐)的目录结构,在项目开发中,请遵照这个目录结构组织代码。

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
.
├── config
│ └── config.ts
├── dist
├── mock
│ └── app.ts|tsx
├── src
│ ├── .umi
│ ├── .umi-production
│ ├── app.ts
│ ├── layouts
│ │ ├── BasicLayout.tsx
│ │ ├── index.less
│ ├── models
│ │ ├── global.ts
│ │ └── index.ts
│ ├── pages
│ │ ├── index.less
│ │ └── index.tsx
│ ├── utils // 推荐目录
│ │ └── index.ts
│ ├── services // 推荐目录
│ │ └── api.ts
│ ├── global.ts
│ ├── global.(css|less|sass|scss)
│ ├── overrides.(css|less|sass|scss)
│ ├── favicon.(ico|gif|png|jpg|jpeg|svg|avif|webp)
│ └── loading.tsx
├── node_modules
│ └── .cache
│ ├── bundler-webpack
│ ├── mfsu
│ └── mfsu-deps
├── .env
├── plugin.ts
├── .umirc.ts // 与 config/config 文件 2 选一
├── package.json
├── tsconfig.json
└── typings.d.ts

4.1 package.json

包含插件和插件集,以 @umijs/preset-@umijs/plugin-umi-preset-umi-plugin- 开头的依赖会被自动注册为插件或插件集。

4.2 .env

环境变量,比如:

1
2
PORT=8888
COMPRESS=none

4.3 .umirc.ts

config/config.ts 文件功能相同,2 选 1 。.umirc.ts 文件优先级较高

配置文件,包含 Umi 内置功能和插件的配置。

配置文件的优先级见:UMI_ENV

4.4 config/config.ts

.umirc.ts 文件功能相同,2 选 1 。.umirc.ts 文件优先级较高

配置文件,包含 Umi 内置功能和插件的配置。

4.5 dist 目录

执行 umi build 后,产物默认会存放在这里。可通过配置修改产物输出路径。

4.6 mock 目录

存储 mock 文件,此目录下所有 jsts 文件会被解析为 mock 文件。用于本地的模拟数据服务。

4.7 public 目录

此目录下所有文件会被 copy 到输出路径。

4.8 src 目录

4.8.1 .umi 目录

dev 时的临时文件目录,比如入口文件、路由等,都会被临时生成到这里。不要提交 .umi 目录到 git 仓库,他们会在 umi dev 时被删除并重新生成。

4.8.2 .umi-production 目录

build 时的临时文件目录,比如入口文件、路由等,都会被临时生成到这里。不要提交 .umi-production 目录到 git 仓库,他们会在 umi build 时被删除并重新生成。

4.8.3 app.[ts|tsx]

运行时配置文件,可以在这里扩展运行时的能力,比如修改路由、修改 render 方法等。运行时配置是跑在浏览器端,因此我们可以在这里写函数、jsx 语法,import 浏览器端依赖等等。

4.8.4 layouts/index.tsx

约定式路由时的全局布局文件,实际上是在路由外面套了一层。比如,你的路由是:

1
2
3
4
[
{ path: '/', component: './pages/index' },
{ path: '/users', component: './pages/users' },
]

从组件角度可以简单的理解为如下关系:

1
2
3
4
<layout>
<page>1</page>
<page>2</page>
</layout>

4.8.5 pages 目录

所有路由组件存放在这里。使用约定式路由时,约定 pages 下所有的 (j|t)sx? 文件即路由。使用约定式路由,意味着不需要维护可怕的路由配置文件。最常用的有基础路由和动态路由(用于详情页等,需要从 url 取参数的情况)

1.基础路由

假设 pages 目录结构如下:

1
2
3
4
+ pages/
+ users/
- index.js
- index.js

那么,会自动生成路由配置如下:

1
2
3
4
[
{ path: '/', component: './pages/index.js' },
{ path: '/users/', component: './pages/users/index.js' },
];

2.动态路由

约定,带 $ 前缀的目录或文件为动态路由。若 $ 后不指定参数名,则代表 * 通配,比如以下目录结构:

1
2
3
4
5
6
+ pages/
+ foo/
- $slug.js
+ $bar/
- $.js
- index.js

会生成路由配置如下:

1
2
3
4
5
[
{ path: '/', component: './pages/index.js' },
{ path: '/foo/:slug', component: './pages/foo/$slug.js' },
{ path: '/:bar/*', component: './pages/$bar/$.js' },
];

3../src/pages/404.js

当访问的路由地址不存在时,会自动显示 404 页面。只有 build 之后生效。调试的时候可以访问 /404

4.8.6 global.(j|t)sx?

在入口文件最前面被自动引入,可以考虑在此加入 polyfill。Umi 区别于其他前端框架,没有显式的程序主入口,如 src/index.js,所以在引用某些模块的时候,如果模块功能要求在程序主入口添加代码时,你就可以写到这个文件。

4.8.7 global.(css|less|sass|scss)

这个文件不走 css modules,自动被引入,可以写一些全局样式,它的引入位置很靠前,所以优先级相对较低;如果想覆盖三方依赖样式,推荐使用 overrides.(css|less|sass|scss)

4.8.8 overrides.(css|less|sass|scss)

这个文件不走 css modules,自动被引入,专用于覆盖三方依赖的样式;该文件中所有的 CSS 选择器都会被自动加上 body 前缀以确保优先级始终高于原有选择器,这样一来在页面切换时有异步 chunk 动态插入的情况下样式覆盖也能生效。

4.8.9 loading.(tsx|jsx)

定义懒加载过程中要显示的加载动画。Umi 4 默认按页拆包,所以这近似等价于 Umi 3 中的 dynamicImport.loading 选项。

4.8.10 plugin.ts

存在这个文件,会被当前项目加载为 Umi 插件,你可以在这里实现一些插件级的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import type { IApi } from 'umi';

export default (api: IApi) => {
api.onDevCompileDone((opts) => {
opts;
// console.log('> onDevCompileDone', opts.isFirstCompile);
});
api.onBuildComplete((opts) => {
opts;
// console.log('> onBuildComplete', opts.isFirstCompile);
});
api.chainWebpack((memo) => {
memo;
});
};

4.8.11 favicon

约定如果存在 src/favicon.(ico|gif|png|jpg|jpeg|svg|avif|webp) 文件,将会使用它作为构建网页的 shortcut icon,如存在 src/favicon.png 则构建时会生成:

1
<link rel="shortcut icon" href="/favicon.png">

支持多种文件后缀,按以下优先级匹配:

1
2
3
4
5
6
7
8
9
10
const FAVICON_FILES = [
'favicon.ico',
'favicon.gif',
'favicon.png',
'favicon.jpg',
'favicon.jpeg',
'favicon.svg',
'favicon.avif',
'favicon.webp',
];

如果约定方式不满足你的需求,可以使用 favicons 配置。

配置优先级会大于约定

  • 标题: React笔记
  • 作者: Voun
  • 创建于 : 2023-09-04 16:09:00
  • 更新于 : 2024-08-13 05:34:39
  • 链接: http://www.voun.top/2023/09/04/17-React/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
React笔记