重学React之状态管理细析(redux)

1,313 阅读6分钟

我们知道,当我们的项目逐渐壮大,就需要依赖很多数据,而且数据会在各个组件中用到,所以我们就需要共享这些数据,来做到响应式的更新页面。 中文文档请看这里

Redux核心内容

action

  • Redux要求我们通过action来更新数据。所有数据的变化,必须通过派发(dispatch)action来更新。
  • action是一个普通的JavaScript对象,用来描述这次更新的type和value。
    import {INCREMENT, DECREMENT} from './constants'
    // 返回一个对象{type, value}

    export const increment = num => ({
      type: INCREMENT,
      value: num
    })
    export const decrement = num => ({
      type: DECREMENT,
      value: num
    })

reducer

  • reducer是一个纯函数。将state和action联系在一起。
  • reducer做的事情就是将传入的state和action结合起来生成一个新的state。
    import {INCREMENT, DECREMENT} from './constants'
    export default function reducer(state = {count: 0}, action) {
      switch (action.type) {
        case INCREMENT:
          return {...state, count: state.count + action.value}
        case DECREMENT:
          return {...state, count: state.count - action.value}
        default:
          return state
      }
    }

store

  • 提供可以访问的变量和方法。
    import {createStore} from 'redux';
    import reducer from './reducer'

    const store = createStore(reducer);

    export default store;

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官网提供的一幅图。 image.png

基本案例

下面我们来看一下在项目中如何使用吧。这个案例就是点击按钮,调整数字,然后响应在两个组件中。

文件结构

image.png

代码

  • actionCreators.js: 定义action对象。写成函数,方便扩展。
    import {INCREMENT, DECREMENT} from './constants'
    // 返回一个对象{type, value}

    export const increment = num => ({
      type: INCREMENT,
      value: num
    })
    export const decrement = num => ({
      type: DECREMENT,
      value: num
    })
  • constants.js: 定义action常量,防止多处使用写错。
    export const INCREMENT = "increment"
    export const DECREMENT = "decrement"
  • index.js
    import {createStore} from 'redux';
    import reducer from './reducer'

    const store = createStore(reducer);

    export default store;
  • reducer.js
    import {INCREMENT, DECREMENT} from './constants'
    export default function reducer(state = {count: 0}, action) {
      switch (action.type) {
        case INCREMENT:
          return {...state, count: state.count + action.value}
        case DECREMENT:
          return {...state, count: state.count - action.value}
        default:
          return state
      }
    }
  • 组件一
import React, { PureComponent } from 'react'
import { increment, decrement } from '../store/actionCreators.js'
import store from '../store'
export default class About extends PureComponent {
  constructor(props) {
    super(props)
    this.state = store.getState()
  }

  componentDidMount() {
    store.subscribe(() => {
      this.setState(store.getState())
    })
  }

  decrement(num) {
    store.dispatch(decrement(num))
  }
  increment(num) {
    store.dispatch(increment(num))
  }

  render() {
    return (
      <div>
        <hr />
        <div>about</div>
        <div>{this.state.count}</div>
        <button onClick={(e) => this.decrement(5)}>-5</button>
        <button onClick={(e) => this.increment(5)}>+5</button>
      </div>
    )
  }
}
  • 组件二
import React, { PureComponent } from 'react'
import { increment, decrement } from '../store/actionCreators.js'
import store from '../store'
export default class Home extends PureComponent {
  constructor(props) {
    super(props)
    this.state = store.getState()
  }

  componentDidMount() {
    store.subscribe(() => {
      this.setState(store.getState())
    })
  }

  decrement(num) {
    store.dispatch(decrement(num))
  }
  increment(num) {
    store.dispatch(increment(num))
  }

  render() {
    return (
      <div>
        <div>home</div>
        <div>{this.state.count}</div>
        <button onClick={(e) => this.decrement(5)}>-5</button>
        <button onClick={(e) => this.increment(5)}>+5</button>
      </div>
    )
  }
}

btn2.gif

封装高阶组件

从上面的代码可以看出,有很多重复的代码逻辑,所以我么可以抽离代码,让其变的更简洁。

  • mapStateToProps。高阶组件中,调用该函数并传入state。 其实我们的逻辑还是在组件中书写的,即mapStateToProps的方法体。
  • mapDispatchToProps。高阶组件中,调用该函数并传入store.dispatch。 其实我们的action函数依旧是在组件中写的,只是我们在高阶组件中触发而已。
    import React, { PureComponent } from 'react'
    import store from '../store'

    export default function StoreHOC(mapStateToProps, mapDispatchToProps) {
      return (WrappedComponent) => {
        class HoComponent extends PureComponent {
          constructor(props) {
            super(props)
            this.state = store.getState()
          }

          componentDidMount() {
            store.subscribe(() => {
              this.setState(store.getState())
            })
          }
          render() {
            return (
              <WrappedComponent
                {...this.props}
                {...mapStateToProps(this.state)}
                {...mapDispatchToProps(store.dispatch)}
              />
            )
          }
        }
        return HoComponent
      }
    }

使用

    import React, { PureComponent } from 'react'
    import { increment, decrement } from '../store/actionCreators.js'

    import StoreHOC from './StoreHOC'
    class Home extends PureComponent {


      render() {
        return (
          <div>
            <div>home</div>
            <div>{this.props.count}</div>
            <button onClick={(e) => this.props.decrement(5)}>-5</button>
            <button onClick={(e) => this.props.increment(5)}>+5</button>
          </div>
        )
      }
    }

    const mapStateToProps = (state) => ({
      ...state
    })
    const mapDispatchToProps = (dispatch) => ({
      decrement(num) {
        dispatch(decrement(num))
      },
      increment(num) {
        dispatch(increment(num))
      }
    })

    export default StoreHOC(mapStateToProps, mapDispatchToProps)(Home)

