基于Redux的状态管理

391 阅读8分钟

一、什么是Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。源于Facebook的flux架构。是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库 它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。利用React中Context(上下文)在组件之间共享状态的特性实现。

二、核心概念

1. state

整个应用的全局state ,存储在唯一一个store中, store通过createStore函数生成

示例:

//store/index.js
import {createStore} from 'redux';
//引入Reducer
import Reducer from './reducers';
​
const configureStore = (initialState) => {
    const store = createStore(Reducer, initialState);
    return store;
}
export default configureStore({
  addCount: {
    count: 0,
  },
})

2. reducer

reducer定义 state改变的规则, 返回新的statestore,只有在reducer中才能改变state。reducer必须是纯函数,可以有多个,通过combineReducers 函数合成一个根Reducer。

示例:

const initState = {
  count: 0,
};
​
const addCount = (state = initState, action) => {
    switch (action.type) {
        case 'ADD_COUNT':
            return {
              ...state,
              ...action.count,
            };
        default:
            return state;
    }
}
​
​
export default combineReducers({
  addCount
});

3. action

​ 用来描述当前发生的操作,定义操作类型与参数,并传递给Reducer

Action 通过 StoreDispatch 方法传递给 StoreStore 接收到 Action,连同之前的 State 一起传给 Reducer

示例:

const addCount = (count) => {
  return {
      type: 'ADD_COUNT',
      count
  };
};

4. Middleware中间件

在 Redux 中,同步的表现就是:Action 发出以后,Reducer 立即算出 State。那么异步的表现就是:Action 发出以后,过一段时间再执行 Reducer。那怎么才能 Reducer 在异步操作结束后自动执行呢?Redux 引入了中间件 Middleware 的概念。

实际上我们说的中间件指的是对 Dispatch 方法的封装。常用中间件:

  • redux-thunk 中间件 重写store.dispatch,解决异步操作

  • redux-promise 中间件 使得store.dispatch方法可以接受 Promise 对象作为参数

  • redux-logger中间件 日志中间件

示例代码:

//store/index.js
import {createStore, applyMiddleware} from 'redux';
//引入Reducer
import Reducer from './reducers';
//引入中间件
import logger from 'redux-logger';
​
const configureStore = (initialState) => {
    // 通过 applyMiddleware来集成中间件
    const store = createStore(Reducer, initialState, applyMiddleware(logger));
    return store;
}
export default configureStore({
  addCount: {
    count: 0,
  },
})

5. connect

connect是view与redux之间的桥梁。那他们是如何关联的呢,请看示例:

import React, {Component} from 'react';
import {addCount} from './store/actions/index';
import {connect} from 'react-redux';
​
class App extends Component {
    constructor(props) {
        super(props)
    }
​
    addCount = () => {
      this.props.dispatch(addCount({count: this.props.count + 1}));
    }
    
    render() {
      return (
        <div className='App'>
          <div>当前计数:{this.props.count}</div>
          <div className='btn' onClick={this.addCount}>增加</div>
        </div>
      )
    }
}
​
//添加store中的state
const mapStateToProps = (store) => {
    return {
      count: store.addCount.count,
    };
};
//添加dispatch方法
const mapDispatchToProps = (dispatch) => {
    return {
        dispatch,
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(App)

在view中通过redux提供的connect方法将 store中的state与dispatch方法传入view的props中,以下是connect函数的核心代码

export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
  return function wrapWithConnect(WrappedComponent) {
    class Connect extends Component {
      constructor(props, context) {
        // 从祖先Component处获得store
        this.store = props.store || context.store
        this.stateProps = computeStateProps(this.store, props)
        this.dispatchProps = computeDispatchProps(this.store, props)
        this.state = { storeState: null }
        // 对stateProps、dispatchProps、parentProps进行合并
        this.updateState()
      }
      shouldComponentUpdate(nextProps, nextState) {
        // 进行判断,当数据发生改变时,Component重新渲染
        if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
          this.updateState(nextProps)
            return true
          }
        }
        componentDidMount() {
          // 改变Component的state
          this.store.subscribe(() = {
            this.setState({
              storeState: this.store.getState()
            })
          })
        }
        render() {
          // 生成包裹组件Connect
          return (
            <WrappedComponent {...this.nextState} />
          )
        }
      }
      Connect.contextTypes = {
        store: storeShape
      }
      return Connect;
    }
  }

6.数据流

此时我们看一下Redux事件流

redux-middleware流程图.png

总结一下:Store通过connect将state、dispatch传给View, view调用dispatch发送Action,中间件处理action以及异步任务,处理完之后将结果放到action中并发送给Store,Store将action、previousState发送给Reducer来更改对应的State,并将处理之后的state返回给Store,view监听到state的变化之后从而触发Render。

三、技术演进

1. dva

dva 首先是一个基于 reduxredux-saga 的数据流方案,也可以理解为一个轻量级的应用框架。

redux-saga是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

