Redux状态管理修炼手册

266

在使用react进行开发的过程中,Redux作为一种状态管理库对于我们来讲是十分重要的。但是在我对redux的学习中发现,同样作为状态管理的工具,Vuex会显得更加简单好懂。但是redux的一些思想对我们学习是十分有帮助的

1.初识Redux

1.1 为什么要使用Redux

管理状态,管理什么状态?

实际上随着就前端业务的不断发展不断复杂,前端在业务中需要管理的状态也就越来越多了,比如服务器返回的数据、缓存的数据、用户操作产生的数据,UI的状态信息等等,所有需要我们管理的数据,都需要使用一个机制进行管理。

React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:

  • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;
  • React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;

Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理。

注:实际上redux和react是没有关系的,redux只是一个普通的第三方库,它可以用在任何地方,包括我们原生的项目以及vue、angular等

1.2 redux的核心理念

Redux要求我们通过action来更新数据:

所有的数据的变动都需要通过派发(dispeach)action来进行更新;action实际上就是一个JavaScript的一个普通的对象,用来描述当前的type和content;

比如下面就是几个更新数据的action:

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" } }

那么我们如何将state和action结合起来生成一个新的state从而达到修改state的目的呢?

使用reducer函数,reducer函数是一个纯函数。

const reducer = (state = initDataState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, counter: state.counter++ }
    case 'DECREMENT':
      return { ...state, counter: state.counter-- }
    case 'ADD_NUM':
      return { ...state, counter: state.counter+action.num}
    case 'SUB_NUM':
      return { ...state, counter: state.counter-action.num }
    default:
      break;
  }
  return state
}

1.3 redux的三大原则

单一数据源

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

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

单一的数据源可以使整个应用程序的state变得清晰、可维护

State状态是只读的

修改更新state唯一的方式就是action的触发,不能再其他地方通过任何方式来对state进行修改。

  • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;

使用纯函数来执行修改

通过reducer将 旧stateactions联系在一起,并且返回一个新的State:

  • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;
  • 但是所有的reducer都应该是纯函数,不能产生任何的副作用;

2.redux的基本使用

2.1使用流程

安装redux:

yarn add redux

1.导入redux

const redux = require('redux');

2.创建一个对象,作为我们要保存的state

// 初始化数据
const initDataState = {
  counter: 0
}

3.创建reducer函数

const reducer = (state = initDataState, action) => {
  return state
}

4.创建store来储存这个store(创建时必须使用reducer作为参数)

const storeX = redux.createStore(reducer);

注:我们可以通过store.getState来获取当前的state值

5.通过action来修改state,action中通常都会有type属性,也可以携带其他的数据;

const actions1 = { type: 'INCREMENT' }
const actions2 = { type: 'DECREMENT' }
const actions3 = { type: 'ADD_NUM', num: 6 }
const actions4 = { type: 'SUB_NUM', num: 4 }

6.修改reducer中的处理代码(这里一定要记住,reducer是一个纯函数,不需要直接修改state)

const reducer = (state = initDataState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, counter: state.counter++ }
    case 'DECREMENT':
      return { ...state, counter: state.counter-- }
    case 'ADD_NUM':
      return { ...state, counter: state.counter+action.num}
    case 'SUB_NUM':
      return { ...state, counter: state.counter-action.num }
    default:
      break;
  }
  return state
}

7.可以在派发action之前,监听store的变化

storeX.subscribe(() => {
  console.log(storeX.getState());
})

8.派发action

storeX.dispatch(actions1)
storeX.dispatch(actions2)
storeX.dispatch(actions3)
storeX.dispatch(actions4)

2.2 redux目录结构划分

如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护。

接下来,我会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。

我们在实际开发中一般会将redux部分划分为四个文件。 在这里插入图片描述

index.js文件,用于保存我们的store实例:

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

reducer.js文件(存放state和reducer函数)

import { ADD_NUM, DECREMENT, INCREMENT, SUB_NUM } from "./constants.js"

const initialState = {
  counter: 0
}

export default (state = initialState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { ...state, counter: state.counter+1 }
    case DECREMENT:
      return { ...state, counter: state.counter-1 }
    case ADD_NUM:
      return { ...state, counter: state.counter+action.num }
    case SUB_NUM:
      return { ...state, counter: state.counter-action.num  }
    default:
      return state
  }
}

