React 学习笔记

jsx 语法

jsx 介绍

jsx 是嵌入到 js 中的一种结构语法

书写规范:

  • jsx 顶层只能有一个根元素
  • 通常会在 jsx 外层包裹一个小括号() 可以进行换行书写
  • jsx 中的标签可以是单标签,也可以是双标签 单标签必须以/>结尾

jsx 中的注释

1
2
3
4
<div>
{/* 我是一段注释 */}
<h2>Hello World</h2>
</div>

jsx 嵌入的数据类型

在{}中可以正常显示的内容

  • String
  • Number
  • Array

不能显示的(忽略)

  • null
  • undifined
  • Boolean

可以转为字符串形式显示

_对象不能作为 jsx 的子类_(not valid as a React child)

jsx 中嵌入表达式

1
2
3
4
5
6
7
8
9
10
11
<div>
{/*1.运算符表达式*/}
<h2>{firstname + " " + lastname}</h2>
<h2>{20 * 50}</h2>

{/*2.三元表达式*/}
<h2>{isLogin ? "欢迎回来~" : "请先登录~"}</h2>

{/*3.进行函数调用*/}
<h2>{this.getFullName()}</h2>
</div>

jsx 绑定属性

  • 属性中不能出现 js 的关键字 比如 class 要写成 className label 标签的 for 属性要写成 htmlFor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
render() {
const { title, imgUrl, link, active } = this.state;
return (
<div>
{/* 1.绑定普通属性 */}
<h2 title={title}>我是标题</h2>
<img src={getSizeImage(imgUrl, 140)} alt="" />
<a href={link} target="_blank">百度一下</a>

{/* 2.绑定class */}
<div className="box title">我是div元素</div>
<div className={"box title " + (active ? "active" : "")}>我也是div元素</div>
<label htmlFor=""></label>

{/* 3.绑定style */}
<div style={{ color: "red", fontSize: "50px" }}>我是div,绑定style属性</div>
</div>
)
}
}

jsx 绑定事件

  • 在使用 bind 绑定 this 的时候 可以在构造方法中直接绑定
  • 也可以直接使用箭头函数直接定义方法 就不需要传递 this 箭头函数会一层层往上找 把箭头函数赋值给变量就是在 ES6 中给对象增加属性: class 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
48
49
<script type="text/babel">
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "你好啊",
counter: 100
}

this.btnClick = this.btnClick.bind(this);
}

render() {
return (
<div>
{/* 1.方案一: bind绑定this(显示绑定) */}
<button onClick={this.btnClick}>按钮1</button>
<button onClick={this.btnClick}>按钮2</button>
<button onClick={this.btnClick}>按钮3</button>

{/* 2.方案二: 定义函数时, 使用箭头函数 */}
<button onClick={this.increment}>+1</button>

{/* 2.方案三(推荐): 直接传入一个箭头函数, 在箭头函数中调用需要执行的函数*/}
<button onClick={() => { this.decrement("why") }}>-1</button>
</div>
)
}

btnClick() {
console.log(this.state.message);
}

// increment() {
// console.log(this.state.counter);
// }
// 箭头函数中永远不绑定this
// ES6中给对象增加属性: class fields
increment = () => {
console.log(this.state.counter);
}

decrement(name) {
console.log(this.state.counter, name);
}
}

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

传递事件参数

  • 通过箭头函数的方式直接传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
render() {
return (
<div>
<ul>
{
this.state.movies.map((item, index, arr) => {
return (
<li className="item"
onClick={e => { this.liClick(item, index, e) }}
title="li">
{item}
</li>
)
})
}
</ul>
</div>
)
}

liClick(item, index, event) {
console.log("li发生了点击", item, index, event);
}

jsx 条件渲染

  • 通过 if 判断 适合逻辑代码很多的情况
  • 通过三元运算符
  • &&逻辑与运算符 为 true 时则显示
1
2
3
4
5
6
7
8
9
10
11
  <button onClick={e => this.loginClick()}>{isLogin ? "退出" : "登录"}</button>

<hr />

<h2>{isLogin ? "你好啊, coderwhy" : null}</h2>

{/* 3.方案三: 逻辑与&& */}
{/* 逻辑与: 一个条件不成立, 后面的条件都不会进行判断了 */}
<h2>{isLogin && "你好啊, coderwhy"}</h2>
{isLogin && <h2>你好啊, coderwhy</h2>}
</div>

实现 vue 中的 v-show 效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render() {
const { isLogin } = this.state;
const titleDisplayValue = isLogin ? "block" : "none";
return (
<div>
<button onClick={e => this.loginClick()}>{isLogin ? "退出" : "登录"}</button>
<h2 style={{ display: titleDisplayValue }}>你好啊, coderwhy</h2>
</div>
)
}

loginClick() {
this.setState({
isLogin: !this.state.isLogin
})
}

jsx 列表渲染

  • 最多使用的就是 js 的 map 函数
  • 过滤数组
1
2
3
4
5
6
7
<ul>
{this.state.numbers
.filter((item) => item >= 50)
.map((item) => (
<li>{item}</li>
))}
</ul>
  • 数组截取
1
2
3
4
5
<ul>
{this.state.numbers.slice(0, 4).map((item) => {
return <li>{item}</li>;
})}
</ul>

jsx 本质

jsx 仅仅只是 React.createElement(compoent,props,…children)函数的语法糖

  • 所有的 jsx 最终都会被转换成 React.createElement 的函数调用

比如下面两个效果是一样的 message2 就是 message1 通过 babel 转换的效果

Jsx -> babel -> React.createElement

1
2
const message1 = <h2>Hello React</h2>;
const message2 = React.createElement("h2", null, "Hello React");

createElement 三个参数

createElement三个参数

可以使用https://babeljs.io/repl将jsx转换为React.createElement

虚拟 dom 的创建过程

通过 createElement 最终创建出来的是一个 ReactElement 对象

  1. React 利用 ReactElement 对象组成了一个 JS 的对象树

  2. JS 的对象树就是虚拟 dom(Virtual DOM)

    jsx -> createElement 函数 -> ReactElement(对象树) -> ReactDOM.render -> 真实 DOM

为什么使用虚拟 DOM

  • 很难跟踪状态发生的改变 不方便针对应用程序进行调试
  • 操作真是 DOM 性能较低

虚拟 DOM 帮助我们从命令式编程转到了声明式编程的模式

React 官方的说法:Virtual DOM 是一种编程理念。

  • 在这个理念中,UI 以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的 JavaScript 对象

  • 我们可以通过 ReactDOM.render 让 虚拟 DOM 和 真实 DOM 同步起来,这个过程中叫做协调(Reconciliation);

这种编程的方式赋予了 React 声明式的 API:

  • 你只需要告诉 React 希望让 UI 是什么状态;
  • React 来确保 DOM 和这些状态是匹配的;
  • 你不需要直接进行 DOM 操作,只可以从手动更改 DOM、属性操作、事件处理中解放出来;

React 脚手架解析

三大框架的脚手架

  • Vue:vue-cli
  • Angular:angular-cli
  • React:create-react-app

创建 React 项目的脚手架

1
$ npm install -g create-react-app

创建项目

