# Redux

纯函数

理解 JavaScript 纯函数

  • 函数式编程中有一个非常重要的概念叫纯函数,JavaScript 符合函数式编程的范式,所以也有纯函数的概念;
    • 在 react 开发中纯函数是被多次提及的;
    • 比如 react 中组件就被要求像是一个纯函数(为什么是像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求必须是一个纯函数;
    • 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
  • 纯函数的维基百科定义:
    • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
    • 函数在确定的输入值时,需产生确定的输出
    • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关
    • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
  • 当然上面的定义会过于的晦涩,所以我简单总结一下:
    • 确定的输入,一定会产生确定的输出 , 且不依赖外部的变量;
    • 函数在执行过程中,不能产生副作用 , 即不能修改外部的变量或属性;

副作用概念的理解

  • 那么这里又有一个概念,叫做副作用,什么又是副作用呢?
    • 副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;
    • 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
  • 纯函数在执行的过程中就是不能产生这样的副作用:
    • 副作用往往是产生 bug 的 “温床”。

纯函数的案例

  • 我们来看一个对数组操作的两个函数:
    • slice:slice 截取数组时不会对原数组进行任何操作,而是生成一个新的数组;
    • splice:splice 截取数组, 会返回一个新的数组, 也会对原数组进行修改;
  • slice 就是一个纯函数,不会修改数组本身,而 splice 函数不是一个纯函数;

纯函数的作用和优势

  • 为什么纯函数在函数式编程中非常重要呢?
    • 因为你可以安心的编写和安心的使用;
    • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;
    • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
  • React 中就要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改:

1667817446487

  • 在接下来学习 redux 中,reducer 也被要求是一个纯函数。

Redux

为什么需要 redux

  • JavaScript 开发的应用程序,已经变得越来越复杂了:
    • JavaScript 需要管理的状态越来越多,越来越复杂;
      • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些 UI 的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
  • 管理不断变化的 state 是非常困难的:
    • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View 页面也有可能会引起状态的变化;
    • 当应用程序复杂时,state 在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
  • React 是在视图层帮助我们解决了 DOM 的渲染过程,但是 State 依然是留给我们自己来管理:
    • 无论是组件定义自己的 state,还是组件之间的通信通过 props 进行传递;也包括通过 Context 进行数据之间的共享;
    • React 主要负责帮助我们管理视图,state 如何维护最终还是我们自己来决定;

1667817506841

  • Redux 就是一个帮助我们管理 State 的容器:Redux 是 JavaScript 的状态容器,提供了可预测的状态管理;
  • Redux 除了和 React 一起使用之外,它也可以和其他界面库一起来使用(比如 Vue),并且它非常小(包括依赖在内,只有 2kb)

Redux 三大核心

Store

  • Redux 的核心理念非常简单。
  • 比如我们有一个朋友列表需要管理:
    • 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
    • 比如页面的某处通过 products.push 的方式增加了一条数据;
    • 比如另一个页面通过 products[0].age = 25 修改了一条数据;
  • 整个应用程序错综复杂,当出现 bug 时,很难跟踪到底哪里发生的变化;

Action

  • Redux 要求我们通过 action 来更新数据:
    • 所有数据的变化,必须通过派发(dispatch)action 来更新;
    • action 是一个普通的 JavaScript 对象,用来描述这次更新的 type 和 content;
  • 比如下面就是几个更新 friends 的 action:
    • 强制使用 action 的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;
    • 当然,目前我们的 action 是固定的对象;
    • 真实应用中,我们会通过函数来定义,返回一个 action;

Reducer

  • 但是如何将 state 和 action 联系在一起呢?答案就是 reducer
    • reducer 必须是一个纯函数;
    • reducer 做的事情就是将传入的 state 和 action 结合起来生成一个新的 state;

Redux 的三大原则

单一数据源

  • 整个应用程序的 state 被存储在一棵 object tree 中,并且这个 object tree 只存储在一个 store 中
  • Redux 并没有强制让我们不能创建多个 Store,但是那样做并不利于数据的维护;
  • 单一的数据源可以让整个应用程序的 state 变得方便维护、追踪、修改;

State 是只读的

  • 唯一修改 State 的方法一定是触发 action,不要试图在其他地方通过任何的方式来修改 State
  • 这样就确保了 View 或网络请求都不能直接修改 state,它们只能通过 action 来描述自己想要如何修改 state;
  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竟态)的问题

使用纯函数修改 state

  • 通过 reducer 将 旧 state 和 actions 联系在一起,并且返回一个新的 State
  • 随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同 state tree 的一部分;
  • 但是所有的 reducer 都应该是纯函数,不能产生任何的副作用;

Redux 的独立使用

项目初始化

  • 01 创建一个新的项目文件夹:learn-redux , 并初始化项目
1
2
3
4
5
# npm
npm init -y

# yand
yarn init
  • 安装 redux
1
2
3
4
5
# npm
npm install redux