再次优化

但是上面代码的耦合性还是挺高的,必须的引入store。如果你想让别人使用这个高阶组件,那么他们也需要在你的代码中导入store。所以就有了下面的封装。

我们可以通过React.createContext。

    // context.js
    import  {createContext} from 'react';

    const StoreContext = createContext();
    export default StoreContext

高阶组件

  • 就是将store换成this.context
import React, { PureComponent } from 'react'
- // import store from '../store'
import StoreContext from './context.js'

export default function StoreHOC(mapStateToProps, mapDispatchToProps) {
  return (WrappedComponent) => {
    class HoComponent extends PureComponent {
      + static contextType = StoreContext
      + constructor(props, context) {
        super(props, context)
        this.state = context.getState()
      }

      componentDidMount() {
       - // 需要跟新state
       - // store.subscribe(() => {
       - //   this.setState(store.getState())
       - // })
       + this.context.subscribe(() => {
       +   this.setState(this.context.getState())
       + })
      }
      render() {
        return (
          <WrappedComponent
            {...this.props}
            {...mapStateToProps(this.state)}
            + {...mapDispatchToProps(this.context.dispatch)}
          />
        )
      }
    }
    return HoComponent
  }
}

使用


    import StoreContext from './storeTest/context'
    import store from './store'
    ReactDOM.render((
        <div>
          <StoreContext.Provider value={store}>
            <Home></Home>
            <About></About>
          </StoreContext.Provider>
        </div>
    ), document.getElementById("root"))

使用react-redux

但是在工作中,我们都会使用react-redux库。

安装

    npm install react-redux

使用

使用方法和上面我们自己封装的高阶函数一样。只不过我们给Provider组件设置共享数据时,是使用store,而非value。

    import React from 'react'
    import ReactDOM from 'react-dom'
    import './index.css'
    import App from './App'
    import store from './app/store'
    import { Provider } from 'react-redux'

    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    )

redux处理异步

我们只打,当我们做项目时,需要请求数据,所以就需要发送网络请求获取数据。那么如何在redux中处理异步请求呢?

网络请求位置

  • 可以放在组件的生命周期函数中。
  • 可以放在redux中。 但是网络请求到的数据也属于我们状态管理的一部分,更好的应该是将其也交给redux来管理。所以我们就需要使用redux-thunk来完成这些操作。

redux-thunk的介绍

  • 默认情况下的dispatch(action),action需要是一个JavaScript的对象。
  • redux-thunk可以让dispatch(action函数),action可以是一个函数。
    • 该函数会自动被调用,并且会传给这个函数一个dispatch函数和getState函数。
    • dispatch函数用于我们之后再次派发action。
    • getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态。

具体使用

我们需要redux中内置的applyMiddleware来使用redux-thunk。

    import {createStore, applyMiddleware, compose} from 'redux'

    import reducer from './reducer'

    import thunk from 'redux-thunk'
    
    // 这里也使用了redux-devtools工具
    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) : compose

    const enhancer = composeEnhancers(applyMiddleware(thunk))
    const store = createStore(
      reducer,
      enhancer
    )

    export default store

在action操作中发送网络请求。

    export const getBannerList = (dispatch, getState) => {
      axios({
        url: 'http://123.207.32.32:8000/home/multidata'
      }).then((res) => {
      // 二次dispatch
        dispatch({
            type: "getBannerList",
            value: res.data.data.banner.list
          })
      })
    }

组件中的一些逻辑

    componentDidMount() {
    // 调用映射后的action函数。实现一次dispatch。
        this.props.getBannerList()
    }
    const mapDispatchToProps = (dispatch) => ({
      getBannerList() {
        dispatch(getBannerList)
      }
    })
    export default connect(mapStateToProps, mapDispatchToProps)(Home)

简单剖析redux中间件

    function thunk(store) {
      const next = store.dispatch;
      // 把它当做dispatch函数
      function dispatchAndThunk(action) {
        if(typeof action === "function") {
          action(store.dispatch, store.getState)
        }else {
          next(action)
        }
      }
      // 方式一
      // store.dispatch = dispatchAndThunk
      // 方式二
      return dispatchAndThunk
    }


    function applyMiddleware(...middleware) {
      for(let i = 0; i < middleware.length; i++) {
      // 方式一
      // middleware[i](store)
      // 方式二
        store.dispatch = middleware[i](store)
      }
    }
    // 可以传入多个中间件函数
    applyMiddleware(thunk)

拆分reducer

我们发现,由于项目的不断壮大,数据量越来越多,如果将所有的状态都放到一个reducer中进行管理,reducer也会变得非常臃肿。不利于维护。所以我们需要对reducer进行拆分。

如何拆分呢?

学习过vuex的同学的知道,有模块的概念。所以我们也可以将对应的数据分为一个模块,让各自维护自己的reducer, actionCreator, constants文件。然后再在总的reducer中进行合并。 image.png 那么如何在总的reducer合并呢?

这时候我们就需要用到redux提供的combineReducers函数了。 image.png

redux这部分还是比较难理解的,不会像vuex一样拿来即用。所以还需要自己好好理解redux,多多练习。