1
$ create-react-app 项目名称

创建完成后 进入目录 就可以执行yarn start运行项目

脚手架项目结构

├── README.md

├── package-lock.json

├── package.json

├── public

│ ├── favicon.ico 图标

│ ├── index.html 项目入口

│ ├── logo192.png 不同尺寸 logo 在 manifest.json 中使用

│ ├── logo512.png 不同尺寸 logo 在 manifest.json 中使用

│ ├── manifest.json 和 web app 配置相关

│ └── robots.txt 指定搜索引擎可以或者无法爬取哪些文件

└── src

├── App.css App 组件的样式文件

├── App.js App 组件的代码文件

├── App.test.js 测试用例

├── index.css 全局样式

├── index.js 整个应用的入口文件

├── logo.svg 启动项目时看到的 React 图标

├── reportWebVitals.js 帮助我们写好的注册 PWA 相关的代码

└── setupTests.js 测试初始化文件

关于 PWA

  • 全称 Progressive Web App 渐进式 web 应用
  • 一个 PWA 应用首先是一个网页 可以通过 Web 技术编写出一个网页应用
  • 随后添加上 App Manifest 和 service worker 来实现 PWA 的安装和离线等功能
  • 这种 web 存在的形式 称之为 web app

PWA 解决的问题

  • 可以添加至主屏幕 点击主屏幕图标可以实现启动动画以及隐藏地址栏
  • 实现离线缓存功能 即使用户手机没有网络 依然可以使用一些离线功能
  • 实现了消息推送等等一系列类似于 native app 相关的功能

React 组件化开发

React 的组件相对于 Vue 更加灵活,按照不同的方式可以分为多种类型

  • 按组件的定义方式:函数组件、类组件
  • 按组件内部是否有状态需要维护:无状态组件、有状态组件
  • 按组件的不同指责:展示型组件、容器型组件

类组件

  • 组件名称必须上大写字母开头(函数组件也是) 默认会被 jsx 当成组件
  • 类组件需要继承自 React.Component
  • 类组件必须实现 render 函数

使用 class 来定义一个组件:

  • constructor 是可选的 通常在 constructor 中初始化数据
  • this.state 中维护的就是组件内部的数据
  • render()方法是 class 组件中唯一必须实现的方法

函数式组件

使用 function 来定义组件

  • 没有 this 对象
  • 没有内部状态(hooks)
  • 没有生命周期 也会被更新并挂载 但是没有生命周期函数

render 函数的返回值

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

  • react 元素 例如
  • 数组或 fragments:使 render 方法可以返回多个元素
  • Portals:可以渲染子节点到不同的 DOM 子树中
  • 字符串或数值类型:在 DOM 中会被渲染为文本节点
  • 布尔类型或 null:什么都不渲染

组件(类)的生命周期

常用的:

mounting 挂载阶段:构造函数->render->react 更新 dom ->执行 compoentDidMount 回调函数

updating 更新阶段:render->react 更新 dom ->componetDidUpdate 回调函数 新增 props、setState()、forceUpdate()都会触发 updating

unmounting 卸载阶段:compoentWillUnmount 回调函数

组件生命周期

生命周期图谱:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

包括不常用的:

生命周期

生命周期函数所做的事情

constructor

若不初始化 state 或不进行方法绑定,则不需要实现 constructor

constructor 通常做两件事情:

  • 给 this.state 赋值对象来初始化内部的 state
  • 为事件绑定实例(this)
compoentDidMount

compoentDidMount()会在组件挂载后(插入 DOM 树中)立即调用

  • 依赖于 DOM 的操作可以在这里进行
  • 在此处发送网络请求(官方建议)
  • 在此处添加一些订阅(会在 componentWillUnmount 取消订阅)
componetDidUpdate

componetDidUpdate()会在更新后立即调用 首次渲染不会执行

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

三个参数

compoentWillUnmount

compoentWillUnmount()会在组件卸载及销毁之前直接调用

  • 在此方法中执行必要的清理操作 比如清楚 timer,取消网络请求或订阅等

父子间组件通信

父传子通信-类组件

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

class ChildCpn extends Component {
componentDidMount() {
console.log(this.props, "componentDidMount");
}

render() {
// console.log(this.props, "render");
const { name, age, height } = this.props;
return <h2>子组件展示数据: {name + " " + age + " " + height}</h2>;
}
}

export default class App extends Component {
render() {
return (
<div>
<ChildCpn name="why" age="18" height="1.88" />
<ChildCpn name="kobe" age="40" height="1.98" />
</div>
);
}
}

父传子通信-函数式组件

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

function ChildCpn(props) {
const { name, age, height } = props;

return <h2>{name + age + height}</h2>;
}

export default class App extends Component {
render() {
return (
<div>
<ChildCpn name="why" age="18" height="1.88" />
<ChildCpn name="kobe" age="40" height="1.98" />
</div>
);
}
}

参数验证和默认值

react15 之后 把参数验证库导入了 prop-types

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
import React, { Component } from "react";
import PropTypes from "prop-types";
function ChildCpn(props) {
const { name, age, height } = props;
const { names } = props;

return (
<div>
<h2>{name + age + height}</h2>
<ul>
{names.map((item, index) => {
return <li>{item}</li>;
})}
</ul>
</div>
);
}

class ChildCpn2 extends Component {
// es6中的class fields写法
static propTypes = {};

static defaultProps = {};
}

ChildCpn.propTypes = {
name: PropTypes.string.isRequired, //isRequired为必传
age: PropTypes.number,
height: PropTypes.number,
names: PropTypes.array,
};

ChildCpn.defaultProps = {
name: "why",
age: 30,
height: 1.98,
names: ["aaa", "bbb"],
};

export default class App extends Component {
render() {
return (
<div>
<ChildCpn name="why" age={18} height={1.88} names={["abc", "cba"]} />
<ChildCpn name="kobe" age={40} height={1.98} names={["nba", "mba"]} />
<ChildCpn />
</div>
);
}
}

子传父通信-函数传递

同样通过属性传递 传递一个函数即可 这个函数也可以带参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { Component } from "react";

class CounterButton extends Component {
render() {
const { onClick } = this.props;
return <button onClick={onClick(1)}>+1</button>;
}
}

export default class App extends Component {
constructor(props) {
super(props);

this.state = {
counter: 0,
};
}

render() {
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={(e) => this.increment()}>+</button>
<CounterButton onClick={(index) => this.increment()} name="why" />
</div>
);
}

increment(index) {
this.setState({
counter: this.state.counter + 1,
});
}
}

插槽

插槽的实现方式有两种

方式一:通过在组件双标签中直接将插槽的内容写入 引用直接通过 this.props.children[]使用 这种情况适用于只有一个插槽的情况

插槽1

方式二:直接通过属性传递标签 直接使用 props

插槽2

在 jsx 中使用 a 标签 href 属性值为#会出现警告 可以写成’/#’

跨组件通信

组件层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

  • React 提供了一个 API:Context;
  • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
  • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;
Context 相关 api

React.createContext

  • 创建一个需要共享的 Context 对象:
  • 如果一个组件订阅了 Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的 context 值;
  • defaultValue 是组件在顶层查找过程中没有找到对应的 Provider,那么就使用默认值