# yard
yarn add redux

读取 store 中的数据

  • 创建 src 目录,并且创建 ./src/store/index.js 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 导入依赖 , node 环境用 require
const { createStore } = require('redux')

// 初始数据
const initialState = {
name: 'why',
counter: 100,
}

// 定义 reducer 函数,要求是一个纯函数
function reducer() {
return initialState
}

// 创建 store 实例,并将 reducer 传入
const store = createStore(reducer)

// 暴露 store 实例
module.exports = store
  • 创建 ./src/01_使用 store 的数据.js 文件
1
2
3
4
5
// 导入创建的 store 实例
const store = require('./store/index.js')

// 读取 store 中的数据
console.log(store.getState()) // { name: 'why', counter: 100 }
  • 终端进入到 src 目录 , 执行指令 : node 01_使用 store 的数据.js ,终端中即可输入获取到的数据

修改 store 中的数据

  • ./src/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
// 导入依赖 , node 环境用 require
const { createStore } = require('redux')

// 初始数据
const initialState = {
name: 'why',
counter: 100,
}

// 定义 reducer 函数,要求是一个纯函数
// 参数一: store 中目前保存的 state , 旧的 state
// 参数二: 本次需要更新的 action , 即派发来的 action 对象
// 返回值: 即决定 store 中保存的新的 state
// 有数据更新时返回新的 state,没有数据更新时返回旧的 state
function reducer(state = initialState, action) {
if (action.type === 'change_name') {
return { ...state, name: action.name }
} else if (action.type === 'add_number') {
return { ...state, counter: state.counter + action.num }
}
return state
}

// 创建 store 实例,并将 reducer 传入
const store = createStore(reducer)

// 暴露 store 实例
module.exports = store
  • 创建 ./src/02_修改 store 中的数据.js 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导入创建的 store 实例
const store = require('./store/index.js')

// 读取 store 中的数据
console.log(store.getState()) // { name: 'why', counter: 100 }

// 修改 store 中的数据
const nameAction = { type: 'change_name', name: 'kobe' }
store.dispatch(nameAction)
// 修改 store 中的数据
const counterAction = { type: 'add_number', num: 10 }
store.dispatch(counterAction)

// 读取 store 中的数据
console.log(store.getState()) // { name: 'kobe', counter: 110 }

订阅 store 中的 state

  • 即只要 store 中的 state 数据发生变化,就自动执行回调

./src/03_订阅 store 中的数据.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 导入创建的 store 实例
const store = require('./store/index.js')

// 读取 store 中最初始的 state
console.log(' store 中的初始 state :', store.getState())

// 订阅 store 中的数据,返回值为一个函数
const unsubscribe = store.subscribe(() => {
console.log('订阅的store数据发生了变化:', store.getState())
})

// 修改 store 中的数据
store.dispatch({ type: 'change_name', name: 'kobe' })
store.dispatch({ type: 'add_number', num: 10 })

// 取消订阅
unsubscribe() // 下面的 dispatch 未打印数据的变化
store.dispatch({ type: 'add_number', num: 200 })
// 手动打印更新后的 state
console.log(' store 中的更新后的 state :', store.getState())

动态生成 action 的优化

新建 ./src/store/constants.js , 用于定义常量

1
2
3
4
5
6
// 定义常量
const CHANGE_NAME = 'change_name'
const ADD_NUMBER = 'add_number'

// 导出变量
module.exports = { CHANGE_NAME, ADD_NUMBER }

新建 ./src/store/actionCreators.js , 用于存放 action

1
2
3
4
5
6
7
8
9
// 导入常量
const { CHANGE_NAME, ADD_NUMBER } = require('./constants.js')

// 定义函数,动态生成 action
const changeNameAction = (name) => ({ type: CHANGE_NAME, name })
const addNumberAction = (num) => ({ type: ADD_NUMBER, num })

// 导出方法
module.exports = { changeNameAction, addNumberAction }

新建 ./src/04_动态生成 action.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导入创建的 store 实例
const store = require('./store/index.js')
const { changeNameAction, addNumberAction } = require('./store/actionCreators.js')

// 订阅 store 中的数据,返回值为一个函数
const unsubscribe = store.subscribe(() => {
console.log('订阅的store数据发生了变化:', store.getState())
})

// 修改 store 中的数据
store.dispatch(changeNameAction('kobe'))
store.dispatch(addNumberAction(10))

// 取消订阅
unsubscribe() // 下面的 dispatch 未打印数据的变化

对 reducer 的优化

  • 新建 ./src/store/reducer.js , 用于定义 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
// 导入常量
const { CHANGE_NAME, ADD_NUMBER } = require('./constants.js')

// 初始数据
const initialState = {
name: 'why',
counter: 100,
}

