Redux状态管理
Redux状态管理
概述
Redux是一个用于JavaScript应用程序状态管理的开源库。它主要被用于处理前端应用中数据的流动和状态的管理。Redux遵循单一数据流的原则,通过一个中央数据存储(称为Store)来管理应用的所有状态,并使用纯函数(称为Reducers)来修改状态。
同级元素想做数据交互,就必须通过相同父级。Redux可以解决这个问题。Store就像一个独立的状态库,管理着所有组件的数据,涉及增删改查时都与Store做交互,而不是另外的目标组件。
此外,这种状态变化是响应式的,一个组件修改数据,其它用到的组件可以同时响应。状态变化可预测、可监控。
如果项目中数据交互与组件层级非常庞杂,数据需要来回传递,才需要redux。
核心概念
- store:仓库,项目中唯一,存储状态;
- state:状态,保存当前数据;
- reducer:指定应用状态的变化如何响应actions并发给store;
- actions:把数据传到store中,store传给reducer修改再传回store。
项目中集成Redux
安装
redux本身只给js,react要用要单独安一遍:
npm install --save redux
npm install --save react-redux
基本使用
一般项目结构为:
├─ 📁src
│ ├─ 📁components
│ ├─ 📁redux
│ │ ├─ 📁reducers
│ │ │ └─ 📄count.js
│ │ ├─ 📁actions
│ │ │ └─ 📄count.js
│ │ └─ 📁store
│ │ └─ 📄index.js
│ └─ 📄index.js
创建仓库:
import { configureStore } from "@reduxjs/toolkit";
import count from './redux/reducers/count';
export default configureStore(count);
其中configureStore(reducer, middleware)
中reducer参数必填,那么就需要创建一个reducer。在redux/reducers
创建一个count.js
来管理状态:
const initState = {
count: 0
}
export default count = (state=initState, action) => {
switch(action.type) {
default:
return state;
}
}
关联,在项目的入口文件引入关联方案:
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux'
import store from './redux/store'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<div></div>
</Provider>
);
这样一来整个项目都可以读取到store的内容。一个简单的读取:
store.getState().count;
工具安装
在浏览器安装redux和react的开发工具,另外在项目中也要安装:
npm install --save-dev redux-devtools-extension
在redux的index中开启:
import { configureStore } from "@reduxjs/toolkit";
import count from './redux/reducers/count';
import { composeWithDevTools } from 'redux-devtools-extension'
export default configureStore(count, composeWithDevTools());
再启动项目就可以开启此工具。启动后工具框如图:

mapStateToProps读取state
每个组件都要关联redux才能直接读取到其中的数据。在组件开头引入关联方案:
import { connect } from 'react-redux'
最后导出时,用connect
绑定mapStateToProps
与类名:
const mapStateToProps = state => {
console.log(state);
return state;
}
export default connect(mapStateToProps)(Read)
这样一来组件中可以直接用props读store中的数据:
<p>{ this.props.count }</p>
mapDispatchToProps修改state
由于修改数据必须通过actions和reducers,所以先配置这两项。假设要在刚才的基础上通过事件来修改count的值:
<p>{ this.props.count }</p>
<button onClick={ this.handleClickAdd }>+</button>
<button onClick={ this.handleClickMinus }>-</button>
首先在redux文件夹下创建一个actions文件夹,新建一个count.js专门用来处理count的修改。这个文件导出两个函数,各自的返回值都是一个键为type的键值:
export function addCount() {
return {
type: "addCount"
}
}
export function minusCount() {
return {
type: "minusCount"
}
}
这是为了配合reducer中写的:
const count = (state=initState, action) => {
switch(action.type) {
case "addCount":
return;
case "minusCount":
return;
default:
return state;
}
}
reducer可以通过type字段来判断你要干什么。
注意
在reducer中,state是只读的,不能在导出的函数中显式修改state的值。
既然不能修改,那就复制一份做处理然后返回:
case "addCount":
let addState = Object.assign({}, state);
addState.count += 1;
return addState;
在视图中,把刚才action中的两个函数塞进事件回调:
handleClickAdd = () => {
this.props.addCount();
}
handleClickMinus = () => {
this.props.minusCount();
}
注意到有一个问题,调用addCount()
之后,它仅返回一个type
键值对,不能操作state。所以,需要一个分配器把函数的返回值分配给能够做操作的有关部门。在后面加上一个mapDispatchToProps
来实现分配:
const mapDispatchToProps = dispatch => {
return {
addCount: () => {
dispatch(addCount());
},
minusCount: () => {
dispatch(minusCount());
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Read)
注意,导出时读取器一定要在分配器之前,是有顺序的。
传参与bindActionCreators
传参
在上述操作中,如果想要直接在回调中传参,比如更改一次性增减的数值:
handleClickAdd = () => {
this.props.addCount(5);
}
handleClickMinus = () => {
this.props.minusCount(10);
}
由于在调用props中的函数后,会先经过分配器,所以参数需要被传入分配器定义的新函数中:
const mapDispatchToProps = dispatch => {
return {
addCount: (num) => {
dispatch(addCount(num));
},
minusCount: (num) => {
dispatch(minusCount(num));
}
}
}
分配器把函数分配到action上,所以在action里也要加上形参:
export function addCount(num) {
return {
type: "addCount",
num
}
}
export function minusCount(num) {
return {
type: "minusCount",
num
}
}
这样一来,action会同时把type和要增加的num一起传给reducer。在reducer中接收这两个值来对state做最后的操作:
const count = (state=initState, action) => {
switch(action.type) {
case "addCount":
let addState = Object.assign({}, state);
addState.count += action.num;
return addState;
case "minusCount":
let minusState = Object.assign({}, state);
minusState.count -= action.num;
return minusState;
default:
return state;
}
}
做到传参的修改,后面就可以进行一系列复杂操作,比如让用户觉定如何修改数据等。
bindActionCreators
import { bindActionCreators } from 'redux'
这东西主要是用来简写action方法的。目前导入action中的方法:
import { addCount, minusCount } from '../redux/actions/count'
如果后期方法很多就不能这样。可以一次性导入:
import * as countActions from '../redux/actions/count'
之后分配器部分就可以写成:
const mapDispatchToProps = dispatch => {
return {
countActions: bindActionCreators(countActions, dispatch)
}
}
在事件回调中调用时用this.props.countActions.方法名()
即可,相当于按名称自动分配。
combineReducers合并reducer
假设在reducers文件夹下有两个reducer,可以新建一个index.js用来合并它们:
├─ 📁reducers
│ ├─ 📄count.js
│ ├─ 📄order.js
│ ├─ 📄index.js
在index.js中引入combineReducers:
import { combineReducers } from "redux";
import count from "./count";
import order from "./order";
const rootReducer = combineReducers({
count,
order
});
export default rootReducer;
后续在store的index中引入时可以直接
import rootReducer from "../reducers";
export default createStore(rootReducer, composeWithDevTools());
注意,由于在导出时把count和order放在了一个对象里,所以后续读取时不能直接用state,而是必须指定state.count或state.order:
const mapStateToProps = state => {
// console.log(state);
return state.count;
}
集成中间件——logger为例
中间件其实就是在redux执行过程中的钩子,你可以拦截当前的结果做些副作用再把它返回去。比如打印日志的logger中间件(写在store的index里):
const logger = store => next => action => {
// 打印执行的是什么操作,参数是什么
console.log('dispach: ', action);
let result = next(action);
// 打印下个状态将是什么
console.log('next state: ', store.getState());
return result;
}
如果想用,先导入加载中间件的工具,然后一起放进开发者工具的参数里:
export default createStore(rootReducer, composeWithDevTools(applyMiddleware(logger)));
这时一个对logger的简单实现,可以先安装原版:
npm install --save redux-logger
使用时也是一样的:
import reduxLogger from 'redux-logger'
export default createStore(rootReducer, composeWithDevTools(applyMiddleware(reduxLogger)));
redux-thunk异步操作
主要用来解决数据异步获得的问题(网络请求)。有些数据需要在ui加载前获得,就应该放在redux中获得,具体是要把这些操作放在action里。安装中间件redux-thunk:
npm install --save redux-thunk
在主入口导入:
import thunk from 'redux-thunk'
// ...
export default createStore(rootReducer, composeWithDevTools(applyMiddleware(reduxLogger, thunk)));
假设要把actions中order.js改成异步的:
export function addOrder(order) {
return {
type: 'addOrder',
order
}
}
export function delOrder(order_id) {
return {
type: 'delOrder',
order_id
}
}
假设order需要通过url来发请求获得,那么异步添加order应该写成:
export function asyncAddOrder(url) {
return dispatch => {
fetch(url).then(res => res.json())
.then(data => {
dispatch(addOrder(data));
})
}
}
个人理解与总结
关于流程
首先说store、action和reducer分别都干了啥。action其实是动作的一种描述,它只传type和payload,表示我要对这些payload做type这样的操作,具体怎么做这个我不管,我把这些传给reducer就行。
reducer收到后负责执行具体的更改,它根据action传过来的东西和目前的state计算并返回新的state。同时它是纯函数,不能修改state只能返回新对象覆盖原来的,这也是它为啥叫reducer而不是modifier什么的。
关于dispatch
它的作用其实只有一个,就是用来触发action。可以看出前面所有的action操作都要被dispath调用。所有状态变更都通过 dispatch,这是为了添加中间件方便。在刚才的异步例子中:
export function asyncAddOrder(url) {
return dispatch => {
fetch(url).then(res => res.json())
.then(data => {
dispatch(addOrder(data));
})
}
}
这段代码是写在action里的,asyncAddOrder(url)
它本身就是一个action。它返回的是一个以dispatch为参数的函数,也就是说外部在调用时实际上调用的是这个dispatch为参数的新函数。第二个dispatch才是执行操作的,它等待异步操作完成后才dispatch需要异步数据的action。
关于中间件
中间件其实就是在redux执行过程中的钩子,让程序员可以在数据流的任意位置插入处理,比如在 action 到达 reducer 前进行预处理或加工、添加日志、错误报告、数据分析等。
它的实现也很简单,主要是通过函数调用链:每个中间件接收下一个中间件的引用,action 像接力棒一样在中间件间传递。在中间件内部,主要需要实现新的dispatch处理逻辑:即我拿到一个action后到底要做什么?直接调用?还是先整点私活?最后的 dispatch 才是真正调用 reducer 的原始版本。