React 之 Redux - 状态管理

一、前言

1. 纯函数

函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
  • 确定的输入,一定会产生确定的输出

  • 函数在执行过程中,不能产生副作用

2. 副作用

表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,
比如修改了全局变量,修改参数或者改变外部的存储
  • 纯函数在执行的过程中就是不能产生这样的副作用

  • 副作用往往是产生bug的 “温床”

3. 纯函数的案例

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

  • splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改

4. 判断是否纯函数

5. 纯函数的作用和优势

纯函数在函数式编程中非常重要 :
  • 可以安心的编写和安心的使用

  • 在写的时候保证了函数的纯度,只是单纯实现业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改

  • 在用的时候,确定的输入内容不会被任意篡改,并且确定的输入,一定会有确定的输出

6. 在React中

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

7. 为啥使用Redux

二、Redux的核心理念

1. 核心理念 - store

store : 用于存储共享数据的仓库

2. 核心理念 - action

action : store中所有数据的变化,必须通过派发(dispatch)action来更新
action是一个普通的JavaScript对象,用来描述这次更新的type和content

3. 核心理念 - reducer

reducer : reducer是一个纯函数,将state和action联系在一起
reducer做的事情就是将传入的state和action结合起来生成一个新的state

4. Redux的三大原则

单一数据源

  • 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store

  • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护

  • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改

State是只读的

  • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State

  • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state

  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题

使用纯函数来执行修改

  • 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State

  • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分

  • 但是所有的reducer都应该是纯函数,不能产生任何的副作用

三、Redux的基本使用

1. 测试项目搭建

01. 创建一个新的项目文件夹: learn-redux

02. 进入文件夹后进行项目初始化
npm init -y

03. 安装redux
npm install redux

04. 创建src目录,以及其中的index.js文件

05. 修改package.json可以执行index.js
也可不修改,直接控制台输入 node src/index.js

"scripts": {// node v13.2.0之后,添加属性:"type": "module",就可以在node中对ES6模块化的支持"type": "module","start": "node src/index.js"
},

06. 在src中创建目录store,并创建index.js

// 需要配置下type,否则的话直接使用commonjs也是可以的
import { createStore } from 'redux';// 1. 定义初始化数据
const initialState = {name: 'coder',age: 18,counter: 100
};// 2. 定义reducer纯函数
// 2.1 reducer函数的第一个参数是state,第二个参数是action
// 2.2 reducer函数必须返回一个新的state,这个state会覆盖原来的state
// 2.3 reducer函数中,不能修改原来的state,必须返回一个新的state// 第一次调用reducer函数的时候,state是undefined,所以给state设置默认值,只会在第一次调用的时候生效
const reducer = (state = initialState, action) => {// 2.4 在这里,根据action的type,来决定如何修改state,返回一个新的stateswitch (action.type) {case 'xxx' : return {...state,...}}// 2.5 没有数据更新的时候,返回一个新的statereturn state;
};// 3. 创建store
export const store = createStore(reducer);

2. 使用store中的数据


// 1. 导入store
import { store } from './store/index.js';// 2. 拿到store中存储的数据
console.log(store.getState()); // { name: 'coder', age: 18, counter: 100 }

3. 修改store中的数据

执行代码

// 导入store
import { store } from './store/index.js';// 创建一个action对象
const nameAction = { type: 'change_name', name: 'star' };
// 进行派发, 派发的时候,会调用reducer函数,传入state和action
store.dispatch(nameAction)// 创建一个action对象
const counterAction = { type: 'increment_counter', number: 10 };
// 进行派发
store.dispatch(counterAction);console.log(store.getState()); // { name: 'star', age: 18, counter: 110 }
在reducer函数中新增判断

const reducer = (state = initialState, action) => {// action => { type: 'change_name', name: 'star' }switch (action.type) {case 'change_name':return {// 先对原来staet进行解构...state,// 再对name进行覆盖name: action.name};case 'increment_counter':return { ...state, counter: state.counter + action.number };default:// 如果没有匹配到action.type,就返回原来的statereturn state;}
};

4. 订阅store中的数


// 导入store
import { store } from './store/index.js';// 监听state的变化, 只要state发生了变化,就会调用回调函数
// 返回值是一个函数,调用这个函数,就可以取消监听
const unSubscribe = store.subscribe(() => {console.log('state发生了变化');console.log(store.getState());
});// 派发action => 可以监听到
store.dispatch({ type: 'change_name', name: 'star' });// 取消监听
unSubscribe();// 派发action => 不会监听到,但是state会发生变化,因为store.dispatch会调用reducer函数
store.dispatch({ type: 'increment_counter', number: 10 });

5. 优化

动态生成action


// 导入store
import { store } from './store/index.js';store.subscribe(() => {console.log('state发生了变化', store.getState());
});// 动态生成action : actionCreator => 创建action的函数, 返回一个action对象
const changeNameAction = (name) => ({ type: 'change_name', name });// store.dispatch({ type: 'change_name', name: 'star' });
// store.dispatch({ type: 'change_name', name: 'coderstar' });
store.dispatch(changeNameAction('star'));
store.dispatch(changeNameAction('coderstar'));