// 定义 reducer 函数,要求是一个纯函数
// 参数一: store 中目前保存的 state , 旧的 state
// 参数二: 本次需要更新的 action , 即派发来的 action 对象
// 返回值: 即决定 store 中保存的新的 state
// 有数据更新时返回新的 state,没有数据更新时返回旧的 state
function reducer(state = initialState, action) {
switch (action.type) {
case CHANGE_NAME:
return { ...state, name: action.name }
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num }
default:
return state
}
}

module.exports = reducer
  • ./src/store/index.js 文件中,创建 store 实例
1
2
3
4
5
6
7
8
9
// 导入依赖 , node 环境用 require
const { createStore } = require('redux')
const reducer = require('./reducer.js')

// 创建 store 实例,并将 reducer 传入
const store = createStore(reducer)

// 暴露 store 实例
module.exports = store

Redux 独立使用小结

redux 代码优化:

  • 1 将派发的 action 生成过程放到一个 actionCreators 函数中
  • 2 将定义的所有 actionCreators 的函数, 放到一个独立的文件中: actionCreators.js
  • 3 actionCreators 和 reducer 函数中使用字符串常量是一致的, 所以将常量抽取到一个独立 constants 的文件中
  • 4 将 reducer 和默认值 (initialState) 放到一个独立的 reducer.js 文件中, 而不是在 index.js

Redux 的使用过程

  • 1 创建一个对象,作为我们要保存的状态:
  • 2 创建 Store 来存储这个 state
    • 创建 store 时必须创建 reducer;
    • 我们可以通过 store.getState 来获取当前的 state;
  • 3 通过 action 来修改 state
    • 通过 dispatch 来派发 action;
    • 通常 action 中都会有 type 属性,也可以携带其他的数据;
  • 4 修改 reducer 中的处理代码
    • 这里一定要记住,reducer 是一个纯函数,不需要直接修改 state;
    • 后面我会讲到直接修改 state 带来的问题;
  • 5 可以在派发 action 之前,监听 store 的变化:

Redux 结构划分

1
2
3
4
5
6
7
--src
|-- store
|----- index.js # 用于创建 store 实例
|----- reducer.js # 用于创建 reducer
|----- constants.js # 用于定义常量
|----- actionCreators.js # 用于创建 action

  • 如果我们将所有的逻辑代码写到一起,那么当 redux 变得复杂时代码就难以维护。
    • 接下来,我会对代码进行拆分,将 store、reducer、action、constants 拆分成一个个文件。
    • 创建 store/index.js 文件:
    • 创建 store/reducer.js 文件:
    • 创建 store/actionCreators.js 文件:
    • 创建 store/constants.js 文件:
  • 注意:node 中对 ES6 模块化的支持
    • 目前我使用的 node 版本是 v12.16.1,从 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 的基本使用过程,那么我们就更加清晰来认识一下 redux 在实际开发中的流程:

1667817986781

  • redux 官方图:

image-20230312112543387

React 中使用 Redux

案例需求分析

  • 目前 redux 在 react 中使用是最多的,所以我们需要将之前编写的 redux 代码,融入到 react 当中去。
  • 这里我创建了两个组件:
    • Home 组件:其中会展示当前的 counter 值,并且有一个 +1 和 +10 的按钮;
    • Profile 组件:其中会展示当前的 counter 值,并且有一个 -1 和 -10 的按钮;

1671698912913

搭建 store

新建 ./src/store/constant.js , 用于定义常量

1
2
export const ADD_NUMBER = 'add_number'
export const SUB_NUMBER = 'sub_number'

新建 ./src/store/actionCreators.js , 用于定义 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as actionTypes from './constant.js'

export const addNumberAction = (num) => {
return {
type: actionTypes.ADD_NUMBER,
num,
}
}

export const subNumberAction = (num) => {
return {
type: actionTypes.SUB_NUMBER,
num,
}
}

新建 ./src/store/reducer.js , 用于定义 reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import * as actionTypes from './constant'

const initialState = {
counter: 100,
}

function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.ADD_NUMBER:
return { ...state, counter: state.counter + action.num }
case actionTypes.SUB_NUMBER:
return { ...state, counter: state.counter - action.num }
default:
return state
}
}

export default reducer

新建 ./src/store/index.js , 用于创建 store 实例

1
2
3
4
5
6
import { createStore } from 'redux'
import reducer from './reducer.js'

const store = createStore(reducer)

export default store

读取和修改 store

新建 ./src/pages/home.jsx 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { PureComponent } from 'react'
import store from '../store/index.js'
import { addNumberAction } from '../store/actionCreators.js'

export class home extends PureComponent {
constructor(props) {
super(props)
this.state = {
counter: store.getState().counter,
}
}
// 组件挂载完毕,并订阅消息
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({ counter: state.counter })
})
}
// 按钮事件回调
addNumber = (num) => {
store.dispatch(addNumberAction(num))
}
render() {
const { counter } = this.state
return (
<div>
<h2>home Counter:{counter}</h2>
<div>
<button onClick={(e) => this.addNumber(1)}>+1</button>
<button onClick={(e) => this.addNumber(10)}>+10</button>
<button onClick={(e) => this.addNumber(20)}>+20</button>
</div>
</div>
)
}
}

