Redux状态管理

Redux状态管理

1. 为什么需要Redux

  • JavaScript开发的应用程序,已经变得越来越复杂了:

    • Javascript需要管理的状态(state)越来越多,越来越复杂;
    • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
  • 管理不断变化的state是非常困难的:

    • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化
    • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
  • React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:

    • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享
    • React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;
  • Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理

  • Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)

2. Redux核心理念-Store

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

3. Redux核心理念-Action

  • Redux要求我们通过action来更新数据:
    • 所有数据的变化,必须通过派发 (dispatch) action来更新
    • action是一个普通的JavaScript对象,用来描述这次更新的type和content;
  • 比如下面就是几个更新friends的action:
    • 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的
    • 当然,目前我们的action是固定的对象
    • 真实应用中,我们会通过函数来定义,返回一个action;
1
2
3
const action1 = { type: "ADD_FRIEND", info: { name: "lucy", age: 20 } }
const action2 = [ type: "INC_AGE", index: 0 }
const action3 = { type: "CHANGE NAME", playload: { index: 0, newName: "coderwhy" } }

4. Redux核心理念-Reducer

  • 如何将state和action联系在一起?使用reducer
    • reducer是一个纯函数
    • reducer做的事情就是将传入的state和action结合起来生成一个新的state

5. Redux的基本使用

创建redux

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 {createStore} = require("redux");

// 初始化的数据
const initialState = {
name: 'zwx',
age: 18
}

// 定义reducer函数:纯函数
// 参数一:store中目前保存的state
// 参数二:本次需要更新的action(dispatch传入的action)
// 返回值:作为store之后存储的state
function reducer(state = initialState, action) {
// 有新数据进行更新的时候,那么就返回一个新的state
if (action.type === "change_name") {
return {...state, name: action.name};
}

// 没有新数据就返回之前的state
return state;
}

// 创建store
const store = createStore(reducer)

module.exports = store;

修改store中的数据

1
2
3
4
5
6
7
8
const store = require("./store");

console.log(store.getState);

// 修改store中的数据:必须通过action
const nameAction = {type: "change_name", name: "ysx"};
store.dispatch(nameAction);
console.log(store.getState());

6. store数据订阅

使用store.subscribe()进行数据订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const store = require("./store");

// 订阅数据变化
const unsubscribe = store.subscribe(() => {
console.log("订阅数据的变化:", store.getState());
})

// 修改store中的数据:必须通过action
store.dispatch({type: "change_name", name: "ysx"});
store.dispatch({type: "change_name", name: "yxx"});

// 取消订阅
unsubscribe();

// 修改counter
store.dispatch({type: "add_number", num: 10});

7. Redux的三大原则

7.1 单一数据源

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

7.2 State是只读的

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

7.3 使用纯函数来执行修改

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

8. 在React中使用Redux

8.1 创建store

1
2
3
4
5
6
import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);

export default store;

8.2 创建常量类型

1
2
export const ADD_NUMBER = "add_number";
export const SUB_NUMBER = "sub_number";

8.3 创建action

1
2
3
4
5
6
7
8
9
10
11
import * as actionTypes from "./constants"

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

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

8.4 创建reducer

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

const initialState = {
counter: 0
}

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;

8.5 在组件中使用redux

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, { PureComponent } from 'react'
import store from '../store'
import {addNumberAction} from '../store/actionCreators'

export class Home extends PureComponent {
constructor() {
super();
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(5)}>+5</button>
<button onClick={e => this.addNumber(8)}>+8</button>
</div>
</div>
)
}
}

export default Home

9. react-redux库的使用

  • 可以看到在上面的示例中,每个组件都有很多重复的代码
    • 比如构造函数中需要写store要用到的数据,并且需要在componentDidMount订阅redux
  • 我们可以将重复的代码封装到一个高阶组件中
  • 这时候就需要用到react-redux库

9.1 安装react-redux

1
npm install react-redux

9.2 给整个应用程序提供store

1
2
3
4
5
import store from './store';

<Provider store={store}>
<App />
</Provider>

9.3 在组件中使用redux

mapStateToProps函数的返回值是一个对象,对象中包含的属性就是本组件需要用到的数据
当state中的这些值发生改变时,render就会重新渲染当前组件
connect是一个高阶组件,同时其返回值也是一个高阶组件

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 { connect } from 'react-redux';
import {addNumberAction, subNumberAction} from '../store/actionCreators'