核心API

  • State 全局状态

  • Action

    Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。

    示例:

    dispatch({
      type: 'add',
    });
    
  • dispatch函数

    ​ 一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式

  • Reducer

    与Redux的reducer一样,描述如何改变数据的,必须是”纯函数”【一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数】

    示例:

    // 纯函数
    const foo = (a,b) => a + b;
    const bar = (obj, b) => obj.x + b;
    //以下不是纯函数
    const  a = 1;
    const foo = (b) => a + b;
    const foo = (obj, b) => {
      obj.x = 2;
      return obj.x + b;
    }
    
  • Effect

    ​ Effect被称为副作用,在我们的应用中,最常见的就是异步操作。dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。

    示例:

     effects: {
        *fetch({ payload }, { call, put, select }) {
          let count = yield select(state => state.example.count);
          count ++;
          console.log(count, 'count---')
          yield put({ type: 'save', payload: {count: count}});
        },
      },
    
  • Subscription

    ​ Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

    示例:

    subscriptions: {
        setup({dispatch, history}) {
          history.listen(({pathname}) => {
            if (pathname === '/users) {
              dispatch({
                type: 'users/fetch'
              });
            }
          })
        },
      }
    

2. Vuex

​ Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex借鉴了Redux的思想,并且针对web应用的开发模式和VUE框架做了优化。

核心概念

  • State 单一状态树,包含了全部的应用层级状态

  • Getters 类似于computed,用于从state中派生出一些状态

  • Mutations

    ​ 更改Vuex 的 store 中的状态的唯一方法是提交 mutation。Mutation需遵守Vue的响应规则;Mutation必须是同步函数

  • Actions

    Action 提交的是 mutation,而不是直接变更状态。

    Action 可以包含任意异步操作。

  • Modules

    ​ 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

    ​ 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

    默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

    如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

  • plugins

    Vuex 插件就是一个函数,它接收 store 作为唯一参数:

    插件可用来同步数据源到store,监控state变化,生成state快照等,内置Logger插件

    示例:

    import createLogger from 'vuex/dist/logger'
    ​
    Vue.use(Vuex)
    ​
    const debug = process.env.NODE_ENV !== 'production'
    ​
    export default new Vuex.Store({
      state: {},
      mutations: {},
      actions: {},
      modules: {},
      plugins: debug ? [createLogger()] : []
    })
    

3. Vuex、dva事件流

vuex-dva事件流.png

对比流程图我们发现,Vuex与dva的数据流向是极为相似的,区别只是api的命名以及实现方式不同,Reducer变成了Mutation,effect变成了action。

四、什么情况下应该用状态管理

1. 当遇到如下问题时,建议开始使用 :

  • 你有很多数据随时间而变化
  • 你希望状态有一个唯一确定的来源(single source of truth)
  • 你发现将所有状态放在顶层组件中管理已不可维护

例如: 用户信息、位置信息等,全局唯一且共用的属性

2. 当页面组件嵌套达到3层及以上时

父子组件嵌套达到3层时,数据传递时需要层层处理,容易出现漏洞,维护成本高。

3. 当相同页面会在路由中重复出现时不建议使用或谨慎使用

比如商品详情页,没有状态的隔离,会导致页面数据错乱。

如果逻辑实在复杂着实需要使用状态管理, 在Vuex中推荐使用vuex的模块重用(仅 2.3.0+ 支持)

即使用module的store.registerModulestore.unregisterModule(moduleName) 进行动态注册

这是module的state与vue组件内的data有同样的问题,所以需要使用函数来声明模块的状态


const MyReusableModule = {
  state: () => ({
    foo: 'bar'
  }),
  // mutation、action 和 getter 等等...
}

示例:

// src/store/dynamicModule/dynamicGoods.jsimport store from '../index'
import goods from '../moduleGoods'
​
export default {
  install (key) {
    store.registerModule(key, goods)
  },
  uninstall (key) {
    store.unregisterModule(key)
  }
}
​
// Vue文件
 created () {
   // 用路由名称 + query + 时间戳 创建唯一routerKey进行隔离
    this.routerKey = `${this.$route.name}${this.$route.query.productId || ''}${new Date().getTime()}`
    console.log('created-key', this.routerKey)
    storeGoods.install(this.routerKey)
     this.$store.dispatch(`${this.routerKey}/changeGoodsId`, this.$route.query.productId)
  },
​
computed: {
  getNum () {
    // 通过唯一routerKey获取对应module数据
    return this.$store.state[this.routerKey].num
  },
},

在dva中原理类似,可直接将model中的state改为Map,利用页面路由参数创建唯一Key,作为Map的key来进行存储数据,达到隔离数据的效果。

Vuex使用误区

1.vuex中action里可以直接操作state且会生效

 changeCount ({ commit, state }) {
      state.count++
      // commit('setCount', state.count + 1)
 }

虽然可以生效,但是不建议这么使用,不利于追踪state的改变,state必须在Mutations中进行改变。

参考资料

redux文档

vuex文档

Flux文档

dva文档

react-saga文档

Vue.js技术揭秘