Redux状态管理

编程2705 字

JavaScript纯函数

在学习Redux之前我们要学习一个前置技能,函数式编程中的纯函数

维基百科定义:

  • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的 外部输出无关。

  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

上面的定义过于晦涩,简单总结:

  1. **确定的输入,一定会产生确定的输出;**不会因为外部变量的改变而影响输出结果。
  2. **函数在执行过程中,不能产生副作用;**副作用一般指函数运行时改变了外部变量,纯函数应该只执行自己的业务逻辑

纯函数示例

// 是否是一个纯函数?
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使用流程

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支持异步方法

异步方法调用流程图:

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优点

  1. 配置更简单
  2. 无需编写action,action将根据我们的reducer函数自动生成
  3. 默认集成redux-devtools,且无需配置
  4. 默认集成redux-thunk中间件,并增加了(等待/成功/失败)三种状态
  5. 放心更改state数据,改变state时无需再拷贝原state⭐️

安装使用

安装:

yarn add @reduxjs/toolkit

**包清理:**Redux Toolkit 已经包含了我们正在使用的几个包,例如 reduxredux-thunkreselect,我们再使用上面的功能时,从@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,这里不做介绍,具体查看官网使用方法

Viceroy
做事果敢有温度,做人温柔有原则
赞赏
OωO
开启隐私评论,您的评论仅作者和评论双方可见