1
2
3
4
5
// 创建Context对象
const UserContext = React.createContext({
nickname: "aaaa",
level: -1,
});

Context.Provider

  • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
  • Provider 接收一个 value 属性,传递给消费组件;
  • 一个 Provider 可以和多个消费组件有对应关系;
  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
1
2
3
<UserContext.Provider value={this.state}>
<Profile />
</UserContext.Provider>

Class.contextType

  • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
  • 这能让你使用 this.context 来消费最近 Context 上的那个值;
  • 你可以在任何生命周期中访问到它,包括 render 函数中;
1
ProfileHeader.contextType = UserContext;

Context.Consumer

  • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
  • 这里需要 函数作为子元素(function as child)这种做法;
  • 这个函数接收当前的 context 值,返回一个 React 节点;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ProfileHeader() {
return (
<UserContext.Consumer>
{(value) => {
return (
<div>
<h2>用户昵称: {value.nickname}</h2>
<h2>用户等级: {value.level}</h2>
</div>
);
}}
</UserContext.Consumer>
);
}

setState

setState({})

setState 异步更新

下图最终打印结果是 Hello World;可见 setState 是异步的操作,并不能在执行完 setState 之后立马拿到最新的 state 的结果

为什么 setState 设计为异步?

React 核心成员(Redux 的作者)Dan Abramov 也有对应的回复, https://github.com/facebook/react/issues/11527#issuecomment-360199710;

  • setState 设计为异步,可以显著的提升性能;
    • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步;
    • state 和 props 不能保持一致性,会在开发中产生很多的问题;
获取异步的结果

方式一:setState 的回调

  • setState 接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
  • 格式如下:setState(partialState, callback)

方式二:在生命周期函数

setState 同步更新的情况

setState 数据合并

setState 在进行更新时 若有多个属性,会自动进行合并。 源码中使用了 Object.assign({}, prevState, partialState)将原对象和新对象进行了合并

1
2
3
4
this.setState({
message: "你好啊,李银河",
});
// Object.assign({}, this.state, {message: "你好啊,李银河"})

setState 本身也有合并

如下效果 counter 只加了一次 1 不会加三次

若需要将 setState 合并时进行累加 就需要给 setState 传递一个函数

这是因为第一种情况的 prevState 始终是初始值 而若给 setState 传递一个函数 prevState 就是上一次+1 的操作

React 性能优化

React 更新机制

react 更新流程

React 在 props 或 state 发生改变时,会调用 React 的 render 方法,会创建一颗不同的树。

React 需要基于这两颗不同的树之间的差别来判断如何有效的更新 UI:

  • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n^3 ),其中 是树中元素的数量;
  • https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
  • 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
  • 这个开销太过昂贵了,React 的更新性能会变得非常低效;

于是,React 对这个算法进行了优化,将其优化成了 O(n),如何优化的呢?

  • 同层节点之间相互比较,不会垮节点比较;
  • 不同类型的节点,产生不同的树结构;
  • 开发中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定

情况一:对比不同类型的元素

1
2
3
4
5
6
7
<div>
<Counter />
</div>

<span>
<Counter />
</span>

情况二:对比同一类型的元素

当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。

比如下面的代码更改:

  • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性;

比如下面的代码更改:

  • 当更新 style 属性时,React 仅更新有所更变的属性。
  • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。

如果是同类型的组件元素:

  • 组件会保持不变,React 会更新该组件的 props,并且调用 componentWillReceiveProps() 和 componentWillUpdate() 方法;
  • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;

情况三:对子节点进行递归

keys 优化

在遍历列表时,总是会提示一个警告,让我们加入一个 key 属性:

方式一:在最后位置插入数据

  • 这种情况,有无 key 意义并不大

方式二:在前面插入数据

  • 这种做法,在没有 key 的情况下,所有的 li 都需要进行修改;

当子元素(这里的 li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

  • 在下面这种场景下,key 为 111 和 222 的元素仅仅进行位移,不需要进行任何的修改;
  • 将 key 为 333 的元素插入到最前面的位置即可;

key 的注意事项:

  • key 应该是唯一的;
  • key 不要使用随机数(随机数在下一次 render 时,会重新生成一个数字);
  • 使用 index 作为 key,对性能是没有优化的;

render 函数被重复调用问题

只要是修改了 App 中的数据,所有的组件都需要重新 render,进行 diff 算法,性能必然是很低的:

  • 事实上,很多的组件没有必须要重新 render;
  • 它们调用 render 应该有一个前提,就是依赖的数据(state、 props)发生改变时,再调用自己的 render 方法;

如何来控制 render 方法是否被调用呢?通过 shouldComponentUpdate 方法即可;

shouldComponentUpdate 方法

React 给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为 SCU),这个方法接受参数,并且需要有返回值:

该方法有两个参数:

  • 参数一:nextProps 修改之后,最新的 props 属性
  • 参数二:nextState 修改之后,最新的 state 属性

该方法返回值是一个 boolean 类型

  • 返回值为 true,那么就需要调用 render 方法;
  • 返回值为 false,那么久不需要调用 render 方法;
  • 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;

比如我们在 App 中增加一个 message 属性:

  • jsx 中并没有依赖这个 message,那么它的改变不应该引起重新渲染;
  • 但是因为 render 监听到 state 的改变,就会重新 render,所以最后 render 方法还是被重新调用了;

PureComponent

如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。

  • 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?
  • props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false;

事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了,如何实现呢?

  • 将 class继承自 PureComponent
高阶函数 memo

若需要在函数式组件中避免 render 被重复调用,我们需要使用一个高阶组件 memo:

  • 我们将之前的 Header、Banner、ProductList 都通过 memo 函数进行一层包裹;
  • Footer 没有使用 memo 函数进行包裹;
  • 最终的效果是,当 counter 发生改变时,Header、Banner、ProductList 的函数不会重新执行,而 Footer 的函数会被重新执行;

setState 不可变数据

state 是不可变的 不能脱离 setState 直接更改 state 的数据 会影响性能 继承 PureComponet 时会出现问题

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.在开发中不要这样来做
// const newData = {name: "tom", age: 30}
// this.state.friends.push(newData);
// this.setState({
// friends: this.state.friends
// });

// 2.推荐做法
const newFriends = [...this.state.friends];
newFriends.push({ name: "tom", age: 30 });
this.setState({
friends: newFriends,
});

全局事件传递

通过 Context 主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,在 React 中,我们可以依赖一个使用较多的库 events 来完成对应的操作;

我们可以通过 npm 或者 yarn 来安装 events:

1
$ yarn add events