export default home

新建 ./src/pages/profile.jsx 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { PureComponent } from 'react'
import store from '../store/index.js'
import { subNumberAction } from '../store/actionCreators.js'

export class profile extends PureComponent {
constructor(props) {
super(props)
this.state = {
counter: store.getState().counter,
}
}
// 组件挂载完毕,并订阅消息
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({ counter: state.counter })
})
}
// 按钮事件回调
subNumber = (num) => {
store.dispatch(subNumberAction(num))
}
render() {
const { counter } = this.state
return (
<div>
<h2>profile Counter:{counter}</h2>
<div>
<button onClick={() => this.subNumber(1)}>-1</button>
<button onClick={() => this.subNumber(10)}>-10</button>
<button onClick={() => this.subNumber(20)}>-20</button>
</div>
</div>
)
}
}

export default profile

App.jsx

  • App.jsx 组件中使用以上创建的两个组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { PureComponent } from 'react'
import store from './store/index.js'
import Home from './pages/home'
import Profile from './pages/profile'

import './style.css'

export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
counter: store.getState().counter,
}
}
// 组件挂载完毕,并订阅消息
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({ counter: state.counter })
})
}
render() {
const { counter } = this.state
return (
<div>
<h2>App Counter: {counter}</h2>
<div className="pages">
<Home></Home>
<Profile></Profile>
</div>
</div>
)
}
}

export default App
  • 对应的全局 CSS 样式

style.css

1
2
3
4
5
6
7
8
.pages {
display: flex;
}
.pages > div {
flex: 1;
border: 1px solid red;
padding: 10px;
}

案例小结

  • 核心代码主要是两个:
    • 在 componentDidMount 中定义数据的变化,当数据发生变化时重新设置 counter;
    • 在发生点击事件时,调用 store 的 dispatch 来派发对应的 action;

react-redux 的使用

  • 开始之前需要强调一下,redux 和 react 没有直接的关系,你完全可以在 React, Angular, Ember, jQuery, or vanilla JavaScript 中使用 Redux。
  • 尽管这样说,redux 依然是和 React 库结合的更好,因为他们是通过 state 函数来描述界面的状态,Redux 可以发射状态的更新,让他们作出相应。
  • 虽然我们之前已经实现了 connect、Provider 这些帮助我们完成连接 redux、react 的辅助工具,但是实际上 redux 官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。
  • 安装 react-redux:yarn add react-reduxnpm install react-redux

读取 store 的数据

入口文件 index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

import { Provider } from 'react-redux'
import store from './store/index.js'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<Provider store={store}>
{' '}
{/* 1. 指定提供的 store */}
<App />
</Provider>
</React.StrictMode>
)

任意组件

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, { PureComponent } from 'react'
import { connect } from 'react-redux'

export class About extends PureComponent {
render() {
const { counter } = this.props
return (
<div>
<h2>about</h2>
<h2>{counter}</h2> {/* 4. 使用 store 中的数据 */}
</div>
)
}
}

// 3. connect 函数的参数一,是一个函数,会把返回的对象传入 about 组件
const mapStateToProps = (state) => {
return {
counter: state.counter,
}
}

// 2. connect 函数的返回值是一个高阶组件
export default connect(mapStateToProps)(About)

修改 store 的数据

  • 同上,要在入口文件 index.js 中指定提供的 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { addNumberAction, subNumberAction } from '../store/actionCreators'

export class About extends PureComponent {
// 事件回调
calcNumber = (num, isAdd) => {
if (isAdd) {
this.props.addNumber(num) // 3. 执行指定的函数派发 action
} else {
this.props.subNumber(num)
}
}

render() {
const { counter } = this.props
return (
<div>
<h2>about:{counter}</h2>
<button
onClick={() => {
this.calcNumber(100, true)
}}
>
+100
</button>
<button
onClick={() => {
this.calcNumber(200, false)
}}
>
-200
</button>
<button
onClick={() => {
this.calcNumber(300, true)
}}
>
+300
</button>
</div>
)
}
}

// connect 函数的参数一,是一个函数,会把返回的对象传入 about 组件
const mapStateToProps = (state) => {
return {
counter: state.counter,
}
}

// 1. connect 函数的参数二,是一个函数
const mapDispatchToProps = (dispatch) => {
return {
addNumber(num) {
dispatch(addNumberAction(num))
},
subNumber(num) {
dispatch(subNumberAction(num))
},
}
}

// 2. connect 函数的返回值是一个高阶组件
export default connect(mapStateToProps, mapDispatchToProps)(About)

组件中异步操作

在组件中发起异步请求

  • 在之前简单的案例中,redux 中保存的 counter 是一个本地定义的数据
    • 我们可以直接通过同步的操作来 dispatch action,state 就会被立即更新。
    • 但是真实开发中,redux 中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到 redux 中。
  • 在之前学习网络请求的时候我们讲过,网络请求可以在 class 组件的 componentDidMount 中发送,所以我们可以有这样的结构:

1671165496024

  • 我现在完成如下案例操作:
    • 在任意某一个组件中请求 banners 的数据,并保存到 store 中;
    • 在 About 组件中展示 banners 和 recommends 的数据;

./src/store/constant.js 中定义常量

1
export const CHANGE_BANNERS = 'change_banners'

./src/store/actionCreators.js 中,定义 action

1
2
3
4
5
6
7
8
import * as actionTypes from './constant.js'

export const changeBannersAction = (banners) => {
return {
type: actionTypes.CHANGE_BANNERS,
banners,
}
}

./src/store/reducer.js 中 , 修改 store 中的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as actionTypes from './constant'

const initialState = {
banners: [],
}

function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.CHANGE_BANNERS:
return { ...state, banners: action.banners }
default:
return state
}
}

export default 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
import React, { PureComponent } from 'react'
import axios from 'axios'
import { connect } from 'react-redux'
import { changeBannersAction } from '../store/actionCreators'

export class category extends PureComponent {
componentDidMount() {
axios.get('http://123.207.32.32:8000/home/multidata').then((res) => {
const banners = res.data.data.banner.list
this.props.changeBanners(banners)
})
}
render() {
return (
<div>
<h2>category</h2>
</div>
)
}
}

const mapDispatchToProps = (dispatch) => {
return {
changeBanners(banners) {
dispatch(changeBannersAction(banners))
},
}
}

export default connect(null, mapDispatchToProps)(category)

另一个任意组件 , 用于展示获取到的网络数据

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, { PureComponent } from 'react'
import { connect } from 'react-redux'

export class About extends PureComponent {
render() {
const { banners } = this.props
return (
<div>
<div className="banner">
<ul>
{banners.map((item, index) => {
return <li key={index}>{item.title}</li>
})}
</ul>
</div>
</div>
)
}
}

// connect 函数的参数一,是一个函数,会把返回的对象传入 about 组件
const mapStateToProps = (state) => {
return {
banners: state.banners,
}
}

// connect 函数的返回值是一个高阶组件
export default connect(mapStateToProps, null)(About)
  • 上面的代码有一个缺陷:
    • 我们必须将网络请求的异步代码放到组件的生命周期中来完成;
    • 事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给 redux 来管理;

1671165572925

  • 但是在 redux 中如何可以进行异步的操作呢?
    • 答案就是使用中间件(Middleware);
    • 学习过 Express 或 Koa 框架的童鞋对中间件的概念一定不陌生;
    • 在这类框架中,Middleware 可以帮助我们在请求和响应之间嵌入一些操作的代码,比如 cookie 解析、日志记录、文件压缩等操作;

理解中间件

  • redux 也引入了中间件(Middleware)的概念:

    • 这个中间件的目的是在 dispatch 的 action 和最终达到的 reducer 之间,扩展一些自己的代码;
    • 比如日志记录、调用异步接口、添加代码调试功能等等;
  • 我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:

    • 这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk;
  • redux-thunk 是如何做到让我们可以发送异步的请求呢?

    • 我们知道,默认情况下的 dispatch(action),action 需要是一个 JavaScript 的对象;
    • redux-thunk 可以让 dispatch(action 函数),action 可以是一个函数;
    • 该函数会被调用,并且会传给这个函数一个 dispatch 函数和 getState 函数;
      • ✓ dispatch 函数用于我们之后再次派发 action;
      • ✓ getState 函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

使用 redux-thunk

  • 安装 redux-thunk : yarn add redux-thunknpm install redux-thunk

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

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

    • 注意:这里不是返回一个对象了,而是一个函数;
    • 该函数在 dispatch 之后会被执行;

./src/store/index.js

1
2
3
4
5
6
7
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk' // 导入中间件
import reducer from './reducer.js'

const store = createStore(reducer, applyMiddleware(thunk)) // 使用中间件

export default store

./src/pages/category.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
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { fetchHomeMultidataAction } from '../store/actionCreators' // 导入 action 方法

export class category extends PureComponent {
componentDidMount() {
this.props.fetchHomeMultidata() // 调用方法发起网络请求
}
render() {
return (
<div>
<h2>category</h2>
</div>
)
}
}

const mapDispatchToProps = (dispatch) => {
return {
fetchHomeMultidata() {
dispatch(fetchHomeMultidataAction()) // 派发 action 发起网络请求
},
}
}

export default connect(null, mapDispatchToProps)(category)

./src/store/actionCreators.js

1
2
3
4
5
6
7
8
9
export const fetchHomeMultidataAction = () => {
return (dispatch, getState) => {
// 异步操作,发起网络请求
axios.get('http://123.207.32.32:8000/home/multidata').then((res) => {
const banners = res.data.data.banner.list
dispatch(changeBannersAction(banners))
})
}
}

