Redux状态管理
JavaScript纯函数
在学习Redux之前我们要学习一个前置技能,函数式编程中的纯函数
维基百科定义:
此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的 外部输出无关。
该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
上面的定义过于晦涩,简单总结:
- **确定的输入,一定会产生确定的输出;**不会因为外部变量的改变而影响输出结果。
- **函数在执行过程中,不能产生副作用;**副作用一般指函数运行时改变了外部变量,纯函数应该只执行自己的业务逻辑
纯函数示例
// 是否是一个纯函数?
function sum(num1, num2) {
return num1 + num2;
}
// 答:是纯函数,因为两点同时满足
// add函数是否是一个纯函数?
let foo = 10;
function add(num) {
return foo + num;
}
// 答:不是一个纯函数,因为foo会改变,不满足第一条定义。改写为纯函数需要把变量let换为const,保证为常量不可改变
// printInfo是否为纯函数?
function printInfo(info) {
info.name = "code";
console.log(info.name, info.age);
}
const obj = {
name: "why",
age: 18
}
printInfo(info);
console.log(obj);
// 答:不是一个纯函数,因为不满足定义第二条,产生了副作用。
Redux核心理念
store
首先我们需要定义一个状态来管理
const initialState = { count:0 }
action
action是一个普通的JavaScript对象,用来描述这次更新的type和参数;
store状态的变化,必须通过派发(dispatch)action来更新,这样数据才会是可跟追,可预测的。
// action = { type,需传递的参数 } const addAction = {type:'ADD',num:10};
reducer
注意:reducer是一个纯函数,它所做的事情就是将state和action结合,返回新的state。
const reducers = (state = initialState, action) => { switch (action.type) { case ADD: return { ...state, count: state.count + action.num } default: return state } }
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结构及示例
如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护,我们可以把redux拆分成以下文件。
如果是学习的话安装redux即可,但如果是在react项目中使用的话,需要安装react-redux
yarn add redux
代码结构
// src/store/actions.js
export const ADD = 'ADD';
export const SUB = 'SUB';
export const addAction = (num) => ({
type: ADD,
num
})
export const subAction = (num) => ({
type: SUB,
num
})
// src/store/index.js
import {createStore} from 'redux';
import reducer from './reducers';
const store = createStore(reducer);
export default store;
// src/store/reducers.js
import {ADD, SUB} from "./actions";
const initialState = {
count: 0,
name: 'viceroy'
}
const reducers = (state = initialState, action) => {
switch (action.type) {
case ADD:
// 不能影响到外界变量,如果我们不解构state对象,state则会只有{count:x},丢失了state中其他属性,违反了reducer是纯函数的规则
return {
...state,
count: state.count + action.num
}
case SUB:
return {
...state,
count: state.count - action.num
}
default:
return state
}
}
export default reducers;
使用redux:
import store from "./store";
// 监听store变化
store.subscribe(() => {
// 通过getState方法得到当前状态
console.log('store changed', store.getState());
});
// 派发action事件
store.dispatch({type: 'ADD', num: 10});
store.dispatch({type: 'SUB', num: 2});
React中使用Redux
上面学到的redux代码结构中没有告诉我们,在React中如何使用。我们这就来讲解一下
Redux使用流程
安装react-redux
React-redux中即包含connect,上面我们封装的connect函数只是为了让我们懂得其中原理,实际使用中我们直接使用react-redux中的connect即可
安装:yarn add react-redux
页面使用connect
// home.js
import React, {PureComponent} from 'react';
import {connect} from 'react-redux';
import {addAction} from "../../store/actions";
class Home extends PureComponent {
render() {
return (
<div>
<h2>Home</h2>
<p>当前计数:{this.props.count}</p>
<button onClick={() => this.props.addAction(5)}>+5</button>
</div>
);
}
}
const mapStateToProps = state => ({
count: state.count
})
const mapDispatchToProps = dispatch => ({
addAction: num => dispatch(addAction(num))
})
export default connect(mapStateToProps, mapDispatchToProps)(Home);
// index.js
import store from "./store";
import {Provider} from "react-redux";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
connect函数原理
在使用Redux时每个页面中重复代码太多,我们可以封装一个connect函数,利用函数式编程来抽取每个页面中重读的state/dispatch及状态监听,然后再让state/dispatch以props的方式传递给其页面。
// connect.js
import React, {PureComponent} from "react";
import store from "../store";
export function connect(mapStateToProps, mapDispatchToProps) {
return function HOC(Component) {
return class extends PureComponent {
constructor(props) {
super(props);
this.state = {
storeState: mapStateToProps(store.getState()) // 定义页面传递来的state
}
}
componentDidMount() {
// 监听state变化更新页面
this.unsubcribe = store.subscribe(() => {
this.setState({
storeState: mapStateToProps(store.getState())
})
})
}
componentWillUnmount() {
// 卸载监听
this.unsubcribe()
}
render() {
return <Component
{...this.props}
{...this.state.storeState}
{...mapDispatchToProps(store.dispatch)}
/>
}
}
}
}
// home.js
import React, {PureComponent} from 'react';
// import {connect} from 'react-redux';
import {connect} from '../../utils/connect'; // 引用自定义的connect
import {addAction} from "../../store/actions";
// 其他代码不变
redux中异步流程
异步方法我们可以放在组件生命周期中完成,事实上,网络请求的数据也属于状态管理的一部分,更好的方式是交给redux管理。但redux中并不支持异步方法,答案就是使用中间件,让redux支持异步方法
异步方法调用流程图:
使用中间件
**中间件(Middleware)**的概念:在dispatch的action之前,扩展一些自己的代码。一般的功能比如日志记录、调用异步接口、添加代码调试功能等
中间件redux-thunk
默认情况下的dispatch(action),action需要是一个JS对象,redux-thunk可以让dispatch的action使用函数
安装redux-thunk:yarn add redux-thunk
使用redux-thunk
// store/index.js
import {createStore, applyMiddleware} from 'redux';
import reducer from './reducers';
import thunkMiddleware from 'redux-thunk'; // 导入thunk中间件
// 通过applyMiddleware来合并多个中间件, 并作为第二个参数传入到createStore中;
const enhancer = applyMiddleware(thunkMiddleware);
const store = createStore(reducer, enhancer);
export default store;
// store/actions.js
// 作为中间件的函数,会接收dispatch,getState两个参数来供使用
export const getHomeDataAction =(dispatch,getState) => {
console.log(getState());
axios({
url: "http://api/data",
}).then(res=>{
const data = res.data.data;
// 调用对应action方法,保存对应数据
dispatch(changeBannersAction(data.banner.list));
dispatch(changeRecommendsAction(data.recommend.list));
})
}
// hack用法 : 函数中返回函数,使用时传入参数dispatch(getHomeDataAction('参数')),这样即没有打破规则,又传递了参数过来。
export const getHomeDataHackAction = (arg) => (dispatch,getState) => {
console.log('被传入参数', arg);
// 正常写逻辑
}
// home.js
import {getHomeDataAction, getHomeDataHackAction} from './actions';
class Home extends PureComponent {
componentDidMount() {
// 在生命周期中调用action
this.props.getHomeDataAction();
}
}
const mapDispatchToProps = dispatch => ({
// 给action传入函数,无需调用,函数会自动被调用并传入(dispatch,getState)
getHomeDataAction: () => dispatch(getHomeDataAction),
// hack用法
getHomeDataHackAction: () => dispatch(getHomeDataHackAction('参数')),
})
中间件redux-saga
redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活,一般在大型项目中使用,但使用难度也更大,一般redux-thunk也可以满足基本需求
注意⚠️:redux-saga暂不支持最新的Redux Toolkit方式配置redux,建议使用redux-thunk
安装redux-saga:yarn add redux-saga
redux-saga属性:
put:在saga中派发action不再是通过dispatch,而是通过put;
all:可以在yield的时候put多个action;
takeEvery:可以传入多个监听的actionType,每一个都可以被执行
takeEvery(type, 监听函数) // 或者传入数组,监听多个type takeEvery([type1,type2], 监听函数)
takeLatest:使用类似takeEvery,但只执行最后一次
使用redux-saga
// store/index.js
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducers';
import thunkMiddleware from 'redux-thunk';
// 1、导入创建saga中间件函数
import createSagaMiddleware from 'redux-saga';
import rootSaga from './saga';
// 使用redux-devtools工具,并配置支持trace(跟踪代码)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
// 2、创建saga中间件
const sagaMiddleware = createSagaMiddleware();
// 3、合并中间件
const enhancer = applyMiddleware(thunkMiddleware, sagaMiddleware);
// 4、应用redux-devtools配置及中间件
const store = createStore(reducer, composeEnhancers(enhancer));
// 5、导入saga,启动中间件saga监听流程
sagaMiddleware.run(rootSaga);
export default store;
// store/actions.js
export const FETCH_HOME_DATA = 'FETCH_HOME_DATA';
export const fetchHomeDataAction = {
type: FETCH_HOME_DATA
}
// store/saga.js
import {put, all, takeEvery, takeLatest} from "redux-saga/effects";
import {FETCH_HOME_DATA, changeBannersAction, changeRecommendsAction} from "./actions";
import axios from "axios";
function* fetchHomeData() {
const res = yield axios.get('http://xxx/api/data');
const data = res.data.data;
// yield put(changeBannersAction(data.banner.list));
// yield put(changeRecommendsAction(data.recommend.list));
// 使用all,一次性派发多个action,all里面的action无需加yield
yield all([
put(changeBannersAction(data.banner.list)),
put(changeRecommendsAction(data.recommend.list))
])
}
function* rootSaga() {
// 监听多个type,全部执行
// yield takeEvery([FETCH_HOME_DATA,TEST], fetchHomeData);
// 监听多个type,只执行最后一个
// yield takeLatest([FETCH_HOME_DATA,TEST], fetchHomeData);
yield all([
takeLatest(FETCH_HOME_DATA, fetchHomeData),
])
}
export default rootSaga;
// home.js
class Home extends PureComponent {
componentDidMount() {
this.props.fetchHomeData();
}
}
const mapDispatchToProps = dispatch => ({
fetchHomeData: () => dispatch(fetchHomeDataAction); // 传入定义的action对象即可
})
redux-devtools
redux-devtools可以方便的让我们对状态进行跟踪和调试
安装:
- 直接去谷歌浏览器商店搜索Redux DevTools下载即可
- 或者去github中搜索Redux DevTools,查看使用方法
使用:
注意:后面使用Redux Toolkit后默认使用devtools,无需手动配置。
若没有使用,我们还需要在代码中配置才能生效
// store/index.js
import {createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducers';
import thunkMiddleware from 'redux-thunk';
// 使用redux-devtools工具,并配置支持trace(跟踪代码)
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const enhancer = applyMiddleware(thunkMiddleware);
// 合并redux-devtools配置及中间件
const store = createStore(reducer, composeEnhancers(enhancer));
export default store;
中间件简易实现
中间件的目的是在redux中插入一些自己的操作,其实我们也可以自定义一个自己的中间件。
简易实现日志中间件:
这里以打印日志为案例,在dispatch之前获取state,之后获取最新state
const addAction = (num)=> ({type:'ADD', num})
function dispatchAndLog(action){
console.log('before:',store.getState());
store.dispatch(action);
console.log('after:',store.getState());
}
// 使用函数
dispatchAndLog(addAction(10));
// 但这样直接使用省略了原来的store.dispatch,不是我们想要的方法,我们可以进一步改进。
//代码改进👇
function patchLogging(){
// 这里利用hack一点的技术:Monkey Patching
// 利用它可以修改原有的程序逻辑,当我们调用store.dispatch时,实际上调用的是dispatchAndLog函数。
const next = store.dispatch;
function dispatchAndLog(action){
console.log('before:',store.getState());
next(action); // 利用next保存原有dispatch
console.log('after:',store.getState());
}
store.dispatch = dispatchAndLog;
// return dispatchAndLog;
}
// 使用方法
patchLogging()
store.dispatch(addAction(10))
简易实现thunk中间件:
function patchThunk() {
const next = store.dispatch;
function dispatchAndThunk(action) {
if (typeof action === 'function') {
// thunk内部其实就是判断,如果是函数,则传入dispatch和getState
action(store.dispatch, store.getState);
} else {
next(action);
}
}
store.dispatch = dispatchAndThunk;
}
// 使用方法
patchThunk();
store.dispatch(addAction(10))
简易实现applyMiddleware:
上面我们封装好的函数来使用不是特别方便,我们可以封装一个函数来统一合并中间件,模拟applyMiddleware
function applyMiddlewares(store, ...middlewares) {
const copyMiddlewares = [...middlewares];
copyMiddlewares.forEach(middleware => {
middleware()
// store.dispatch = middleware(store); // 也可以中间件中直接返回赋值函数,在这里统一赋值
});
}
applyMiddlewares(store, patchLogging, patchThunk);
reducer文件拆分
当前我们代码基本功能已经实现,但现在代码都拥挤在一个reducer文件中,我们可以根据不同的页面进行拆分
./store
|-index.js // 总配置文件
|--home // home页面结构
| |-- index.js
|--count // count页面结构
| |-- index.js
拆分后的文件示例:
// store/count/index.js
export const ADD = 'ADD';
export const addAction = (num) => ({
type: ADD,
num
})
// 为拆分后的文件设置初始值
const initCountState = {
count: 0,
}
const countReducer = (state = initCountState, action) => {
switch (action.type) {
case ADD:
return {
...state,
count: state.count + action.num
}
default:
return state
}
}
export default countReducer;
总reducers.js文件集合:
// store/index.js
import {countReducer} from "./count";
import {combineReducers} from "redux";
// 合并后state结构被改变,原来的{count: 0} 变成了 {countInfo: {count:0}}
// const reducers = (state = {}, action) => ({
// countInfo: countReducer(state.countInfo, action),
// countInfo1: countReducer(state.countInfo, action)
// })
// redux给我们提供了combineReducers函数,让我们对多个reducer进行合并
const reducers = combineReducers({
countInfo: countReducer
})
export default reducers;
Redux Toolkit
**Redux Toolkit 是编写 Redux 应用程序逻辑的标准方式。**上面我们所介绍的所有概念(actions、reducers、store setup、action creators、thunk 等)仍然存在,但是Redux Toolkit 提供了更简单的方法来编写代码。
Redux Toolkit优点
- 配置更简单
- 无需编写action,action将根据我们的reducer函数自动生成
- 默认集成redux-devtools,且无需配置
- 默认集成redux-thunk中间件,并增加了(等待/成功/失败)三种状态
- 放心更改state数据,改变state时无需再拷贝原state⭐️
安装使用
安装:
yarn add @reduxjs/toolkit
**包清理:**Redux Toolkit 已经包含了我们正在使用的几个包,例如 redux
、redux-thunk
和 reselect
,我们再使用上面的功能时,从@reduxjs/toolkit
导入即可
yarn remove redux redux-thunk reselect
核心API:
configureStore:包装createStore以提供简化的配置选项。它可以自动组合slice reducer,默认使用redux-thunk,默认启用 Redux DevTools
createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,actions。
createAsyncThunk: 接受一个字符串和一个返回函数,并生成一个pending/fulfilled/rejected三种状态
更多API可以查看React官网
使用案例:
// store/index
import {configureStore} from "@reduxjs/toolkit";
import homeReducer from './home';
import countReducer from './count';
// configureStore 替代 createStore
const store = configureStore({
reducer: {
homeInfo: homeReducer,
countInfo: countReducer
}
})
export default store;
// store/home/index.js
import {createSlice, createAsyncThunk} from '@reduxjs/toolkit'
import axios from "axios";
// 使用createAsyncThunk 替代原来的定义方式,并增加了新的extraReducers状态检测
export const getHomeDataThunk = createAsyncThunk('home/getHomeDataThunk', async (arg, thunkAPI) => {
// arg:调用getHomeDataThunk传入的参数,
// thunkAPI:thunk的所有API,getState/dispatch等
const {data} = await axios({
url: "http://xxx/home/multidata",
})
return data.data; // 这里直接返回即可,返回值会在extraReducers中传递给action.payload
})
// 使用createSlice 创建下层reducer,简化并集合逻辑
const homeSlice = createSlice({
name: 'home', // 名字
initialState: { // 初始state
banners: [],
recommends: []
},
reducers: { // 使用对象的方式去定义reducer,无需使用switch
changeBanners(state, action) {
state.banners = action.payload
},
changeRecommends(state, action) {
state.recommends = action.payload
}
},
extraReducers: builder => { // 加载/完成/失败,三种状态
builder
.addCase(getHomeDataThunk.pending, (state, action) => {
console.log('pending加载中')
})
.addCase(getHomeDataThunk.fulfilled, (state, action) => {
state.banners = action.payload.banner.list;
state.recommends = action.payload.recommend.list;
})
.addCase(getHomeDataThunk.rejected, (state, action) => {
console.log('rejected失败')
})
},
// extraReducers另一种写法,计算属性名写法
// extraReducers: {
// [getHomeDataThunk.pending](state, action){
// console.log('pending加载中')
// }
// }
})
// 从homeSlice中获取自动生成的action
export const {changeBanners, changeRecommends} = homeSlice.actions;
// 当然,如果我们不想使用createAsyncThunk,也可以自己定义,还使用原来方式,只不过extraReducers无法检测到,需要自己派发改变数据
// hack方法:利用函数中返回函数,传递arg参数
export const getHomeDataAction = (arg) => (dispatch, getState) => {
axios({
url: "http://xxx/home/multidata",
}).then(res => {
const data = res.data.data;
dispatch(changeBanners(data.banner.list));
dispatch(changeRecommends(data.recommend.list));
})
}
export default homeSlice.reducer;
// home.js
import React, {PureComponent} from 'react';
import {connect} from 'react-redux';
import {addAction, getHomeDataThunk, getHomeDataAction} from "../../store/home";
class Home extends PureComponent {
componentDidMount() {
// 调用中间件
this.props.getHomeDataThunk();
}
}
const mapStateToProps = state => ({
banners: state.home.banners
})
const mapDispatchToProps = dispatch => ({
getHomeDataThunk: () => dispatch(getHomeDataThunk('参数'))
// getHomeDataThunk: () => dispatch(getHomeDataAction('参数'))
})
export default connect(mapStateToProps, mapDispatchToProps)(Home);
reducer多参数传递
默认派发reducer时只支持传递一个参数,如果需要传递多个,除了自己组成对象,slice还为我们提供了准回调函数
const homeSlice = createSlice({
name: 'home',
initialState: {
banners: [],
recommends: []
},
reducers: {
// 自己组成对象
// changeAllData(state, action) {
// console.log(action.payload)
// state.banners = action.payload
// state.recommends = action.payload
// }
// 利用prepare准回调函数,重新返回action.payload
changeAllData:{
reducer(state, action) {
state.banners = action.payload.banners
state.recommends = action.payload.recommends
},
prepare(banners, recommends) {
return {
payload: {
banners,
recommends
}
}
}
}
},
})
// 使用
// dispatch(changeAllData(data));
dispatch(changeAllData(data.banner.list, data.recommend.list));
归一化 State
“归一化” state:items保存在由它的 ID 作为键的对象中,如{[item.id]: item},这使我们能够通过 ID 查找任何的 items,而无需遍历整个数组
使用归一化state,我们需要判断前端项目中是否具有大量数据,需要id来查找对应item,有的话则使用归一化state,这里不做介绍,具体查看官网使用方法