events 常用的 API:

  • 创建 EventEmitter 对象:eventBus 对象;

  • ```javascript
    import { EventEmitter } from “events”;

    // 事件总线: event bus
    const eventBus = new EventEmitter();

    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

    - 发出事件:eventBus.emit("事件名称", 参数列表);

    - 监听事件:eventBus.addListener("事件名称", 监听函数);

    - 移除事件:eventBus.removeListener("事件名称", 监听函数);

    ### refs 的使用

    在 React 的开发模式中,通常情况下不需要、也不建议直接操作 DOM 原生,但是某些特殊的情况,确实需要获取到 DOM 进行某些操作:

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

    创建 refs 来获取对应的 DOM 目前有三种方式:

    1. 方式一:传入字符串(不推荐 官方更新后已删除该用法)

    - 使用时通过 this.refs.传入的字符串格式获取对应的元素;

    2. 方式二:传入一个对象

    - 对象是通过 React.createRef() 方式创建出来的;
    - 使用时获取到创建的对象其中有一个 current 属性就是对应的元素;

    3. 方式三:传入一个函数

    - 该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;

    - 使用时,直接拿到之前保存的元素对象即可;

    若 ref 绑定的是一个组件 则可以直接通过 ref 访问组件的方法

    ```javascript
    export default class App extends PureComponent {
    constructor(props) {
    super(props);

    this.titleRef = createRef();
    this.counterRef = createRef();
    this.titleEl = null;
    }

    render() {
    return (
    <div>
    {/* <h2 ref=字符串/对象/函数>Hello React</h2> */}
    <h2 ref="titleRef">Hello React</h2>
    {/* 目前React推荐的方式 */}
    <h2 ref={this.titleRef}>Hello React</h2>
    <h2 ref={(arg) => (this.titleEl = arg)}>Hello React</h2>
    <button onClick={(e) => this.changeText()}>改变文本</button>
    <hr />
    <Counter ref={this.counterRef} />
    <button onClick={(e) => this.appBtnClick()}>App按钮</button>
    </div>
    );
    }

    changeText() {
    // 1.使用方式一: 字符串(不推荐, 后续的更新会删除)
    this.refs.titleRef.innerHTML = "Hello Coderwhy";
    // 2.使用方式二: 对象方式
    this.titleRef.current.innerHTML = "Hello JavaScript";
    // 3.使用方式三: 回调函数方式
    this.titleEl.innerHTML = "Hello TypeScript";
    }

    appBtnClick() {
    this.counterRef.current.increment();
    }
    }

ref 的类型

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

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

函数式组件是没有实例的,所以无法通过 ref 获取他们的实例:

  • 但是某些时候,我们可能想要获取函数式组件中的某个 DOM 元素; 这个时候可以通过 React.forwardRef ,后面学习 hooks 中如何使用 ref;

受控组件和非受控组件

在 React 中,HTML 表单的处理方式和普通的 DOM 元素不太一样:表单元素通常会保存在一些内部的 state。

比如下面的 HTML 表单元素:

  • 这个处理方式是 DOM 默认处理 HTML 表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
  • 在 React 中,并没有禁止这个行为,它依然是有效的;
  • 但是通常情况下会使用 JavaScript 函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
  • 实现这种效果的标准方式是使用“受控组件”;

受控组件使用

在 HTML 中,表单元素(如input、 textarea 和 select)之类的表单元素通常自己维护 state,并根据用户输入进行更新。

而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

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

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。

由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export default class App extends PureComponent {
constructor(props) {
super(props);

this.state = {
username: "",
};
}

render() {
return (
<div>
<form onSubmit={(e) => this.handleSubmit(e)}>
<label htmlFor="username">
用户:
{/* 受控组件 */}
<input
type="text"
id="username"
onChange={(e) => this.handleChange(e)}
value={this.state.username}
/>
</label>
<input type="submit" value="提交" />
</form>
</div>
);
}

handleSubmit(event) {
event.preventDefault();
console.log(this.state.username);
}

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

textarea 标签

texteare 标签和 input 比较相似:

select 标签

select 标签的使用也非常简单,只是它不需要通过 selected 属性来控制哪一个被选中,它可以匹配 state 的 value 来选中。

1
2
3
4
5
6
7
8
9
10
11
12
13
        <select name="fruits"
onChange={e => this.handleChange(e)}
value={this.state.fruits}>
<option value="apple">苹果</option>
<option value="banana">香蕉</option>
<option value="orange">橘子</option>
</select>

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

处理多个输入

多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:

可以使用 ES6 的一个语法:计算属性名(Computed property )

非受控组件

React 推荐大多数情况下使用 受控组件 来处理表单数据:

  • 一个受控组件中,表单数据是由 React 组件来管理的;另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;

如果要使用非受控组件中的数据,那么我们需要使用 ref 来从 DOM 节点中获取表单数据。

使用 ref 来获取 input 元素;

  • 在非受控组件中通常使用 defaultValue 来设置默认值;
  • 同样,input type=”checkbox” 和 input type=”radio” 支持 defaultChecked,select 和 textarea 支 持 defaultValue。

高阶组件

高阶函数的维基百科定义:至少满足以下条件之一:

  • 接受一个或多个函数作为输入;
  • 输出一个函数;

JavaScript 中比较常见的 filter、map、reduce 都是高阶函数。

那么什么是高阶组件呢?

  • 高阶组件的英文是 Higher-Order Components,简称为 HOC;
  • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数

我们可以进行如下的解析:

  • 首先, 高阶组件 本身不是一个组件,而是一个函数
  • 其次,这个函数的参数是一个组件,返回值也是一个组件;

![image-20230119203550656](/Users/chenrongqi/Library/Application Support/typora-user-images/image-20230119203550656.png)

高阶组件应用

props 的增强

不修改原有代码的情况下,添加新的 props

1
2
3
4
5
function enhanceRegionProps(WrappedComponent) {
return (props) => {
return <WrappedComponent {...props} region="中国" />;
};
}

利用高阶组件来共享 Context

1
2
3
4
5
6
7
8
9
10
11
function withUser(WrappedComponent) {
return (props) => {
return (
<UserContext.Consumer>
{(user) => {
return <WrappedComponent {...props} {...user} />;
}}
</UserContext.Consumer>
);
};
}
登录权限判断

在开发中,我们可能遇到这样的场景:

  • 某些页面是必须用户登录成功才能进行进入;
  • 如果用户没有登录成功,那么直接跳转到登录页面;

这个时候,就可以使用高阶组件来完成鉴权操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function withAuth(WrappedComponent) {
const NewCpn = (props) => {
const { isLogin } = props;
if (isLogin) {
return <WrappedComponent {...props} />;
} else {
return <LoginPage />;
}
};

NewCpn.displayName = "AuthCpn";

return NewCpn;
}

const AuthCartPage = withAuth(CartPage);
// 传递属性
<AuthCartPage isLogin={true} />
生命周期劫持

可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function withRenderTime(WrappedComponent) {
return class extends PureComponent {
// 即将渲染获取一个时间 beginTime
UNSAFE_componentWillMount() {
this.beginTime = Date.now();
}

// 渲染完成再获取一个时间 endTime
componentDidMount() {
this.endTime = Date.now();
const interval = this.endTime - this.beginTime;
console.log(`${WrappedComponent.name}渲染时间: ${interval}`);
}

render() {
return <WrappedComponent {...this.props} />;
}
};
}
HOC 的缺陷

HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;

HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突;

Hooks 的出现,是开创性的,它解决了很多 React 之前的存在的问题

比如 this 指向问题、比如 hoc 的嵌套复杂度问题等等;

ref 的转发

ref 不能应用于函数式组件,因为函数式组件没有实例,所以不能获取到对应的组件对象

但是,在开发中我们可能想要获取函数式组件中某个元素的 DOM,这个时候我们应该如何操作呢?

  • 方式一:直接传入 ref 属性(错误的做法)
  • 方式二:通过 forwardRef 高阶函数

forwardRef 函数第二个回调参数就是 props 传递的 ref

1
2
3
const Profile = forwardRef(function (props, ref) {
return <p ref={ref}>Profile</p>;
});

Portals 的使用

某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的 DOM 元 素上的)。

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:

  • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
  • 第二个参数(container)是一个 DOM 元素;