redux-devtools

  • 我们之前讲过,redux 可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?
    • redux 官网为我们提供了 redux-devtools 的工具;
    • 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
  • 安装该工具需要两步:
    • 第一步:在对应的浏览器中安装相关的插件(比如 Chrome 浏览器扩展商店中搜索 Redux DevTools 即可), 极简插件 有提供;
    • 第二步:在 redux 中继承 devtools 的中间件;
1
2
3
4
5
6
7
8
9
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk' // 中间件
import reducer from './reducer.js'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose // __REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 是从开发工具中读取的
// const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose // 同时开启 trace , 默认是关闭的
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))) // 使用中间件

export default store

Reducer 结构拆分

Reducer 代码拆分

  • 我们先来理解一下,为什么这个函数叫 reducer?
  • 我们来看一下目前我们的 reducer:
    • 当前这个 reducer 既有处理 counter 的代码,又有处理 home 页面的数据;
    • 后续 counter 相关的状态或 home 相关的状态会进一步变得更加复杂;
    • 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
    • 如果将所有的状态都放到一个 reducer 中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
  • 因此,我们可以对 reducer 进行拆分:
    • 我们先抽取一个对 counter 处理的 reducer;
    • 再抽取一个对 home 处理的 reducer;
    • 将它们合并起来;

目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
|-- src
|-- store
|---- counter
|------ actionCreators.js
|------ constant.js
|------ reducer.js
|------ index.js
|---- home
|------ actionCreators.js
|------ constant.js
|------ reducer.js
|------ index.js

./src/store/counter/index.js , 另一个模块也是一样, 仅用于统一暴露

1
2
3
4
import reducer from './reducer'

export default reducer
export * from './actionCreators'

最后在 ./src/store/index 中, 通过 combineReducers 将多个模块合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
import thunk from 'redux-thunk' // 中间件

import counterReducer from './counter'
import homeReducer from './home'

// 将两个 reducer 合并
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
})

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))

export default store
  • 在项目中其他地方读取变量,以及导入依赖等的修改,仅需要注意模块名,在此不再列出相关的代码

combineReducers 函数

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

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

    ./src/store/index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
import thunk from 'redux-thunk' // 中间件

import counterReducer from './counter'
import homeReducer from './home'

// 将两个 reducer 合并
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
})

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))

export default store
  • 那么 combineReducers 是如何实现的呢?
    • 事实上,它也是将我们传入的 reducers 合并到一个对象中,最终返回一个 combination 的函数(相当于我们之前的 reducer 函数了);
    • 在执行 combination 函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的 state 还是新的 state;
    • 新的 state 会触发订阅者发生对应的刷新,而旧的 state 可以有效的组织订阅者发生刷新;
  • 也可以查看源码来学习。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过 combineReducers 将多个 reducer 模块合并
// const reducer = combineReducers({
// counter: counterReducer,
// home: homeReducer,
// })

// combineReducers 的实现原理
function reducer(state = {}, action) {
return {
counter: counterReducer(state.counter, action),
home: homeReducer(state.home, action),
}
}

Redux Toolkit

认识 Redux Toolkit

  • Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
    • 在前面我们学习 Redux 的时候应该已经发现,redux 的编写逻辑过于的繁琐和麻烦。
    • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
    • Redux Toolkit 包旨在成为编写 Redux 逻辑的标准方式,从而解决上面提到的问题;
    • 在很多地方为了称呼方便,也将之称为 “RTK” ;
  • 安装 Redux Toolkit:npm install @reduxjs/toolkit react-redux
  • Redux Toolkit 的核心 API 主要是如下几个:
    • configureStore:包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk 默认包含,并启用 Redux DevTools Extension。
    • createSlice:接受 reducer 函数的对象、切片名称和初始状态值,并自动生成切片 reducer,并带有相应的 actions。
    • createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个 pending/fulfilled/rejected 基于该承诺分派动作类型的 thunk

创建 reducer

  • 我们先对 counter 的 reducer 进行重构: 通过 createSlice 创建一个 slice。
  • createSlice 主要包含如下几个参数:
    • name:用户标记 slice 的名词
      • 在之后的 redux-devtool 中会显示对应的名词;
    • initialState:初始化值
      • 第一次初始化时的值;
    • reducers:相当于之前的 reducer 函数
      • 对象类型,并且可以添加很多的函数;
      • 函数类似于 redux 原来 reducer 中的一个 case 语句;
      • 函数的参数:
        • 参数一:state
        • 参数二:调用这个 action 时,传递的 action 参数;
  • createSlice 返回值是一个对象,包含所有的 actions;

新建 ./src/store/features/counter.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 } from '@reduxjs/toolkit'

const counterSlice = createSlice({
name: 'counter',
initialState: {
counter: 888,
},
reducers: {
addNumber(state, action) {
const payload = action.payload
state.counter = state.counter + payload
},
subNumber(state, action) {
const payload = action.payload
state.counter = state.counter - payload
},
},
})