actionCreators.js文件(所有的action操作)

import { ADD_NUM, DECREMENT, INCREMENT, SUB_NUM } from "./constants.js";

let increment = () =>
({
  type: INCREMENT
})

let decrement = () =>
({
  type: DECREMENT
})
let add_num = (num) =>
({
  type: ADD_NUM,
  num
})
let sub_num = (num) =>
({
  type: SUB_NUM,
  num
})
export {
  increment,
  decrement, add_num, sub_num
}

constants.js文件(保存常量,统一处理type的值)

export const INCREMENT='INCREMENT'
export const DECREMENT='DECREMENT'
export const ADD_NUM='ADD_NUM'
export const SUB_NUM='SUB_NUM'

2.3 redux的处理流程

  • 1.全局通常只有一个Store,存储我们的State;
  • 2.Component中在某些情况会派发Action(这些Action是我们提前定义好的);
  • 3.Reducer会接收到这些Action,并且在Reducer中会返回一个新的State,作为Store的State;
  • 4.State发生更新之后会触发通知,告知订阅者数据发生了改变;
  • 5.订阅者拿到最新的数据(在props中),更新到jsx中,界面发生改变;

在这里插入图片描述

3.react结合redux

​ 我们学习redux的最终目标就是在框架中使用它。在这一节我会先使用两种方式来引入这个模块的知识点,一个是在组件中直接使用redux,另一个是我们实现一个简单的connect的功能(利用高阶组件、Context)。其目的是为了是我们更好的理解一个库的使用——react-redux。

3.1 在组件中直接使用

我们可以模拟一个计数器的案例:

在组件中直接使用以下代码:

import React, { PureComponent } from 'react'
import { store } from '../store'
import { sub_num } from '../store/actionCreator'

export default class About extends PureComponent {
  constructor(props) {
    super(props);
    this.state={
      counter:store.getState().counter      
    }
  }
  render() {
    return (
      <div>
        About
        <button onClick={() => {
          this.btnclick(1)
        }}>-1</button>
      </div>
    )
  }
  componentDidMount(){
    store.subscribe(() => {
      this.setState({
        counter:store.getState().counter
      })
    })
  }
  btnclick(num) {
    store.dispatch(sub_num(num))
  }
}

分析以下上面的代码,虽然达到了react与redux结合的目的,但是我们组件中的一些操作可能会出现一些重复,比如,在生命周期钩子中监听state数据变动的代码会出现重复;派发事件前都要先导入store的实例再调其对应的action。那么我们能否将这些公共的部分提取出来呢?

3.2 自定义connect函数

首先我们需要创建一个connect函数

connect函数本身接收两个参数:

  • mapStateToProps:存放目标组件需要使用到的State
  • mapDispatchToProps:存放目标组件需要派发的action

返回一个高阶组件:

  • 在其构造器内通过mapStateToProps(state.getState)初始化所需要的状态
  • 在其componentDidMount生命周期内通过订阅事件的当时对state状态进行实时监测更新
  • componentWillUnmount生命周期函数内对其取消订阅
  • 在render函数内返回传入的组件,并且将所有的state状态以及action事件都通过props传递给该组件
import { PureComponent } from "react"
import StoreContext from "./ContentType";

export default function connect(mapStateChange,dispatchChange) {
  return  function handleMapCpn(WarppedComponnet) {
     class CpnComponent extends PureComponent{
      constructor(props,context) {
        super(props);
        this.state={
          //初始state
          storeState:mapStateChange(context.getState())
        }
      }
      componentDidMount(){
        //订阅
        this.unsubscribe=this.context.subscribe(() => {
          this.setState({
            storeState:mapStateChange(this.context.getState())
          })
        })
      }
      componentWillUnmount(){
        //取消订阅
        this.unsubscribe();
      }
      render(){
        return <WarppedComponnet
        {...this.props}
        {...mapStateChange(this.context.getState())}
        {...dispatchChange(this.context.dispatch)}
        >
        </WarppedComponnet>
      }
    }
    CpnComponent.contextType=StoreContext;
    
    return CpnComponent
  }
} 

接着我们可以看一下使用:

​ 其实通过我们定义的connect函数就可以得知在使用中我们需要分别传入两个存有state和action的函数的映射,其作用是让connect返回我们redux实例中真正的state状态和action,因为现在我们的组件时不会持有store的,这些是connect持有的,这也就是我们封装connect的意义。接着将我们当前的组件作为当前connect返回值的参数传递给内部的高阶组件处理函数。再将其高阶得到的高阶组件导出。

需要注意的是,我们当前就不再使用我们定义的组件,而是使用处理过后的高阶组件,因此我们就可以使用传给我们的props进行获取映射中的数据(也就是我们redux中的state值)及其actions。

栗子:

import React, { PureComponent } from 'react'
import { add_num } from '../store/actionCreator'
import connect from '../utils/connect'

class About extends PureComponent {
  render() {
    return (
      <div>
        <p>{this.props.counter}</p>
        <button onClick={() => { this.props.addNumber(1) }}>+1</button>
        <button onClick={() => { this.props.addNumber(5) }}>+5</button>
      </div>
    )
  }
}
const mapStateToprops = (state) => ({
  counter: state.counter
})
const mapdispeathToprops = (dispatch) => ({
  addNumber(number) {
    console.log('');
    dispatch(add_num(number))
  }
})
export default connect(mapStateToprops, mapdispeathToprops)(About)

这个实例总有一个缺陷,就是需要我们自己导入store实例,那么这就导致我们封装的这个connet函数不够独立,假如别人要使用我们的这个工具类这就意味着别人要修改我们工具类的源码进行导入store。因此我们需要使用context与Provider来解决这个问题。

解决connect函数不够独立的问题

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

1.创建一个Context的文件:

import react from 'react'
let StoreContext=react.createContext();
export default StoreContext

2.修改connect函数中class组件部分的代码:

  • 使用this.context获取store的值,而不是直接导入store(构造器中的使用第二个参数即可)
  • 给我们要返回的类组件设置一个ContextType属性为我们上面创建的StoreContext

3.再组件的入口处将我们的组件根元素设置为StoreContext.Provider,接着再将store使用value导入。

import Home from './useourConnect/Home'
import StoreContext from './utils/ContentType'
export default class App2 extends PureComponent {
  render() {
    return (
      <StoreContext.Provider value={store}>
        <Home></Home>   
        <About></About>
      </StoreContext.Provider>
    )
  }
}

代码实际上就是我们上面的附上的。

4.react-redux使用

​ 虽然我们已经实现了connect、Provider这些帮助我们完成连接redux、react的辅助工具,但是实际上redux官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。

安装:

yarn add react-redux

使用方法实际上就是将我们之前,导入的connect函数替换成我们react-redux下的connect,其余的用法和我们自己封装的用法是一样的。

但是需要注意的是使用Provider这一块有一些区别:

  • 将之前自己创建的Context的Provider,换成react-redux的Provider组件:
  • 注意:这里传入的是store属性,而不是value属性
import React, { PureComponent } from 'react'
import { Provider } from 'react-redux'
import { store } from './store'
import About from './useReactRedux/About'
import Home from './useReactRedux/Home'

export default class App3 extends PureComponent {
  render() {
    return (
      <Provider store={store}>
        <About></About>
        <Home></Home>
      </Provider>
    )
  }
}

看到这里我们可以总结一下使用connect函数的好处:

  • 不用我们再每个页面导入store实例,再去调用action
  • 不用实现生命周期函数来订阅从而更新state的值

5.中间件的使用

5.1.中间件作用和目的

在真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。

在之前学习网络请求的时候我们讲过,网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:

(图片来自coderwhy老师的公众号)在这里插入图片描述

可是这样做有一些不妥:

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

也就是这样的流程: (图片来自coderwhy老师的公众号) 在这里插入图片描述

​ 对于上面的问题就可以使用中间件来进行,首先关于中间件,学习过Express或Koa框架的童鞋对中间件的概念一定不陌生;在这类框架中,Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作;

redux也引入了中间件(Middleware)的概念。

这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;比如日志记录、调用异步接口、添加代码调试功能等等;

5.2 redux-thunk的使用

我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:

官方推荐的做法是网络请求的中间件是redux-thunk.

redux-thunk是如何做到让我们可以发送异步的请求呢?