1
2
3
4
5
6
7
8
class Modal extends PureComponent {
render() {
return ReactDOM.createPortal(
this.props.children,
document.getElementById("modal")
)
}
}

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的

fragment

在之前的开发中,我们总是在一个组件中返回内容时包裹一个 div 元素:

我们又希望可以不渲染这样一个 div 应该如何操作呢?使用 Fragment

Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;

React 还提供了 Fragment 的短语法:

  • 它看起来像空标签 <> </>;
  • 但是,如果我们需要在 Fragment 中添加 key,那么就不能使用短语法

StrictMode

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。

  • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
  • 它为其后代元素触发额外的检查和警告;
  • 严格模式检查仅在开发模式下运行;_它们不会影响生产构建_;

可以为应用程序的任何部分启用严格模式:

  • 不会对 Header 组件运行严格模式检查;
  • 但是,Home 以及它们的所有后代元素都将进行检查;
1
2
3
4
<Header/>
<StrictMode>
<Home/>
</StrictMode>

严格模式检查的是什么

  1. 识别不安全的生命周期:

  2. 使用过时的 ref API

  3. 使用废弃的 findDOMNode 方法

    • 在之前的 React API 中,可以通过 findDOMNode 来获取 DOM,不过已经不推荐使用了
  4. 检查意外的副作用

    • 这个组件的 constructor 会被调用两次;

    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;

    • 在生产环境中,是不会被调用两次的;

  5. 检测过时的 context API

    • 早期的 Context 是通过 static 属性声明 Context 对象属性,通过 getChildContext 返回 Context 对象等方式来使用 Context 的;

    • 目前这种方式已经不推荐使用

React 中的样式

内联样式

内联样式是官方推荐的一种 css 样式的写法:

  • style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串;
  • 并且可以引用 state 中的状态来设置相关的样式;

内联样式的优点:

  1. 内联样式, 样式之间不会有冲突

  2. 可以动态获取当前 state 中的状态

内联样式的缺点:

  1. 写法上都需要使用驼峰标识

  2. 某些样式没有提示

  3. 大量的样式, 代码混乱

  4. 某些样式无法编写(比如伪类/伪元素)

所以官方依然是希望内联合适和普通的 css 来结合编写

普通的 css

普通的 css 通常会编写到一个单独的文件,之后再进行引入。

这样的编写方式和普通的网页开发中编写方式是一致的:

  • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
  • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
  • 但是普通的 css 都属于全局的 css,样式之间会相互影响;

这种编写方式最大的问题是样式之间会相互层叠掉;

CSS modules

css modules 并不是 React 特有的解决方案,而是所有使用了类似于 webpack 配置的环境下都可以使用的。

  • 但是,如果在其他项目中使用个,那么我们需要自己来进行配置,比如配置 webpack.config.js 中的 modules: true 等。

React 的脚手架已经内置了 css modules 的配置:

  • .css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss 等,之后就可以引用并且进行使用了;

css modules 确实解决了局部作用域的问题,也是很多人喜欢在 React 中使用的一种方案。

但是这种方案也有自己的缺陷:

  • 引用的类名,不能使用连接符(.home-title),在 JavaScript 中是不识别的;
  • 所有的 className 都必须使用{style.className} 的形式来编写;
  • 不方便动态来修改某些样式,依然需要使用内联样式的方式;

如果你觉得上面的缺陷还算 OK,那么你在开发中完全可以选择使用 css modules 来编写,并且也是在 React 中很受欢迎的一种方式。

CSS in JS

实际上,官方文档也有提到过 CSS in JS 这种方案:

  • “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;
  • 注意此功能并不是 React 的一部分,而是由第三方库提供。 React 对样式如何定义并没有明确态度;

在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。

  • 但是在前面的学习中提到过,React 的思想中认为逻辑本身和 UI 是无法分离的,所以才会有了 JSX 的语法。
  • 样式呢?样式也是属于 UI 的一部分;
  • 事实上 CSS-in-JS 的模式就是一种将样式(CSS)也写入到 JavaScript 中的方式,并且可以方便的使用 JavaScript 的状态;
  • 所以 React 有被人称之为 All in JS;

当然,这种开发的方式也受到了很多的批评:

styled-components 插件

批评声音虽然有,但是在我们看来很多优秀的 CSS-in-JS 的库依然非常强大、方便:

  • CSS-in-JS 通过 JavaScript 来为 CSS 赋予一些能力,包括类似于 CSS 预处理器一样的样式嵌套、函数定义、逻辑复用、动态修
  • 改状态等等;
  • 依然 CSS 预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
  • 所以,目前可以说 CSS-in-JS 是 React 编写 CSS 最为受欢迎的一种解决方案;

目前比较流行的 CSS-in-JS 的库有:

  • styled-components
  • emotion
  • glamorous

目前可以说 styled-components 依然是社区最流行的 CSS-in-JS 库

安装 styled-components:yarn add styled-components

标签模板字符串

ES6 中增加了模板字符串的语法,模板字符串还有另外一种用法:标签模板字符串(Tagged Template

Literals)。

正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外一种调用方式:

如果我们在调用的时候插入其他的变量:

  • 模板字符串被拆分了;
  • 第一个元素是数组,是被模块字符串拆分的字符串组合;
  • 后面的元素是一个个模块字符串传入的内容;

在 styled component 中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的

Snipaste_2023-01-20_21-02-21

styled 的基本使用

styled-components 的本质是通过函数的调用,最终创建出一个组件:

  • 这个组件会被自动添加上一个不重复的 class;

styled-components 会给该 class 添加相关的样式;

另外,它支持类似于 CSS 预处理器一样的样式嵌套:

  • 支持直接子代选择器或后代选择器,并且 直接编写样式;
  • 可以通过&符号获取当前元素;
  • 直接伪类选择器、伪元素等;

props、attrs 属性

props 可以穿透,props 可以被传递给 styled 组件

  • 获取 props 需要通过${}传入一个插值函数,props 会作为该函数的参数;
  • 这种方式可以有效的解决动态样式的问题;

也可以添加 attrs 属性

styled 高级特性

支持样式继承

styled 可以设置主题

React 中 Ant Design 的使用

React 中添加 class

React 在 JSX 给了开发者足够多的灵活性,可以像编写 JavaScript 代码一样,通过一些逻辑来决定是否添加某些 class:

这个时候我们可以借助于一个第三方的库:classnames,用于动态添加 classnames 的一个库

AntDesign 的安装

使用 npm 或 yarn 安装

1
$ npm install antd –save

1
$ yarn add antd

需要在 index.js 中引入全局的 Antd 样式:

1
import "antd/dist/antd.css";

在 App.js 中就可以使用一些组件了:

考虑一个问题:Antd 是否会将一些没有用的代码(组件或者逻辑代码)引入,造成包很大呢?

antd 官网有提到:antd 的 JS 代码默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from ‘antd’ 就会有按需加载的效果。

使用 craco 修改默认配置

对主题等相关的高级特性进行配置,需要修改 create-react-app 的默认配置。

可以通过 yarn run eject 来暴露出来对应的配置信息进行修改;

但是对于 webpack 并不熟悉的人来说,直接修改 CRA 的配置是否会给你的项目带来负担,甚至会增加项目的隐患和不稳定性

所以,在项目开发中是不建议大家直接去修改 CRA 的配置信息的;

那么如何来进行修改默认配置呢?社区目前有两个比较常见的方案:

  • react-app-rewired + customize-cra;(这个是 antd 早期推荐的方案)
  • craco;(目前 antd 推荐的方案)

使用步骤

第一步:安装 craco: yarn add @craco/craco

第二步:修改 package.json 文件

原本启动时,是通过 react-scripts 来管理的;

现在启动时,需要通过 craco 来管理;

1
2
3
4
5
6
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},

第三步:在根目录下创建 craco.config.js 文件用于修改默认配置

1
2
3
module.exports = {
// 配置文件
};

配置主题

  • 按照 配置主题 的要求,自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能:

    • 可以引入 craco-less 来帮助加载 less 样式和修改变量;
  • 安装 craco-less: yarn add craco-less

  • 修改 craco.config.js 中的 plugins:

    • 使用 modifyVars 可以在运行时修改 LESS 变量;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const CracoLessPlugin = require("craco-less");
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { "@primary-color": "#1DA57A" },
javascriptEnabled: true,
},
},
},
},
],
};

引入 antd 的样式时,引入 antd.less 文件: import ‘antd/dist/antd.less’

修改后重启 yarn start,如果看到一个绿色的按钮就说明配置成功了。

配置别名

1
2
3
4
5
6
7
8
9
10
11
const path = require("path");
const resolve = (dir) => path.resolve(__dirname, dir);

module.exports = {
webpack: {
alias: {
"@": resolve("src"),
components: resolve("src/components"),
},
},
};

React中的动画-react-transition-group

react-transition-group介绍

React社区提供了react-transition-group用来完成过渡动画。React曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。

这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装:

npm

1
$ npm install react-transition-group --save

yarn

1
$ yarn add react-transition-group

react-transition-group本身非常小,不会为我们应用程序增加过多的负担。

react-transition-group主要组件

react-transition-group主要包含四个组件:

Transition

  • 该组件是一个和平台无关的组件(不一定要结合CSS);
  • 在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;

CSSTransition

  • 在前端开发中,通常使用CSSTransition来完成过渡动画效果

SwitchTransition

  • 两个组件显示和隐藏切换时,使用该组件

TransitionGroup

  • 将多个动画组件包裹在其中,一般用于列表中元素的动画;

CSSTransition

CSSTransition是基于Transition组件构建的:

CSSTransition执行过程中,有三个状态:appear、enter、exit;

它们有三种状态,需要定义对应的CSS样式:

  • 第一类,开始状态:对于的类是-appear、-enter、exit;
  • 第二类:执行动画:对应的类是-appear-active、-enter-active、-exit-active;
  • 第三类:执行结束:对应的类是-appear-done、-enter-done、-exit-done;

CSSTransition常见对应的属性:

in:触发进入或者退出状态

  • 如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉;
  • 当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;

当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;

classNames:动画class的名称

决定了在编写css时,对应的class名称:比如card-enter、card-enter-active、card-enter-done;

timeout: 过渡动画的时间

appear: 是否在初次进入添加动画(需要和in同时为true)

unmountOnExit:退出后卸载组件

官网:https://reactcommunity.org/react-transition-group/transition

CSSTransition对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作

  • onEnter:在进入动画之前被触发;
  • onEntering:在应用进入动画时被触发;
  • onEntered:在应用进入动画结束后被触发;

SwitchTransition

SwitchTransition可以完成两个组件之间切换的炫酷动画:

  • 比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入;
  • 这个动画在vue中被称之为 vue transition modes;
  • react-transition-group中使用SwitchTransition来实现该动画;

SwitchTransition中主要有一个属性:mode,有两个值

  • in-out:表示新组件先进入,旧组件再移除;
  • out-in:表示就组件先移除,新组建再进入;

SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;

SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性

TransitionGroup

当有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<TransitionGroup>
{
this.state.names.map((item, index) => {
return (
<CSSTransition key={item}
timeout={500}
classNames="item">
<div>
{item}
<button onClick={e => this.removeItem(index)}>-</button>
</div>
</CSSTransition>
)
})
}
</TransitionGroup>

Redux

纯函数

js中的纯函数

函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;

在React中,纯函数的概念非常重要,学习Redux时也非常重要

纯函数的维基百科定义:

在程序设计中,若一个函数符合一下条件,那么这个函数被称为纯函数:

  • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

当然上面的定义会过于的晦涩,所以简单总结一下:

  • 确定的输入,一定会产生确定的输出;
  • 函数在执行过程中,不能产生副作用;

React中的纯函数

为什么纯函数在函数式编程中非常重要呢?

  • 因为你可以安心的写和安心的用;
  • 你在写的时候保证了函数的纯度,只是但是实现自己的业务逻辑即可,不需要关心传入的内容或者依赖其他的外部变量;
  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:

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

Redux介绍

  • JS的状态容器,提供了可预测的状态管理 用来控制和追踪state
  • 还可以和其他界面库一起使用(Vue等)

Redux核心理念

action

所有数据的变化 需要通过派发(dispatch)action来更新

action是一个JS对象 用来描述这次更新的type和content

reducer

  • 是一个纯函数
  • 作用就是将传入的state和action联系起来 返回一个state

三大原则

  • 单一数据源 整个应用的state被存储在一颗object tree中 并且object tree只存储在一个store中
  • state是只读的
  • 只能使用纯函数来执行修改

Redux使用

安装redux:

1
2
3
$ npm install redux --save

$ yarn add redux

Redux的使用过程

  1. 创建一个对象,作为我们要保存的状态:

  2. 创建Store来存储这个state

  • 创建store时必须创建reducer;
  • 我们可以通过 store.getState 来获取当前的state
  1. 通过action来修改state
  • 通过dispatch来派发action;
  • 通常action中都会有type属性,也可以携带其他的数据;
  1. 修改reducer中的处理代码
  • reducer是一个纯函数,不需要直接修改state;
  1. 可以在派发action之前,监听store的变化:
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
// 1.导入redux(不能通过ES6的方式)
// import/export 13.2.0开始支持
// commonjs一种实现 -> nodejs
const redux = require('redux');

const initialState = {
counter: 0
}

// reducer
function reducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, counter: state.counter + 1 }
case "DECREMENT":
return { ...state, counter: state.counter - 1 }
case "ADD_NUMBER":
return { ...state, counter: state.counter + action.num }
case "SUB_NUMBER":
return { ...state, counter: state.counter - action.num }
default:
return state;
}
}