export const { addNumber, subNumber } = counterSlice.actions

export default counterSlice.reducer

store 的创建

  • configureStore 用于创建 store 对象,常见参数如下:
    • reducer,将 slice 中的 reducer 可以组成一个对象传入此处;
    • middleware:可以使用参数,传入其他的中间件(自行了解);
    • devTools:是否配置 devTools 工具,默认为 true;

新建 ./src/store/index.js 文件:

1
2
3
4
5
6
7
8
9
10
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter'

const store = configureStore({
reducer: {
counter: counterReducer,
},
})

export default store

在其他任意组件中

  • 读取 store 中的数据,并派发 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
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { addNumber } from '../store/features/counter'

export class Home extends PureComponent {
addNumber = (num) => {
this.props.addNumber(num)
}
render() {
const { counter } = this.props
return (
<div>
<h2>Home counter : {counter}</h2>
<button onClick={() => this.addNumber(5)}>+5</button>
<button onClick={() => this.addNumber(8)}>+8</button>
<button onClick={() => this.addNumber(18)}>+18</button>
</div>
)
}
}

const mapStateToProps = (state) => {
return {
counter: state.counter.counter,
}
}

const mapDispatchToProps = (dispatch) => {
return {
addNumber(num) {
dispatch(addNumber(num))
},
}
}

export default connect(mapStateToProps, mapDispatchToProps)(Home)

ReduxToolkit 异步操作

  • 在之前的开发中,我们通过 redux-thunk 中间件让 dispatch 中可以进行异步操作。

  • Redux Toolkit 默认已经给我们集成了 Thunk 相关的功能:createAsyncThunk

  • 当 createAsyncThunk 创建出来的 action 被 dispatch 时,会存在三种状态:

    • pending:action 被发出,但是还没有最终的结果;
    • fulfilled:获取到最终的结果(有返回值的结果);
    • rejected:执行过程中有错误或者抛出了异常;
  • 我们可以在 createSlice 的 entraReducer 中监听这些结果:

新建 ./src/store/features/home.js ,,发起网络请求,并将数据保存到 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
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'

export const fetchHomeMultidataAction = createAsyncThunk('fetch/homemultidata', async () => {
const res = await axios.get('http://123.207.32.32:8000/home/multidata')
return res.data
})

const homeSlice = createSlice({
name: 'home',
initialState: {
banners: [],
},
reducers: {
changeBanner(state, action) {
const payload = action.payload
state.banners = payload
},
},
extraReducers: {
[fetchHomeMultidataAction.pending](state, action) {
console.log('fetchHomeMultidataAction/pending')
},
[fetchHomeMultidataAction.fulfilled](state, { payload }) {
state.banners = payload.data.banner.list
},
[fetchHomeMultidataAction.rejected](state, action) {
console.log('fetchHomeMultidataAction/rejected')
},
},
})

export const { changeBanner } = homeSlice.actions

export default homeSlice.reducer

或者:

  • 使用这种写法,上面就不需要 extraReducers 配置项了,但是 extraReducers 配置项的写法是官方推荐的写法,但是略为复杂

  • extraInfo 形参,是派发该 action 时传递来的数据,可以不传

  • 参数二为 store , 可以从中解构出 dispatch, getState

1
2
3
4
5
6
export const fetchHomeMultidataAction = createAsyncThunk('fetch/homemultidata', async (extraInfo, { dispatch, getState }) => {
const res = await axios.get('http://123.207.32.32:8000/home/multidata')
const banners = res.data.data.banner.list
dispatch(changeBanner(banners))
return res.data
})

在**./src/pages/Home.jsx **组件中,派发 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
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { fetchHomeMultidataAction } from '../store/features/home'

export class Home extends PureComponent {
componentDidMount() {
this.props.fetchHomeMultidata()
}
addNumber = (num) => {
this.props.addNumber(num)
}
render() {
return (
<div>
<h2>Home</h2>
</div>
)
}
}

const mapDispatchToProps = (dispatch) => {
return {
fetchHomeMultidata() {
dispatch(fetchHomeMultidataAction())
},
}
}

export default connect(null, mapDispatchToProps)(Home)

在另一任意组件中 , 读取 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
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { subNumber } from '../store/features/counter'

export class Profile extends PureComponent {
render() {
const { banners } = this.props
return (
<div>
<div className="banner">
<h2>轮播图展示</h2>
<ul>
{banners.map((item, index) => {
return <li key={index}>{item.title}</li>
})}
</ul>
</div>
</div>
)
}
}

const mapStateToProps = (state) => {
return {
banners: state.home.banners,
}
}

export default connect(mapStateToProps, null)(Profile)

extraReducer 另外写法

  • extraReducer 还可以传入一个函数,函数接受一个 builder 参数。
    • 我们可以向 builder 中添加 case 来监听异步操作的结果:

image-20230312112646208

底层原理相关