目录结构优化

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

创建store/index.js文件

import { createStore } from 'redux';import reducer from './reducer.js';// 创建store
export const store = createStore(reducer);
创建store/reducer.js文件

import { CHANGE_NAME, INCREMENT_COUNTER } from './constants.js';// 1. 定义初始化数据
const initialState = {name: 'coder',age: 18,counter: 100
};// 2. 定义reducer
export const reducer = (state = initialState, action) => {switch (action.type) {case CHANGE_NAME:return {// 先对原来staet进行解构...state,// 再对name进行覆盖name: action.name};case INCREMENT_COUNTER:return { ...state, counter: state.counter + action.number };default:// 如果没有匹配到action.type,就返回原来的statereturn state;}
};
创建store/actionCreators.js文件 => 用于生成action

// Description: Action Creatorsimport { CHANGE_NAME, INCREMENT_COUNTER } from './constants.js';// 修改name名字的action
export const changeNameAction = (name) => ({ type: CHANGE_NAME, name });// 修改counter的action
export const incrementCounterAction = (number) => ({ type: INCREMENT_COUNTER, number });
创建store/constants.js文件 => 用于定义type常量

// Desc: constants for actionsexport const CHANGE_NAME = 'change_name';
export const INCREMENT_COUNTER = 'increment_counter';

6. Redux使用流程图

四、React结合Redux

1. 安装

npm install redux

2. 基本使用

 store文件夹

index.js

import { createStore } from 'redux';
import { reducer } from './reducer';export const store = createStore(reducer);

 constants.js

export const CHANGE_COUNTER = 'change_counter';export const CHANGE_BANNER = 'change_banner';

reducer.js

import { CHANGE_BANNER, CHANGE_COUNTER } from './constants';const initialState = {counter: 20,bannerList: []
};export const reducer = (state = initialState, action) => {switch (action.type) {case CHANGE_COUNTER:return { ...state, counter: state.counter + action.counter };case CHANGE_BANNER:return { ...state, bannerList: [...state.bannerList, ...action.bannerList] };default:return state;}
};

actionCreatores.js

import { CHANGE_COUNTER, CHANGE_BANNER } from './constants';// 修改counter的action
export const changeCounterAction = (counter) => ({ type: CHANGE_COUNTER, counter });// 修改bannerList的action
export const changeBannerAction = (bannerList) => ({ type: CHANGE_BANNER, bannerList });

page文件夹

Home.jsx

import React, { PureComponent } from 'react';
import { store } from '../store';
import { changeCounterAction } from '../store/actionCreatores.js';export class Home extends PureComponent {constructor(porps) {super(porps);this.state = {// 从store中获取counter的值,赋予初始化值counter: store.getState().counter};}componentDidMount() {// 监听store的变化store.subscribe(() => {this.setState({counter: store.getState().counter});});}changeCounter(num) {// 改变store中的值,通过dispatch派发actionstore.dispatch(changeCounterAction(num));}render() {const { counter } = this.state;return (<><h2>Home counter : {counter}</h2><button onClick={(e) => this.changeCounter(5)}>+5</button>{'                                                       '}<button onClick={(e) => this.changeCounter(10)}>+10</button></>);}
}export default Home;

Profily.jsx

import React, { PureComponent } from 'react';
import { store } from '../store';
import { changeCounterAction } from '../store/actionCreatores.js';export class Profily extends PureComponent {constructor(porps) {super(porps);this.state = {// 从store中获取counter的值,赋予初始化值counter: store.getState().counter};}componentDidMount() {// 监听store的变化store.subscribe(() => {this.setState({counter: store.getState().counter});});}changeCounter(num) {// 改变store中的值,通过dispatch派发actionstore.dispatch(changeCounterAction(num));}render() {const { counter } = this.state;return (<><h2>Profily counter : {counter}</h2><button onClick={(e) => this.changeCounter(-5)}>-5</button>{'                                                       '}<button onClick={(e) => this.changeCounter(-10)}>-10</button></>);}
}export default Profily;

App.jsx

import React, { PureComponent } from 'react';
import Home from './page/Home';
import Profily from './page/Profily';import { store } from './store';export class App extends PureComponent {constructor(porps) {super(porps);this.state = {// 从store中获取counter的值,赋予初始化值counter: store.getState().counter};}componentDidMount() {// 监听store的变化store.subscribe(() => {this.setState({counter: store.getState().counter});});}render() {const { counter } = this.state;return (<div style={{ textAlign: 'center', marginTop: '100px' }}><h2>App counter : {counter}</h2><hr /><hr /><hr /><Home /><hr /><hr /><hr /><Profily /></div>);}
}export default App;

3. react-redux的使用

  • redux和react没有直接的关系,完全可以在React, Angular, Ember, jQuery, or vanilla 
    JavaScript中使用Redux

  • redux依然是和React库结合的更好

  • redux官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效

将组件和store连接

安装 => npm install react-redux

1. 修改index.js 

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
// 引入Provider组件, 用于给整个项目提供一个公共的store
import { Provider } from 'react-redux';
import { store } from './store';const root = ReactDOM.createRoot(document.querySelector('#root'));root.render(<React.StrictMode>{/* 给整个项目提供一个公共的store */}<Provider store={store}><App /></Provider></React.StrictMode>
);

2. 组件中使用

需要进行映射,state、dispatch都需要映射

import React, { PureComponent } from 'react';
// 1. 引入connect,用于连接组件和redux,返回一个高阶组件
import { connect } from 'react-redux';
import { changeCounterAction } from '../store/actionCreatores';export class About extends PureComponent {changeCounter(num) {// 4. 从props中取出changeCounter,不再从state中取,因为state中的值已经被映射到props中了const { changeCounter } = this.props;// 执行了下面定义的方法,相当于调用了dispatch(changeCounterAction(num))changeCounter(num);}render() {// 3. 从props中取出counter,不再从state中取,因为state中的值已经被映射到props中了const { counter } = this.props;return (<><h2>About : {counter}</h2><button onClick={(e) => this.changeCounter(8)}>+8</button>{'                                                       '}<button onClick={(e) => this.changeCounter(-8)}>-8</button></>);}
}/*** connect => connect()()返回一个高阶组件,第一个()传入两个参数,第二个()传入一个组件,返回一个新的 高阶组件🌟* 第一个()传入两个参数,第一个参数是一个函数,第二个参数是一个对象*    第一个参数函数,函数的参数是state,返回一个对象,对象的属性是state,值是state*    第二个参数对象,对象的属性是一个函数,函数的参数是dispatch,返回一个对象,对象的属性是函数,函数的参数是参数,返回一个action* 第二个()传入一个组件,返回一个新的组件*/
// 2. 注入store中的state和dispatch到组件的props中// 用来映射state中的值到组件的props中,可以设置取其中需要的数据
const mapStateToProps = (state) => ({ counter: state.counter });
// 用来映射dispatch到组件的props中,可以设置取其中需要的方法
const mapDispatchToProps = (dispatch) => ({changeCounter: (num) => dispatch(changeCounterAction(num))
});export default connect(mapStateToProps, mapDispatchToProps)(About);

3. 组件中异步操作

About组件        =>   请求banners数据
Category组件   =>   展示banners数据

效果

About组件 

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { changeCounterAction, changeBannerAction } from '../store/actionCreatores';
import axios from 'axios';export class About extends PureComponent {changeCounter(num) {const { changeCounter } = this.props;changeCounter(num);}async getData() {const res = await axios.get('http://xxxxxx');const bannerList = res?.data?.data?.banner?.list || [];const { changeBanner } = this.props;// 请求数据后,改变store中的值changeBanner(bannerList);}render() {const { counter } = this.props;return (<><h2>About : {counter}</h2><button onClick={(e) => this.changeCounter(8)}>+8</button>{'                                                       '}<button onClick={(e) => this.changeCounter(-8)}>-8</button>{'                                                       '}<button onClick={(e) => this.getData()}>请求数据</button></>);}
}const mapStateToProps = (state) => ({ counter: state.counter });
const mapDispatchToProps = (dispatch) => ({changeCounter: (num) => dispatch(changeCounterAction(num)),changeBanner: (bannerList) => dispatch(changeBannerAction(bannerList))
});export default connect(mapStateToProps, mapDispatchToProps)(About);

Category组件

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';export class Category extends PureComponent {render() {const { bannerList } = this.props;return (<><h2>Category</h2><ul>{bannerList.map((item, index) => {return <li key={index}>{item.title}</li>;})}</ul></>);}
}const mapStateToProps = (state) => ({ bannerList: state.bannerList });export default connect(mapStateToProps, null)(Category);

4. redux中异步操作

上面的代码有一个缺陷

  • 必须将网络请求的异步代码放到组件的生命周期中来完成
  • 事实上,网络请求到的数据也属于状态管理的一部分,更好的一种方式应该是将其也交给redux来管理

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的使用

安装    =>     npm install redux-thunk

store/index.js

// applyMiddleware => 应用中间件
import { createStore, applyMiddleware } from 'redux';import thunk from 'redux-thunk';
import { reducer } from './reducer';// 正常情况下,store.dispatch只能接收对象 store.dispatch(对象object)// 想要派发函数store.dispatch(函数function),需要对store进行增强,增强的方式是使用中间件
// 但是如果使用了redux-thunk中间件,那么store.dispatch就可以接收函数 store.dispatch(函数function)// 1. 引入中间件 applyMiddleware(thunk, logger, ...) => 可以传入多个中间件
const enhancer = applyMiddleware(thunk);
// 2. 使用中间件
export const store = createStore(reducer, enhancer);

actionCreatores.js

import axios from 'axios';
import { CHANGE_COUNTER, CHANGE_BANNER } from './constants';// 修改counter的action
export const changeCounterAction = (counter) => ({ type: CHANGE_COUNTER, counter });// 修改bannerList的action
export const changeBannerAction = (bannerList) => ({ type: CHANGE_BANNER, bannerList });// 异步获取bannerList的action
export const getBannerListAction = () => {// 如果是普通的action,那么返回的是一个对象, {type: 'xxx', payload: 'xxx'}// 问题:对象中不能直接拿到从服务器获取的数据// return {}// 如果是函数,那么redux是不支持的,需要使用中间件redux-thunk// 一旦派发来到这个函数,那么redux-thunk就会自动执行这个函数// 这个函数的参数有来两个,第一个是dispatch => store.dispatch dispatch(xxx) , 第二个是getState => 获取store中的数据 getState().counterreturn async (dispatch, getState) => {const res = await axios.get('http://123.207.32.32:8000/home/multidata');const bannerList = res?.data?.data?.banner?.list || [];// 派发action对象dispatch(changeBannerAction(bannerList));};
};

About.jsx

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { changeCounterAction, getBannerListAction } from '../store/actionCreatores';export class About extends PureComponent {changeCounter(num) {const { changeCounter } = this.props;changeCounter(num);}getData() {const { getBannerList } = this.props;// 在这里调用store中的action发送异步请求,获取数据getBannerList();}render() {const { counter } = this.props;return (<><h2>About : {counter}</h2><button onClick={(e) => this.changeCounter(8)}>+8</button>{'                                                       '}<button onClick={(e) => this.changeCounter(-8)}>-8</button>{'                                                       '}<button onClick={(e) => this.getData()}>请求数据</button></>);}
}const mapStateToProps = (state) => ({ counter: state.counter });
const mapDispatchToProps = (dispatch) => ({changeCounter: (num) => dispatch(changeCounterAction(num)),getBannerList: () => dispatch(getBannerListAction())
});export default connect(mapStateToProps, mapDispatchToProps)(About);

5. react调试工具 - redux-devtools

redux可以方便的让我们对状态进行跟踪和调试

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

步骤一 

在对应的浏览器中安装相关的插件

比如Chrome浏览器扩展商店中搜索Redux DevTools即可

步骤二

在redux中继承devtools的中间件

import { createStore, applyMiddleware, compose } from 'redux';import thunk from 'redux-thunk';
import { reducer } from './reducer';// redux-devtools-extension,用于在浏览器中查看redux的状态 => 为了安全,只在开发环境中使用
// 生产环境
let composeEnhancers = compose;
if (process.env.NODE_ENV === 'development') {// 开发环境composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose;
}
const enhancer = applyMiddleware(thunk);export const store = createStore(reducer, composeEnhancers(enhancer));

6. Reducer文件拆分

拆分 

actionCreatores.js

import { CHANGE_COUNTER } from './constants';// 修改counter的action
export const changeCounterAction = (counter) => ({ type: CHANGE_COUNTER, counter });

constants.js

export const CHANGE_COUNTER = 'change_counter';

index.js

// 文件做统一的导出export { reducer as counterReducer } from './reducer';export * from './actionCreatores';

reducer.js

import { CHANGE_COUNTER } from './constants';const initialState = {counter: 20
};export const reducer = (state = initialState, action) => {switch (action.type) {case CHANGE_COUNTER:return { ...state, counter: state.counter + action.counter };default:return state;}
};

store   =>   下的index.js文件,用来合并各个模块的reducer

combineReducers函数可以方便的让我们对多个reducer进行合并

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
});// redux-devtools-extension,用于在浏览器中查看redux的状态 => 为了安全,只在开发环境中使用
// 生产环境
let composeEnhancers = compose;
if (process.env.NODE_ENV === 'development') {// 开发环境composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose;
}
const enhancer = applyMiddleware(thunk);export const store = createStore(reducer, composeEnhancers(enhancer));

使用

直接取数据或者映射数据时,需要加上模块名

派发action还是和以前一样,不需要更改

五、ReduxToolkit

1. 概念

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法

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

安装      =>     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

2. 基本使用

store中的代码

counter.js   =>     store/peature/counter.js

// 1. 导入创建切片的函数
import { createSlice } from '@reduxjs/toolkit';const counterSlice = createSlice({// 2. 切片名称name: 'counter',// 3. 初始状态initialState: {counter: 88},// 4. reducers => 相当于之前的reducerreducers: {// 5. 相当于之前的case,action.type就是这里的方法名称changeCounterAction(state, action) {state.counter += action.payload;}}
});// 6. 导出actions
export const { changeCounterAction } = counterSlice.actions;// 7. 导出reducer
export default counterSlice.reducer;

index.js   =>     store/index.js

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter';
import categoryReducer from './features/category';import thunk from 'redux-thunk';const store = configureStore({// 1. reducer : 将切片的reducer添加到store中reducer: {counter: counterReducer,category: categoryReducer},// 2. devTools : 开启redux-devtools,默认开启,开发环境开启,生产环境关闭devTools: process.env.NODE_ENV === 'development',// 3. middleware : 中间件,默认只有thunk,如果需要添加其他中间件,可以在这里添加middleware: (getDefaultMiddleware) => { return getDefaultMiddleware().concat(thunk);},// 不常用的配置项// 4. enhancers : 增强器,如果需要添加其他增强器,可以在这里添加enhancers: [],// 5. preloadedState : 初始状态,如果需要添加初始状态,可以在这里添加preloadedState: {},// 6. reducerPathInfo : reducer路径信息,如果需要添加reducer路径信息,可以在这里添加reducerPathInfo: {},// 7. middlewareFactories : 中间件工厂,如果需要添加中间件工厂,可以在这里添加middlewareFactories: {},// 8. devToolsEnhancerOptions : devTools增强器选项,如果需要添加devTools增强器选项,可以在这里添加devToolsEnhancerOptions: {},// 9. immutableCheck : 是否开启不可变检查,默认开启immutableCheck: true,// 10. serializableCheck : 是否开启序列化检查,默认开启serializableCheck: true,// 11. middlewareOptions : 中间件选项,如果需要添加中间件选项,可以在这里添加middlewareOptions: {},// 12. thunk : thunk中间件选项,如果需要添加thunk中间件选项,可以在这里添加thunk: {},
});export default store;

主体index.js中的代码

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { Provider } from 'react-redux';
import store from './store';const root = ReactDOM.createRoot(document.querySelector('#root'));root.render(<React.StrictMode>{/* 给整个项目提供一个公共的store */}<Provider store={store}><App /></Provider></React.StrictMode>
);

page中的代码

About.jsx    =>     page/About.jsx

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { changeCounterAction } from '../store/features/counter';export class About extends PureComponent {changeCounter(num) {// 通过props获取actionconst { changeCounterAction } = this.props;// 调用actionchangeCounterAction(num);}render() {const { counter } = this.props;return (<><h2>About : {counter}</h2><button onClick={(e) => this.changeCounter(8)}>+8</button>{'                                                       '}<button onClick={(e) => this.changeCounter(-8)}>-8</button></>);}
}// 映射Redux全局的state到组件的props上 => counter
const mapStateToProps = (state) => ({ counter: state.counter.counter });
// 映射dispatch到props上
const mapDispatchToProps = (dispatch) => ({changeCounterAction: (num) => dispatch(changeCounterAction(num)),
});export default connect(mapStateToProps, mapDispatchToProps)(About);

3. 异步操作

之前通过redux-thunk中间件让dispatch中可以进行异步操作。
Redux Toolkit默认已经继承了Thunk相关的功能:createAsyncThunk

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

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

category.js   =>     store/peature/category.js

import axios from 'axios';
import { createSlice } from '@reduxjs/toolkit';
// 1. 导入createAsyncThunk,用于创建异步action
import { createAsyncThunk } from '@reduxjs/toolkit';// 2. 创建异步action
export const getBannerListAction = createAsyncThunk('home/getBannerListAction',async (extraInfo, store) => {// extraInfo  =>   是调用异步action时传入的参数 => {name: 'coder'}// store      =>   store对象,可以获取到store中的数据和dispatch方法,但是不推荐使用const res = await axios.get('http://123.207.32.32:8000/home/multidata');// 不能直接返回res,不支持序列化会报错,需要返回res.datareturn res.data;}
);const categorySlick = createSlice({name: 'category',initialState: {bannerList: []},reducers: {changeBannerListAction(state, { payload }) {state.bannerList = payload;}},// 3. 添加异步action的处理逻辑extraReducers: {// 不需要监听的话,可以不写[getBannerListAction.pending]: (state, { payload }) => {console.log('pending');},// 4. 在这里拿到异步action的结果,然后修改state[getBannerListAction.fulfilled]: (state, { payload }) => {state.bannerList = payload.data.banner.list;},// 不需要监听的话,可以不写[getBannerListAction.rejected]: (state, { payload }) => {console.log('rejected');}}
});// 6. 导出actions
export const { changeBannerListAction } = categorySlick.actions;// 7. 导出reducer
export default categorySlick.reducer;

Category.jsx   =>     page/Category.jsx 

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';import { getBannerListAction } from '../store/features/category';
export class Category extends PureComponent {componentDidMount() {// 2. 在redux中进行数据请求this.props.getBannerListAction();}render() {// 4. 获取redux中的数据const { bannerList } = this.props;return (<><h2>Category</h2><ul>{bannerList.map((item, index) => {return <li key={index}>{item.title}</li>;})}</ul></>);}
}// 3. 映射redux中的数据
const mapStateToProps = (state) => ({ bannerList: state.category.bannerList });// 1. 映射redux中的方法
const mapDispatchToProps = (dispatch) => ({// 这里可以传递参数过去getBannerListAction: () => dispatch(getBannerListAction({ name: 'coder' }))
});
export default connect(mapStateToProps, mapDispatchToProps)(Category);

4. 异步操作的另一种写法

extraReducer还可以传入一个函数,函数接受一个builder参数

/***  extraReducer还可以传入一个函数,函数接受一个builder参数*    builder对象中包含了pending、fulfilled、rejected三个属性,分别对应异步action的三种状态*    builder对象中的属性值都是函数,函数的第一个参数是state,第二个参数是action*    builder对象中的属性值函数的返回值会直接赋值给state*/
extraReducers: (builder) => {builder.addCase(getBannerListAction.pending, (state, { payload }) => {console.log('pending');}).addCase(getBannerListAction.fulfilled, (state, { payload }) => {state.bannerList = payload.data.banner.list;}).addCase(getBannerListAction.rejected, (state, { payload }) => {console.log('rejected');});
}

5. Redux Toolkit的数据不可变性

六、其他补充

1. 自定义实现connect

组件中引入该文件即可,用法一致

实现代码

connect.js   =>     hoc/Category.js

/*** 自定义实现 connect 函数* @param {*} mapStateToProps => 函数* @param {*} mapDispatchToProps => 函数* @returns => 返回值也是函数,是个高级组件,接收一个组件作为参数*/// 1. 导入stroe
import store from '../store';export default function connect(mapStateToProps, mapDispatchToProps) {// 返回一个高级组件return function (WrappedComponent) {class NewComponent extends WrappedComponent {constructor(props) {super(props);// 2. 给state赋初始值this.state = mapStateToProps(store.getState());}componentDidMount() {// 3. 订阅store的变化, 一旦store发生变化,就会执行回调函数this.unSubscribe = store.subscribe(() => {// 4. 更新statethis.setState(mapStateToProps(store.getState()));// 直接调用父类的forceUpdate方法,强制更新, 但是不推荐使用// this.forceUpdate();});}componentWillUnmount() {// 5. 取消订阅this.unSubscribe();}render() {// 6. 拿到stateObjconst stateObj = mapStateToProps(store.getState());// 7. 拿到dispatchObjconst dispatchObj = mapDispatchToProps(store.dispatch);// 8. 合并stateObj和dispatchObj,传递给WrappedComponentreturn <WrappedComponent {...this.props} {...stateObj} {...dispatchObj} />;}}return NewComponent;};
}

优化代码

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

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

  主体index.js中的代码

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { Provider } from 'react-redux';
import store from './store';
// 导入StoreContext
import { StoreContext } from './hoc';const root = ReactDOM.createRoot(document.querySelector('#root'));root.render(<React.StrictMode><Provider store={store}>{/* 3. 使用StoreContext.Provider包裹App组件 => 把store传进去 */}<StoreContext.Provider value={store}><App /></StoreContext.Provider></Provider></React.StrictMode>
);

connect.js   =>     hoc/connect.js

/*** 自定义实现 connect 函数* @param {*} mapStateToProps => 函数* @param {*} mapDispatchToProps => 函数* @returns => 返回值也是函数,是个高级组件,接收一个组件作为参数*/// 不直接引入store, 而是从storeContext中获取
// import store from '../store';// 1. 引入storeContext
import { StoreContext } from './index.js';// 3. 这里的context就是store   =>   因为在index.js中,使用storeContext.Provider包裹了App组件,把store传进去了export function connect(mapStateToProps, mapDispatchToProps) {// 返回一个高级组件return function (WrappedComponent) {class NewComponent extends WrappedComponent {// 这里有两个参数,props和context,context就是storeconstructor(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 <WrappedComponent {...this.props} {...stateObj} {...dispatchObj} />;}}// 2. 使用storeContext.Consumer包裹NewComponent组件, 传入storeNewComponent.contextType = StoreContext;return NewComponent;};
}

storeContext.js   =>     hoc/storeContext.js

// 1. 引入createContext, 用于创建上下文
import { createContext } from 'react';// 2. 创建上下文
export const StoreContext = createContext();

index.js   =>     hoc/index.js

// 统一的导出export { connect } from './connect';export { StoreContext } from './storeContext';

2. 打印日志需求

在dispatch之前 : 打印一下本次的action对象

在dispatch完成之后 : 打印一下最新的store state

01 - 手动修改

缺陷非常明显:

  • 首先,每一次的dispatch操作,都需要在前面加上这样的逻辑代码
  • 其次,存在大量重复的代码,会非常麻烦和臃肿

02 - 修改dispatch

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

  • 直接修改了dispatch的调用过程
  • 在调用dispatch的过程中,真正调用的函数其实是dispatchAndLog
// 导出之前,做一层拦截,派发action之前,做一些事情,比如  =>  打印日志
function interceptStoreLog(store) {// 1. 拿到原始的dispatch方法const next = store.dispatch;// 2. 重写dispatch方法 => 在组件中派发action的时候,其实就是调用的这个方法store.dispatch = function dispatchAndLog(action) {console.log('当前派发的action', action);// 3. 调用原始的dispatch方法,派发actionnext(action);console.log('派发之后的结果', store.getState());};
}
interceptStoreLog(store);export default store;

3. 自定义实现thunk中间件

// 自定义实现thunk中间件
function thunkMiddleware(store) {// 1. 拿到原始的dispatch方法const next = store.dispatch;// 2. 重写dispatch方法 => 在组件中派发action的时候,其实就是调用的这个方法store.dispatch = function dispatchAndThunk(action) {// 3. 判断action是不是函数if (typeof action === 'function') {// 4. 如果是函数,就调用这个函数,并且传入dispatch和getState方法 => 使用新的dispatch方法,因为可能又派发了函数action(store.dispatch, store.getState);} else {// 5. 如果是对象,就调用原始的dispatch方法next(action);}};
}
thunkMiddleware(store);

4. 合并自定义的中间件

目录修改

log.js        =>        middleware/log.js

// 导出之前,做一层拦截,派发action之前,做一些事情,比如  =>  打印日志
export default function logMiddleware(store) {// 1. 拿到原始的dispatch方法const next = store.dispatch;// 2. 重写dispatch方法 => 在组件中派发action的时候,其实就是调用的这个方法store.dispatch = function dispatchAndLog(action) {console.log('当前派发的action', action);// 3. 调用原始的dispatch方法,派发actionnext(action);console.log('派发之后的结果', store.getState());};
}

thunk.js        =>        middleware/thunk.js

// 自定义实现thunk中间件
export default function thunkMiddleware(store) {// 1. 拿到原始的dispatch方法const next = store.dispatch;// 2. 重写dispatch方法 => 在组件中派发action的时候,其实就是调用的这个方法store.dispatch = function dispatchAndThunk(action) {// 3. 判断action是不是函数if (typeof action === 'function') {// 4. 如果是函数,就调用这个函数,并且传入dispatch和getState方法 => 使用新的dispatch方法,因为可能又派发了函数action(store.dispatch, store.getState);} else {// 5. 如果是对象,就调用原始的dispatch方法next(action);}};
}

index.js        =>        middleware/thunk.js 

export { default as logMiddleware } from './log';
export { default as thunkMiddleware } from './thunk';

合并

封装一个函数来实现所有的中间件合并

七、React中的state如何管理

目前主要有三种状态管理方式:

  • 方式一:组件中自己的state管理
  • 方式二:Context数据的共享状态
  • 方式三:Redux管理应用状态

推荐使用 : 

  • UI相关的组件内部可以维护的状态,在组件内部自己来维护
  • 大部分需要共享的状态,都交给redux来管理和维护
  • 从服务器请求的数据(包括请求的操作),交给redux来维护
  • 根据不同的情况会进行适当的调整

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/75010.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker实战-关于Docker镜像的相关操作(一)

导语   镜像&#xff0c;Docker中三大核心概念之一&#xff0c;并且在运行Docker容器之前需要本地存储对应的镜像。那么下面我们就来介绍一下在Docker中如何使用镜像。 如何获取镜像&#xff1f; 镜像作为容器运行的前提条件&#xff0c;在Docker Hub上提供了各种各样的开放的…

Edge浏览器安装vue devtools

1. 下载地址 GitHub - vuejs/devtools: ⚙️ Browser devtools extension for debugging Vue.js applications. 2. 下载后的压缩包解压并打开文件夹&#xff0c;右键选择&#xff1a;git bush here 3. 安装依赖 npm install 4. 成功安装依赖后打包 npm run build

万界星空科技/免费开源MES系统/免费仓库管理

仓库管理&#xff08;仓储管理&#xff09;&#xff0c;指对仓库及仓库内部的物资进行收发、结存等有效控制和管理&#xff0c;确保仓储货物的完好无损&#xff0c;保证生产经营活动的正常进行&#xff0c;在此基础上对货物进行分类记录&#xff0c;通过报表分析展示仓库状态、…

基于opencv的几种图像滤波

一、介绍 盒式滤波、均值滤波、高斯滤波、中值滤波、双边滤波、导向滤波。 boxFilter() blur() GaussianBlur() medianBlur() bilateralFilter() 二、代码 #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> …

OpenLayers入门,OpenLayers使用fetch加载并显示TopoJson格式区划边界数据并叠加文字标注,动态创建要素样式

专栏目录: OpenLayers入门教程汇总目录 前言 上一章中已经说明了TopoJson格式数据,《OpenLayers入门,OpenLayers加载TopoJson数据,使用行政区划边界作为示例》,大家应该都已经有所了解。 本章在上一章基础上改用fetch加载TopoJson格式数据,并手动解析数据为Feature要素…

【GITHUB】FlipIt – Windows的开源翻页时钟

FlipIt 是一款免费开源的翻页时钟应用&#xff0c;专为 Windows 平台设计。该应用灵感来源于备受喜爱的老牌翻页时钟应用 Fliqlo&#xff0c;后者被公认为经典的翻页时钟屏保。然而&#xff0c;由于 Fliqlo 是基于 Flash 技术开发的&#xff0c;随着微软最近正式禁用 Flash&…

contact.sh:一款专门用于安全漏洞报告的公开资源情报工具

关于contact.sh contact.sh一款专门用于安全漏洞报告的公开资源情报工具&#xff0c;在该工具的帮助下&#xff0c;广大研究人员可以扫描企业域名并搜索企业中的用户账号&#xff0c;并根据安全威胁等级来发送安全漏洞报告。 工具安装 GNU/Linux 在下载该工具之前&#xff…

多线程之NSOperation

套话 与GCD一样&#xff0c;NSOperation也是我们日常开发中经常用到的多线程技术。本文将会介绍NSOperation的基本使用、添加依赖、 初次使用 NSOperation是个抽象类&#xff0c;依赖于子类NSInvocationOperation、NSBlockOperation去实现 下面是开发者文档上对NSOperation…

macOS 虚拟桌面黑屏(转)

转自&#xff1a;macOS重置虚拟桌面、macOS 虚拟桌面黑屏 有几次出现如图的情况&#xff0c;以为是iTerm的问题&#xff0c;但是在关闭软件&#xff0c;重启之后&#xff0c;依旧无效。 后面经过网友告知&#xff0c;才知道是虚拟桌面的问题。 为了清理这个问题&#xff0c;有以…

ELD透明屏在智能家居中有哪些优点展示?

ELD透明屏是一种新型的显示技术&#xff0c;它能够在不需要背光的情况下显示图像和文字。 ELD透明屏的原理是利用电致发光效应&#xff0c;通过在透明基板上涂覆一层特殊的发光材料&#xff0c;当电流通过时&#xff0c;发光材料会发出光线&#xff0c;从而实现显示效果。 ELD…

论文研读|多媒体自动评论生成发展综述

前言&#xff1a;多媒体自动评论生成旨在通过使用生成模型&#xff0c;对给定上下文生成符合情境的评论&#xff0c;近年来&#xff0c;随着图像描述等跨模态工作取得较大突破&#xff0c;相关研究也逐渐展开。评论作为社交平台互动的重要组成部分&#xff0c;在引导舆论、提升…

python-MySQL数据库建表语句(需要连接数据库)转存为Excel文档-工作小记

将create table XXXXXX 转为指定Excel文档。该脚本适用于数据库表结构本地文档记录 呈现效果 代码 # -*- coding:utf-8 -*- # Time : 2023/8/2 15:14 # Author: 水兵没月 # File : MySQL建表_2_excel.py import reimport mysql.connector import pandas as pd db 库名 mydb …

Linux的基本指令(2)

指令1&#xff1a;man 作用&#xff1a;可以查询linux指令语法内容。 格式&#xff1a; man 指令 安装man指令&#xff1a; yum install -y man-pages 例如&#xff1a; 查询 指令 ls 的语法内容。 man ls 查询 fork 指令的语法内容。 man fork 在man中存在9个手册&…

【腾讯云Cloud Studio实战训练营】使用Cloud Studio迅捷开发一个3D家具个性化定制应用

目录 前言&#xff1a; 一、腾讯云 Cloud Studio介绍&#xff1a; 1、接近本地 IDE 的开发体验 2、多环境可选&#xff0c;或连接到云主机 3、随时分享预览效果 4、兼容 VSCode 插件 5、 AI代码助手 二、腾讯云Cloud Studio项目实践&#xff08;3D家具个性化定制应用&…

Day51 算法记录| 动态规划 18(单调栈)

单调栈 739. 每日温度496.下一个更大元素 I503. 下一个更大元素 II42. 接雨水84. 柱状图中最大的矩形 单调栈&#xff1a;找最近的比他大的值 最近大的值&#xff1a;需要一个单调递减的栈&#xff08;大于栈顶元素就弹出&#xff09; 最近最小值&#xff1a;单调递减栈 方向&a…

idea-常用插件汇总

idea-常用插件汇总 码云插件 这个插件是码云提供的ps-码云是国内的一款类似github的代码托管工具。 Lombok Lombok是一个通用Java类库&#xff0c;能自动插入编辑器并构建工具&#xff0c;简化Java开发。通过添加注解的方式&#xff0c;不需要为类编写getter或setter等方法…

记一次 .NET 某物流API系统 CPU爆高分析

一&#xff1a;背景 1. 讲故事 前段时间有位朋友找到我&#xff0c;说他程序CPU直接被打满了&#xff0c;让我帮忙看下怎么回事&#xff0c;截图如下&#xff1a; 看了下是两个相同的程序&#xff0c;既然被打满了那就抓一个 dump 看看到底咋回事。 二&#xff1a;为什么会打…

【雕爷学编程】Arduino动手做(181)---Maixduino AI开发板11

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

中断管理

其实&#xff0c;关于中断的概念和定义在之前已经反复学习和实践了&#xff0c;这节主要讲和FreeRTOS相关的中断知识。 中断优先级 任何中断的优先级都大于任务&#xff01; 在我们的操作系统&#xff0c;中断同样是具有优先级的&#xff0c;并且我们也可以设置它的优先级&a…

汽车EBSE测试流程分析(四):反思证据及当前问题解决

EBSE专题连载共分为“五个”篇章。此文为该连载系列的“第四”篇章&#xff0c;在之前的“篇章&#xff08;三&#xff09;”中已经结合具体研究实践阐述了“步骤二&#xff0c;通过系统调研确定改进方案”等内容。那么&#xff0c;在本篇章&#xff08;四&#xff09;中&#…