export class About extends PureComponent {
changeNumber(num, isAdd) {
if (isAdd) {
this.props.addNumber(num);
} else {
this.props.subNumber(num);
}
}

render() {
const {counter} = this.props;
return (
<div>
<h2>About Page: {counter}</h2>
<button onClick={e => this.changeNumber(6, true)}>+6</button>
<button onClick={e => this.changeNumber(6, false)}>-6</button>
</div>
)
}
}

const mapStateToProps = (state) => ({
counter: state.counter
})
const mapDispatchToProps = (dispatch) => ({
addNumber(num) {
dispatch(addNumberAction(num));
},
subNumber(num) {
dispatch(subNumberAction(num));
}
})
// connect返回值是一个高阶组件
export default connect(mapStateToProps, mapDispatchToProps)(About)

10. redux-thunk库的使用

10.1 redux-thunk是如何支持发送异步请求的

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

10.2 案例

redux-thunk用于增强redux,原本redux中的dispatch只能返回一个对象
通过使用thunk,可以返回一个函数,这主要是在异步请求获取数据的时候会用到
案例:

1
2
3
4
5
6
7
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
import thunk from "redux-thunk";

const store = createStore(reducer, applyMiddleware(thunk));

export default 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
import * as actionTypes from "./constants"
import axios from 'axios'

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

export const changeRecomendsAction = (recommends) => ({
type: actionTypes.CHANGE_RECOMMENDS,
recommends
})

export const fetchHomeMultidataAction = () => {
return function(dispatch, getState) {
axios.get("http://123.207.32.32:8080/home/multidata").then(res => {
const banners = res.data.banner.list;
const recommends = res.data.data.recommend.list;

dispatch(changeBannersAction(banners));
dispatch(changeRecomendsAction(recommends));
})
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { PureComponent } from 'react'
import {fetchHomeMultidataAction} from '../store/actionCreators'
import { connect } from 'react-redux';

export class Category extends PureComponent {
componentDidMount() {
this.props.fetchHomeMultidata();
}

render() {
return (
<div>
<h2>Category</h2>
</div>
)
}
}

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

export default connect(null, mapDispatchToProps)(Category)

11. combineReducers函数

11.1. 简介

  • 目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象
  • 事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并
1
2
3
4
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer
})
  • combineReducers是如何实现的
    • 事实上,它也是将我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了)
    • 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
    • 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;

11.2 combineReducers实现原理

1
2
3
4
5
6
function reducer(state = {}, action) {
return {
counter: counterReducer(state.counter, action),
home: homeReducer(state.home, action)
}
}

12. Redux Toolkit

12.1 简介

  • 在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦
  • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理)
  • Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
  • 在很多地方为了称呼方便,也将之称为“RTK”

12.2 安装

1
npm install @reduxjs/toolkit react-redux

12.3 核心API

  • configureStore:包装createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension
  • createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions
  • createAsyncThunk:接受一个动作类型字符串和一个返回promise的函数,并生成一个pending/fulfilled/rejected基于该promise分派动作类型的thunk

12.4 createSlice

参数:

  1. name:用户标记slice的名词
    1. 在之后的redux-devtool中会显示对应的名词
  2. initialState:初始化值
    1. 第一次初始化时的值
  3. reducer:相当于之前的reducer函数
    1. 对象类型,并且可以添加很多函数
    2. 函数类似于redux原来reducer中的一个case语句
    3. 函数的参数:
      1. 参数一:state
      2. 参数二:调用这个action时,传递的action参数

createSlice返回值是一个对象,包含所有的actions
案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
name: "counter",
initialState: {
counter: 0
},
reducers: {
addNumber(state, action) {

},
subNumber(state, action) {

}
}
})

export const {addNumber, subNumber} = counterSlice.actions;
export default counterSlice.reducer;

相当于:

12.5 configureStore

configureStore用于创建store对象,常见参数如下

  • reducer:将slice中的reducer可以组成一个对象传入此处
  • middleware:可以使用参数,传入其他的中间件
  • devTools:是否配置devTools工具,默认为true

案例:

1
2
3
4
5
6
7
8
9
10
11
12
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counter";
import homeReducer from "./features/home";

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