RTK 的数据不可变

  • 在 React 开发中,我们总是会强调数据的不可变性:
    • 无论是类组件中的 state,还是 redux 中管理的 state;
    • 事实上在整个 JavaScript 编码过程中,数据的不可变性都是非常重要的;
  • 所以在前面我们经常会进行浅拷贝来完成某些操作,但是浅拷贝事实上也是存在问题的:
    • 比如过大的对象,进行浅拷贝也会造成性能的浪费;
    • 比如浅拷贝后的对象,在深层改变时,依然会对之前的对象产生影响;
  • 事实上 Redux Toolkit 底层使用了 immerjs 的一个库来保证数据的不可变性。
  • 在我们公众号的一片文章中也有专门讲解 immutable-js 库的底层原理和使用方法:
  • 为了节约内存,又出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性数据结构);
    • 用一种数据结构来保存数据;
    • 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费;

自定义 connect 函数

1
// 略...难度很大....

context 处理 store

  • 但是上面的 connect 函数有一个很大的缺陷:依赖导入的 store
    • 如果我们将其封装成一个独立的库,需要依赖用于创建的 store,我们应该如何去获取呢?
    • 难道让用户来修改我们的源码吗?不太现实;
  • 正确的做法是我们提供一个 Provider,Provider 来自于我们创建的 Context,让用户将 store 传入到 value 中即可;
1
// 略...难度很大....

打印日志需求

  • 前面我们已经提过,中间件的目的是在 redux 中插入一些自己的操作:
    • 比如我们现在有一个需求,在 dispatch 之前,打印一下本次的 action 对象,dispatch 完成之后可以打印一下最新的 store state;
    • 也就是我们需要将对应的代码插入到 redux 的某部分,让之后所有的 dispatch 都可以包含这样的操作;
  • 如果没有中间件,我们是否可以实现类似的代码呢? 可以在派发的前后进行相关的打印。
  • 但是这种方式缺陷非常明显:
    • 首先,每一次的 dispatch 操作,我们都需要在前面加上这样的逻辑代码;
    • 其次,存在大量重复的代码,会非常麻烦和臃肿;
  • 是否有一种更优雅的方式来处理这样的相同逻辑呢?
    • 我们可以将代码封装到一个独立的函数中
  • 但是这样的代码有一个非常大的缺陷:
    • 调用者(使用者)在使用我的 dispatch 时,必须使用我另外封装的一个函数 dispatchAndLog;
    • 显然,对于调用者来说,很难记住这样的 API,更加习惯的方式是直接调用 dispatch;

修改 dispatch

  • 事实上,我们可以利用一个 hack 一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;
  • 我们对代码进行如下的修改:
    • 这样就意味着我们已经直接修改了 dispatch 的调用过程;
    • 在调用 dispatch 的过程中,真正调用的函数其实是 dispatchAndLog;
  • 当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对 store 进行这样的处理:

thunk 需求

  • redux-thunk 的作用:
    • 我们知道 redux 中利用一个中间件 redux-thunk 可以让我们的 dispatch 不再只是处理对象,并且可以处理函数;
    • 那么 redux-thunk 中的基本实现过程是怎么样的呢?事实上非常的简单。
  • 我们来看下面的代码:
    • 我们又对 dispatch 进行转换,这个 dispatch 会判断传入的

合并中间件

  • 单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
  • 我们来理解一下上面操作之后,代码的流程:

1671167741310

  • 当然,真实的中间件实现起来会更加的灵活,这里我们仅仅做一个抛砖引玉,有兴趣可以参考 redux 合并中间件的源码流程。

React 中的 state 管理

  • 我们学习了 Redux 用来管理我们的应用状态,并且非常好用(当然,你学会前提下,没有学会,好好回顾一下)。

  • 目前我们已经主要学习了三种状态管理方式:

    • 方式一:组件中自己的 state 管理;
    • 方式二:Context 数据的共享状态;
    • 方式三:Redux 管理应用状态;
  • 在开发中如何选择呢?

    • 首先,这个没有一个标准的答案;
    • 某些用户,选择将所有的状态放到 redux 中进行管理,因为这样方便追踪和共享;
    • 有些用户,选择将某些组件自己的状态放到组件内部进行管理;
    • 有些用户,将类似于主题、用户信息等数据放到 Context 中进行共享和管理;
    • 做一个开发者,到底选择怎样的状态管理方式,是你的工作之一,可以一个最好的平衡方式( Find a balance that works for you, and go with it. );
  • Redux 的作者有给出自己的建议:

1671167817676

  • 目前项目中我采用的 state 管理方案:
    • UI 相关的组件内部可以维护的状态,在组件内部自己来维护;
    • 大部分需要共享的状态,都交给 redux 来管理和维护;
    • 从服务器请求的数据(包括请求的操作),交给 redux 来维护;
  • 当然,根据不同的情况会进行适当的调整,在后续学习项目实战时,我也会再次讲解以实战的角度来设计数据的管理方案。