我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象,redux-thunk可以让dispatch(action函数),action可以是一个函数,该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数

  • dispatch函数用于我们之后再次派发action;
  • getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

1.首先第一步安装:

yarn add redux-thunk

2.在创建store时传入应用了middleware的enhance函数

import * as redux from 'redux'
import reducer from './reducer.js';
import thunkMiddleware from 'redux-thunk'
//通过applyMiddleware来结合多个Middleware, 返回一个enhancer;将enhancer作为第二个参数传入到createStore中;
const enhancer=redux.applyMiddleware(thunkMiddleware);
export const store = redux.createStore(reducer,enhancer);

3.定义返回一个函数的action:

注意:这里不是返回一个对象了,而是一个函数;该函数在dispatch之后会被执行;

let getdata = (data) => {
  return {
    type: GETDATA,
    data
  }
}
const getHomeMultidataAction = () => {
  return (dispatch) => {
    axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
      const data = res.data.data;
      dispatch(getdata(data.banner.list));
    })
  }
}

4.使用:

其实和我们正常使用action的流程是一样的,首先在映射函数中添加对action的引用,接着直接在需要派发action的地方直接派发即可

  componentDidMount() {
    this.props.getHomeMultidata();
  }
  function mapDispatchToProps(dispatch) {
  return {
    getHomeMultidata(){
      dispatch(getHomeMultidataAction())
    }
  };
}

这里有一个疑问,为什么要在action里进行中间件的处理,为什么不直接在reducer中处理请求?

因为reducer是一个纯函数,如果在里面处理网络请求会使其变质

6.redux-devtools

redux提供了类似于vue-devtools的状态追踪工具,不过需要我们在代码中进行相关的设置才可以使用。

import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducer.js';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
const enhancer = composeEnhancers(applyMiddleware(thunkMiddleware,SagaMiddleware));
// 将enhancer作为第二个参数传入到createStore中
const store = createStore(reducer, enhancer);

export default store;

就是要实现是个composeEnhancers函数,如果有中间件直接将applyMiddleware方法作为参数传入即可。

接着我们直接运行项目就直接可以使用这个插件来查看我们数据的状态了。

7.redux-saga

saga是一个类似于thunk的中间件,也是用于处理异步操作的中间件。不过使用的是generator的语法,将异步操作使用同步的方式来写。

如果你对generator的知识不是太了解,可以去看看阮一峰的教程

其实在我看来redux-thuck其实在派发action时返回一个函数,当redux派发事件时会判断它的返回类型,如果是函数则直接调用函数,如果是对象则正常派发到reducer;而saga不一样,saga拥有一个完整的数据环,它可以将action直接拦截到自己的部分然后进行操作。

我们现在来看一下saga的具体使用:

1.安装:

yarn add redux-saga

2.在store/index中集成saga

import * as redux from 'redux'
import reducer from './reducer.js';
import thunkMiddleware from 'redux-thunk'
import createSagaMiddleware from 'redux-saga'
import saga from './saga'
// 创建saga中间件
const SagaMiddleware=createSagaMiddleware();
const store = redux.createStore(reducer,redux.applyMiddleware(thunkMiddleware,SagaMiddleware))
//不要忘记run
SagaMiddleware.run(saga);
export default store;

注意我们saga.js文件是我们即将具体处理拦截action的地方

3.saga.js文件的编写:

  • takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLastest,会执行最新的action)
  • put:在saga中派发action不再是通过dispatch,而是通过put;
  • all:可以在yield的时候put多个action;
import axios from 'axios';
import { put, takeEvery } from 'redux-saga/effects'
import { getdata } from './actionCreator';
import { FETCH_HOME_MUTIDATA } from './constants';

function* fetchHomeMultidata(action) {
  const res = yield axios.get('http://123.207.32.32:8000/home/multidata');
  console.log(res);
  yield put(getdata(res.data.data.banner.list))
}
function* mySaga() {
  yield takeEvery(FETCH_HOME_MUTIDATA, fetchHomeMultidata)
}
export default mySaga;

关于redux的知识点我们讲到这里,你就可以在项目中使用了,需要注意的是,当项目中的state状态过于繁杂时我们需要考虑reducer的拆分,这个点我们后续再来单独讨论。