// store(创建的时候需要传入一个reducer)
const store = redux.createStore(reducer)

// 订阅store的修改
store.subscribe(() => {
console.log("counter:", store.getState().counter);
})

// actions
const action1 = { type: "INCREMENT" };
const action2 = { type: "DECREMENT" };

const action3 = { type: "ADD_NUMBER", num: 5 };
const action4 = { type: "SUB_NUMBER", num: 12 };

// 派发action
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action2);
store.dispatch(action3);
store.dispatch(action4);

redux结构划分

将所有的逻辑代码写到一起,当redux变得复杂时代码就难以维护。会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。

注意:node中对ES6模块化的支持

从node v13.2.0开始,node才对ES6模块化提供了支持:

node v13.2.0之前,需要进行如下操作:

  • 在package.json中添加属性: “type”: “module”;
  • 在执行命令中添加如下选项:node –experimental-modules src/index.js;

node v13.2.0之后,只需要进行如下操作:在package.json中添加属性: “type”: “module”;

  • 注意:导入文件时,需要跟上.js后缀名;

redux使用流程图

Redux融入react

自定义connect函数

自定义connect函数简化了在react中每次都需要在compontDidMount和compontWillUnMount中订阅和取消订阅store的操作,在使用时只需要把store和要触发的dispatch函数传入

![image-20230129221016444](/Users/chenrongqi/Library/Application Support/typora-user-images/image-20230129221016444.png)

上面的connect函数有一个很大的缺陷:依赖导入的store

如果将其封装成一个独立的库,需要依赖用于创建的store,应该如何去获取?

正确的做法是提供一个Provider,Provider来自于我们创建的Context,让用户将store传入到value中即可;

context处理store

react-redux使用

安装react-redux:yarn add react-redux

Redux中异步操作

网络请求可以在class组件的componentDidMount中发送,所以可以有这样的结构:

上面的流程有一个缺陷:

  • 必须将网络请求的异步代码放到组件的生命周期中来完成;

事实上,网络请求到的数据也属于状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;

redux-thunk

默认情况下的dispatch(action),action需要是一个JavaScript的对象;

redux-thunk可以让dispatch(action函数),action可以是一个函数;

该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;

  • dispatch函数用于我们之后再次派发action;
  • getState函数考虑到一些操作需要依赖原来的状态,用于可以获取之前的一些状态;
  1. 安装redux-thunkyarn add redux-thunk

  2. 在创建store时传入应用了middleware的enhance函数

  • 通过applyMiddleware来结合多个Middleware, 返回一个enhancer;
  • 将enhancer作为第二个参数传入到createStore中;

  1. 定义返回一个函数的action:
  • 这里不是返回一个对象了,而是一个函数;
  • 该函数在dispatch之后会被执行;

Redux-devtools

redux官网为我们提供了redux-devtools的工具; 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;

安装该工具需要两步:

  • 第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可,其他方法可以参考GitHub);
  • 第二步:在redux中继承devtools的中间件
1
2
3
4
5
6
7
8
9
10
11
import { createStore, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';

import reducer from './reducer.js';

// composeEnhancers函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;

const store = createStore(reducer, composeEnhancers(storeEnhancer));

export default store;

Devtools工具栏如图所示 可以追踪到每一个state的改变

Redux-saga

generator使用

在JavaScript中编写一个普通的函数,进行调用会立即拿到这个函数的返回结果。

如果将这个函数编写成一个生成器函数。

调用iterator的next函数,会销毁一次迭代器,并且返回一个yield的结果。

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
// generator: 生成器
// 1.普通函数
// function foo() {
// console.log("foo被执行");
// }

// foo();
// 2.生成器函数的定义
// 生成器函数
function* foo() {
console.log("111");
yield "Hello";
console.log("222");
yield "World";
console.log("333");
yield "coderwhy";
console.log("444");
}

// iterator: 迭代器
const result = foo();
console.log(result);

// 3.使用迭代器
// 调用一次next, 就会消耗一次迭代器
// const res1 = result.next();
// console.log(res1);
// const res2 = result.next();
// console.log(res2);
// const res3 = result.next();
// console.log(res3);
// const res4 =result.next();
// console.log(res4);

// 4.生成器函数中代码的执行顺序

// 5.练习: 定义一个生成器函数, 依次可以生成1~10的数字
function* generateNumber() {
for (var i = 1; i <= 10; i++) {
yield i;
}
}

// const numIt = generateNumber();
// console.log(numIt.next().value);

// 6.generator和Promise一起来使用
function* bar() {
console.log("1111");
const result = yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hello Generator");
}, 3000);
});
console.log(result);
}

const it = bar();
it.next().value.then(res => {
it.next(res)
})

(preValue = 0, item) => { };
(preState = defaultState, action) => { };

["abc", "cba"].reduce((preValue, item) => { }, 0)

redux-saga使用

redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活。

Redux-saga的使用步骤如下

  1. 安装redux-saga:yarn add redux-saga

  2. 集成redux-saga中间件

  • 导入创建中间件的函数;
  • 通过创建中间件的函数,创建中间件,并且放到applyMiddleware函数中;
  • 启动中间件的监听过程,并且传入要监听的saga;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';

import saga from './saga';
import reducer from './reducer.js';

// 应用一些中间件
// 1.引入thunkMiddleware中间件(上面)
// 2.创建sagaMiddleware中间件
const sagaMiddleware = createSagaMiddleware();

const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware);
const store = createStore(reducer, storeEnhancer);

sagaMiddleware.run(saga);

export default store;
  1. saga.js文件的编写
  • takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLatest,会取消前面的)
  • put:在saga中派发action不再是通过dispatch,而是通过put;
  • all:可以在yield的时候put多个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
import { takeEvery, put, all, takeLatest } from 'redux-saga/effects';
import axios from 'axios';
import {
FETCH_HOME_MULTIDATA, ADD_NUMBER
} from './constants';
import {
changeBannersAction,
changeRecommendAction
} from './actionCreators';

function* fetchHomeMultidata(action) {
const res = yield axios.get("http://123.207.32.32:8000/home/multidata");
const banners = res.data.data.banner.list;
const recommends = res.data.data.recommend.list;
// yield put(changeBannersAction(banners));
// yield put(changeRecommendAction(recommends));
yield all([
yield put(changeBannersAction(banners)),
yield put(changeRecommendAction(recommends))
])
}

function* mySaga() {
// takeLatest takeEvery区别:
// takeLatest: 依次只能监听一个对应的action
// takeEvery: 每一个都会被执行
yield all([
takeLatest(FETCH_HOME_MULTIDATA, fetchHomeMultidata),
// takeLatest(ADD_NUMBER, fetchHomeMultidata),
]);
}

export default mySaga;

使用Monkey Patching实现中间件

可以利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;

用Monkey Patching实现日志打印

1
2
3
4
5
6
7
8
9
10
11
function patchLogging(store) {
const next = store.dispatch;
function dispatchAndLogging(action) {
console.log("dispatch前---dispatching action:", action);
next(action);
console.log("dispatch后---new state:", store.getState());
}
// store.dispatch = dispatchAndLogging;

return dispatchAndLogging;
}