export default store;

12.6 案例

编写counterSlice,相当于之前的constants + actionCreators + reducer

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: 5
},
reducers: {
// reducer根据action修改state,action包含type和payload两个属性
addNumber(state, action) {
const {payload} = action;
state.counter = state.counter + payload;
},
subNumber(state, {payload}) {
state.counter = state.counter - payload;
}
}
})

export const {addNumber, subNumber} = counterSlice.actions

export default counterSlice.reducer;

创建store

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;

使用redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import {Provider} from "react-redux"
import store from "./store"

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { subNumber, addNumber } from '../store/features/counter';

export class Profile extends PureComponent {
subNumber(num) {
// 这里的subNumber就是下面mapDispatchToProps中的subNumber
this.props.subNumber(num);
}
addNumber(num) {
this.props.addNumber(num);
}
render() {
const {counter} = this.props;
return (
<div>
<h2>Page Counter: {counter}</h2>
<button onClick={e => this.subNumber(5)}>-5</button>
<button onClick={e => this.addNumber(5)}>+5</button>
</div>
)
}
}

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

const mapDispatchToProps = (dispatch) => ({
addNumber(num) {
// dispatch的addNumber是'../store/features/counter'中的addNumber
dispatch(addNumber(num))
},
subNumber(num) {
dispatch(subNumber(num))
}
})

export default connect(mapStateToProps, mapDispatchToProps)(Profile)

12.7 Redux Toolkit的异步操作

在之前的开发中,通过redux-thunk中间件让dispatch中可以进行异步操作
redux toolkit默认已经集成了thunk相关的功能:createAsyncThunk

1
2
3
4
export const fetchHomeMultidataAction = createAsyncThunk("fetch/homemultidata", async () => {
const res = await axios.get("http://123.207.32.32:8080/home/multidata");
return res.data.data;
})

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

  • pending:action被发出,但是还没有最终的结果
  • fulfilled:获取到最终的结果(有返回值的结果)
  • rejected:执行过程中有错误或者抛出了异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const homeSilce = createSlice({
name: "home",
initialState: {
banners: [],
recommends: []
},
// 监听异步操作
extraReducers: {
// 将计算属性名作为函数名
[fetchHomeMultidataAction.pending](state, action) {
console.log('fetchHomeMultidataAction pending');
},
[fetchHomeMultidataAction.fulfilled](state, {payload}) {
console.log('fetchHomeMultidataAction fulfilled');
state.banners = payload.data.banner.list;
state.recommends = payload.data.recommend.list;
},
[fetchHomeMultidataAction.rejected](state, action) {
console.log('fetchHomeMultidataAction rejected');
}
}
})
1
2
3
4
5
6
7
8
extraReducers: (builder) => {
builder.addCase(fetchHomeMultidataAction.pending, (state, action) => {
console.log("fetchHomeMultidataAction pending");
}).addCase(fetchHomeMultidataAction.fulfilled, (state, {payload}) => {
state.banners = payload.data.banner.list;
state.recommends = payload.data.recommend.list;
})
}

12.8 Redux Toolkit的数据不可变性(了解)

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

13. Redux-connect函数实现原理

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 { PureComponent } from "react";
import {StoreContext} from "./StoreContext";

export function connect(mapStateToProps, mapDispatchToProps) {

return function(WrapperComponent) {
class NewComponent extends PureComponent {
constructor(props, context) {
super(props);
this.state = mapStateToProps(context.getState());
}

componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState(mapStateToProps(this.context.getState()));
})
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const stateObj = mapStateToProps(this.context.getState());
const dispatchObj = mapDispatchToProps(this.context.dispatch);
return <WrapperComponent {...this.props} {...stateObj} {...dispatchObj}/>;
}
}
NewComponent.contextType = StoreContext;
return NewComponent;
}
}
1
2
3
import { createContext } from "react";

export const StoreContext = createContext();
1
2
export {StoreContext} from "./StoreContext";
export {connect} from "./connect";

使用的时候需要在App外面再包裹一层StoreContext.Provider

1
2
3
4
5
6
7
8
9
10
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<StoreContext.Provider value={store}>
<App/>
</StoreContext.Provider>
</Provider>
</React.StrictMode>
);
作者

zwx

发布于

2024-08-23

更新于

2024-08-23

许可协议

评论