详解Vuex原理

背景

首先我们要明白为什么需要vuex? vuex的出现是为了解决复杂情况下组件数据共享(即全局数据)的问题。将一些数据从组件级别上升到应用级别,而且数据还是 响应式的,即修改了全局数据,页面相应内容被刷新,如果用本地存储的方案数据就不是响应式的。另外vuex还实现了数据的 模块化

说到响应式,是不是会联想到vue? vuex的核心跟vue有很大关系,这个是vuex设计的巧妙之处。

Vuex可以理解为是个全局的数据仓库,存放应用级别的数据,让组件共享数据,数据的修改只能通过mutation, mutation是同步操作,如果我们经过一些异步操作(发送ajax请求),可以用action包装这个过程,在action里面提交mutation进行数据变更。

图解版原理

文字版原理

理解原理我们先从vuex使用开始,流程是:

  1. 先引入Vuex和Vue, 然后Vue.use(Vuex)
  2. 实例化Vuex.store({})获取一个store实例, 传入的对象键名可包括modules/state/getters/mutaions/actions, modules的值是对象,每个键值对的值可以是个也是包含键名state/getters/mutaions/actions的对象
  3. 将这个store实例注入new Vue({})的配置中

下面我们来逐一分析。
对应上面第一步,Vue.use(Vuex),Vuex作为vue的插件,这里执行的是安装过程,use是Vue的一个全局静态方法,会调用插件的install方法,源码中install是通过Vue.mixin方法将一个vuexInit函数混入到所有vue实例的beforeCreate钩子函数,赋值每个vue实例的$router,如果实例化过程中$option传入了router,则将这个router赋值给vue实例的$router属性,否则找vue实例父组件的$option上是否有router。在上面第三步,我们知道我们是将全局的store实例注入给了vue根实例,构建vue组件树是从根实例开始,因此每个子孙组件实际上都是从根实例上拿到全局的store实例。这也就解释为什么我们能在每个组件中通过this.$router去访问同一个对象。

对应上面第二步,实例化Store的过程,执行了Store类的构造函数,主要流程有3步:

  • 构建模块树,实例获得_modules属性,值是树状结果的模块Collection,关键代码this._modules = new ModuleCollection(options)
  • 安装模块, 通过上面获得的模块树,store实例获得_actions, _mutaions, _wrappedGetters属性, 即store通过递归模块树获取到全部模块的actions/mutaion/getters, 而且根模块(_modules.root)上的state获取到了子孙模块的state, 构建了state树, 关键代码installModule(this, state, [], this._modules.root)
  • 设置store的vm, 实例获得_vm属性,关键代码resetStoreVM(this, state),传入的state是上面提到的根模块的上的state树。
    这个store实例的vm属性,是个vue实例,将根模块的state赋值到这个vue实例data上,将根模块的_wrappedGetter赋值到这个vue实例的computed上

    对外暴露通过this.$store.state访问到state, 通过this.$store.getters访问到getter。
    state的访问实际上是设置了store的原型属性state的getter函数,返回了store._vm._data.$$state; getter的访问实际上是返回了定义在这个store._vm上的key对应的值,因为module中的getter函数最后被作为computed属性去实例化这个vue。vue在初始化comoputed的时候会在实例上定义对应的属性,所以能通过vm.a访问到computed{‘a’:function(){}}中a的值

    vue的data和computed本身是响应式对象,所以state也是响应式的。 这就回答了我们最上面提到的原理。

    核心代码见下面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function resetStoreVM (store, state, hot) {
    // bind store public getters
    store.getters = {}
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
    get: () => store._vm[key],
    enumerable: true // for local getters
    })
    })

    // use a Vue instance to store the state tree
    // suppress warnings just in case the user has added
    // some funky global mixins
    const silent = Vue.config.silent
    Vue.config.silent = true
    store._vm = new Vue({
    data: {
    $$state: state
    },
    computed
    })

语法糖

mapState
mapGetters
mapActions
mapMutations

  • 传入命名空间
  • 支持数组和对象映射
  • 对象展开符混入computed(mapState/ mapGetters) & methods(mapActions/ mapMutations)
1
2
3
4
5
6
7
8
9
10
11
export default{
computed: {
...mapState(['count']), // this.count 映射到 this.$store.count
...mapState('a',{'aCount':'count'}) // this.aCount 映射到 this.$store.a.count
...mapState('aCount': state => state.a.count ) // this.aCount 映射到 this.$store.a.count
},
methods: {
...mapMutations(['increase']), // this.increse() 映射到 this.$store.commit('increse')
...mapMutations('a', {'aIncrease': 'increase'}) // // this.aIncrese() 映射到 this.$store.commit('a/increse')
}
}