用Monkey Patching实现redux-thunk

redux-thunk的作用:

  • redux中利用一个中间件redux-thunk可以让dispatch不再只是处理对象,并且可以处理函数;

我们对dispatch进行转换,这个dispatch会判断传入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function patchThunk(store) {
const next = store.dispatch;

function dispatchAndThunk(action) {
if (typeof action === "function") {
action(store.dispatch, store.getState)
} else {
next(action);
}
}

// store.dispatch = dispatchAndThunk;
return dispatchAndThunk;
}

合并中间件

单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:

1
2
3
4
5
6
7
8
function applyMiddlewares(...middlewares) {
// const newMiddleware = [...middlewares];
middlewares.forEach(middleware => {
store.dispatch = middleware(store);
})
}

applyMiddlewares(patchLogging, patchThunk);

代码的流程:

reducer拆分

可以按如下的目录结构 按模块拆分redux

combineReducers函数

目前合并的方式是通过每次调用reducer函数自己来返回一个新的对象。

redux提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:

  • 它也是将传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);
  • 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
  • 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;
1
2
3
4
5
6
const reducer  = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer
})

export default reducer;

路由

前端路由的原理

前端路由是如何做到URL和内容进行映射呢?监听URL的改变。

URL发生变化,同时不引起页面的刷新有两个办法:

  • 通过URL的hash改变URL;
  • 通过HTML5中的history模式修改URL;

当监听到URL发生变化时,可以通过自己判断当前的URL,决定到底渲染什么样的内容。

URL的hash

URL的hash也就是锚点(#), 本质上是改变window.location的href属性;

可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;

注意:

  • hash的优势就是兼容性更好,在老版 IE中都可以运行;
  • 但是缺陷是有一个#,显得不像一个真实的路径;

HTML5的history

history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:

  • replaceState:替换原来的路径;
  • pushState:使用新的路径;
  • popState:路径的回退;
  • go:向前或向后改变路径;
  • forward:向前改变路径;
  • back:向后改变路径;

react-router

目前前端流行的三大框架, 都有自己的路由实现:

  • Angular的ngRouter
  • React的react-router
  • Vue的vue-router

React Router的版本4开始,路由不再集中在一个包中进行管理了:

  • react-router是router的核心部分代码;
  • react-router-dom是用于浏览器的;
  • react-router-native是用于原生应用的;

安装react-router:

安装react-router-dom会自动帮助安装react-router的依赖;yarn add react-router-dom

react-router基本使用

react-router最主要的API是给我们提供的一些组件:

BrowserRouter或HashRouter

  • Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
  • BrowserRouter使用history模式;
  • HashRouter使用hash模式;

Link和NavLink:

  • 通常路径的跳转是使用Link组件,最终会被渲染成a元素;
  • NavLink是在Link基础之上增加了一些样式属性;
  • to属性:Link中最重要的属性,用于设置跳转到的路径;

Route:

  • Route用于路径的匹配;
  • path属性:用于设置匹配到的路径;
  • component属性:设置匹配到路径后,渲染的组件;
  • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
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, { PureComponent } from "react";

import { BrowserRouter, Link, Route, withRouter } from "react-router-dom";

import "./App.css";

import Home from './pages/home'
import About from './pages/about'
import Profile from './pages/profile'


class App extends PureComponent {
constructor(props) {
super(props);
}

render() {
return (
<div>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>

<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />

</div>
);
}
}

export default withRouter(App);

需求:路径选中时,对应的a元素变为红色

这个时候,要使用NavLink组件来替代Link组件:

  • activeStyle:活跃时(匹配时)的样式;
  • activeClassName:活跃时添加的class;
  • exact:是否精准匹配;

但是,在选中about或profile时,第一个也会变成红色:

  • 原因是/路径也匹配到了/about或/profile;
  • 这个时候,可以在第一个NavLink中添加上exact属性;

默认的activeClassName:

  • 在默认匹配成功时,NavLink就会添加上一个动态的active class;
  • 所以也可以直接编写样式
1
2
3
<NavLink exact to="/" activeClassName="link-active">首页</NavLink>
<NavLink to="/about" activeClassName="link-active">关于</NavLink>
<NavLink to="/profile" activeClassName="link-active">我的</NavLink>

Switch的作用

  • 当匹配到某一个路径时,会发现有一些问题;
  • 比如/about路径匹配到的同时,/:userid也被匹配到了,并且最后的一个NoMatch组件总是被匹配到;

原因是什么呢?默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染;

使用了Switch 只要匹配到了第一个,那么后面的就不应该继续匹配了 需要将所有的Route进行包裹

Redirect

Redirect用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:

1
2
3
4
5
6
return this.state.isLogin ? (
<div>
<h2>User</h2>
<h2>用户名: coderwhy</h2>
</div>
): <Redirect to="/login"/>

手动路由跳转

如何可以获取到history的对象呢?两种方式

  • 方式一:如果该组件是通过路由直接跳转过来的,那么可以直接获取history、location、match对象;
  • 方式二:如果该组件是一个普通渲染的组件,那么不可以直接获取history、location、match对象;

如果希望在App组件中获取到history对象,必须满足一下两个条件:

  • App组件必须包裹在Router组件之内;
  • App组件使用withRouter高阶组件包裹;

注意:使用withRouter必须被BrowserRouter或HashRouter包裹 所以根组件(App)要想使用 就得在index.js当中最外层就用BrowserRouter或HashRouter包裹

参数传递

传递参数有三种方式:

  • 动态路由的方式;
  • search传递参数;
  • Link中to传入对象;

动态路由的概念指的是路由中的路径并不会固定:

  • 比如/detail的path对应一个组件Detail;
  • 如果将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示; 这个匹配规则,就称之为动态路由;
  • 通常情况下,使用动态路由可以为路由传递参数。

search传递参数

1
<NavLink to={`/detail2?name=why&age=18`} activeClassName="link-active">详情2</NavLink>

Link中to可以直接传入一个对象(推荐)

1
2
3
4
5
6
7
8
<NavLink to={{
pathname: "/detail3",
search: "name=abc",
state: info
}}
activeClassName="link-active">
详情3
</NavLink>

react-router-config

将所有的路由配置放到一个地方进行集中管理:可以使用react-router-config来完成;

安装react-router-configyarn add react-router-config

  • 配置路由映射的关系数组
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 Home from '../pages/home';
import About, { AboutHisotry, AboutCulture, AboutContact, AboutJoin } from '../pages/about';
import Profile from '../pages/profile';
import User from '../pages/user';

const routes = [
{
path: "/",
exact: true,
component: Home
},
{
path: "/about",
component: About,
routes: [
{
path: "/about",
exact: true,
component: AboutHisotry
},
{
path: "/about/culture",
component: AboutCulture
},
{
path: "/about/contact",
component: AboutContact
},
{
path: "/about/join",
component: AboutJoin
},
]
}
]

export default routes;
  • 使用renderRoutes函数完成配置
1
2
3
import { renderRoutes } from 'react-router-config';

{renderRoutes(routes)}
  • 还提供了一个matchRoutes方法 可以拿到匹配到的route和match信息 第一个参数为匹配的数组 第二个参数为匹配的路径