Vuex核心源码解析之手写Vuex

1,463 阅读19分钟

Vuex 核心源码解析.png 本文章主要讲解的是 Vuex底层 是如何实现的,让大家真正的理解、读懂Vuex内部的实现机制,何谈读懂?自己动手将vuex底层手敲出来。首先,使用Vue中的响应式实现Vuex中的state,其次,使用发布订阅来实现 mutations 和 actions,Object.defineProperty实现getters,最后,再深究 modules 实现机制。文章可能有些长,只要你细心阅读,一定会有所收获。
如果小伙伴们觉得对自己学习 vuex 有帮助,小编希望各位大佬能点个小 赞 哦!!!好了,话不多说,正式进入文章主题!!!

Vuex 概念

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
其实说白了 Vuex 就是一个存储状态的容器,每个组件都可以通过 Vuex 获取共享组件的状态,其实就是组件之间的通信过程。Vuex的运作流程如下图所示:

image.png

Vuex 核心原理

  • Vuex 和 Vue-Router 本质都是一样,都是Vue的一个插件,本质是一个对象。
  • Vuex 对象有两个属性,一个是 install方法,一个是 Store类。
  • install方法 的作用就是将 store 实例挂载到每一个组件上。
  • Store这个类中,包含commit、dispatch等方法,在内部通过new Vue来借助vue的响应式来实现Vuex的内部的响应式。 好了,知道 vuex内部是如何进行工作,我们正式来实现 Vuex

install方法

install方法主要作用就是,使每个组件都挂载上$store实例。使得每一个组件都可以通过this.$store获取到 Vuex 里面的状态和方法。

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store/store.js'
Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')

const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
const applyMinx = (Vue)=> {
    // 每个组件都需要混入挂载$store属性
    Vue.mixin({
        beforeCreate:vuexInit
    })
}
// 子组件的创建过程 先父后子
function vuexInit () {
    const options  = this.$options
    // 判断当前组件是否为根组件 为根组件绑定一个$store
    if(options.store) {
        this.$store = options.store 
    } else if (options.parent && options.parent.$store) {
        // 有父组件并且父组件有$store属性
        // 非根组件 从父组件中获取$store 从而绑定到自己组件实例上
        this.$store =  options.parent.$store
    }
}

上面的代码中,使用Vue.mixin混入,每一个组件在创建的过程中都会执行Vue.mixin里面配置的方法,所以每一个组件都会执行一次 vuexInit方法 ,每一个组件都有一个$options属性,里面包含着组件的配置参数,因为我们在挂载store实例 的时候 只往根组件上挂载了,所有只有根组件才有store这个属性,即$options.store就可以作为我们判断的依据,有store属性,则说明是根组件,将store绑定到根组件并声明为$store,如果没有store属性,说明是子组件,判断子组件有父组件并且父组件有$store属性,则为子组件添加上$store属性。

Store 类

import Vue from 'vue'
import Vuex from '../vuex/index.js'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            return state.age
        }
    },
    modules: {},
    actions: {
        changeAge({commit},payload) {
            setTimeout(()=>{
                commit('getAge',payload)
            },1000)
        }
    }
})
import applyMinx from './mixin'
let Vue
class Store {
    constructor(options) {
        // 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
        this.state = options.state
      }
}
// App.vue
<template>
  <div id="app">
      state: {{$store.state.age}}
      <button @click="handle">点击+10</button>
  </div>
</template>

<script>

export default {
  name: 'App',
  methods: {
    handle() {
      this.$store.state.age += 10
      console.log(this.$store.state.age);
    }
  }
}
</script>

上面的修改$store.state数据方式是错误的,修改store状态只能通过mutation修改,文章只是为了方便测试。
测试结果:
1.gif 此时你发现,可以正常的渲染到页面上,虽然这样能够渲染到页面上,但是,此时这个数据不是响应式的,也就是说,当我通过一个按钮点击修改store中的state的值时,此时是不会触发页面的重新渲染、数据更新的,这也是Vuex和全局变量的区别(面试考点)。Vuex是响应式的,而全局变量不是响应式的。既然说到Vuex和全局变量的区别,那么我们就简单说一下两者的区别:

Vuex 和 全局变量 的区别

  • Vuex的状态存储是响应式。当 Vue 组件从store中读取状态的时候,若 store 中的状态发生了变化,那么相应的组件也会相应地得到高效的更新。同时不能直接修改store中的状态,只能通过显示提交(commit)mutation来改变状态。
  • Vuex 由统一的方法修改数据,全局变量可以任意修改。
  • 全局变量多了会造成命名空间污染,vuex不会,同时vuex还解决了跨组件通信的问题。

State

通过分析,我们知道上面的直接将 options.state 赋值给 this.state 的方法是不可取得,所以,我们需要将store中的状态设置为响应式的,这里vuex就很巧妙的利用了vue的响应式原理,将store中的状态存入到vue实例中作为data的一个属性,以此来实现响应式。代码如下:

import applyMinx from './mixin'
let Vue
class Store {
    constructor(options) {
        // 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
        let state = options.state
        this._vm = new Vue({
            // 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
            // 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
            data() {
                return {// $$ 内部状态
                    $$state: state
                }
            },
        })  
    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时数据是响应式的
        return this._vm._data.$$state
    }
    
}

上面代码中,使用了一个类属性访问器,当访问Store类里面的属性的时候,就会自动触发这个类属性访问器方法,相当于做了一层代理,当组件访问 $store.state 属性的时候,其实就是访问了 vue实例中的$$state这个属性,因为,vue内部已经将data中的数据进行了数据劫持,实现了响应式,当访问vue实例对象的时候,此时实例对象数据也会是响应式的。
测试结果:

2.gif 通过测试,我们发现,此时的数据是响应式的了,当store状态发生改变的时候,此时页面也会同时渲染新的值。

Getters

实现了state之后,我们继续来来实现 Vuex 的 getters 方法。说到这里,不知道小伙伴们在使用 vuex 的 getters 的时候有没有注意到,为什么声明的 getters是一个方法,但是当我们使用的时候却直接写属性值就可以获取到数据了?

// 写的是方法
...
getters: {
        getAge(state) {
            return state.age
        }
    },
...
// 获取的时候却是属性
<template>
  <div id="app">
      state: {{$store.state.age}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">点击+10</button>
  </div>
</template>

写的是方法,获取的是属性,此时,你可能会想到的是Object.defineProperty的 get方法 来进行代理,实现方法如下:

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        // 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
        let state = options.state
        // 用存储store里的方法
        this._getters = {}
        // getters 写的方法 获取的时候是属性
        // 遍历options.getters里面的key  getAge是key  函数是属性value   获取方法里面的属性
        Object.keys(options.getters).forEach(key => {
            Object.defineProperty(this.getters,key,{
                get:()=> {
                    return options.getters[key](this.state)
                }
            })
        })
        this._vm = new Vue({
            // 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
            // 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
            data() {
                return {// $$ 内部状态
                    $$state: state
                }
            },
        })
    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
}

上面的代码中。我们在 Store类 上声明一个 _getters对象 来存储 store中的getters方法,遍历store中的getters的key,将其存放到_getters上,然后使用Object.defineProperty来进行代理,当访问getter属性的是,此时就会触发get方法就会自动执行options.getters对应属性的函数。然会将其返回。
测试结果:

3.gif 测试你会发现,getters 可以显示到页面中,同时也实现了 响应式,这是因为在获取getters属性时会触发get,而get执行的函数包含了this.state,所以state实现了响应式,间接的 getters 也实现了响应式。
但是,这里的getters是没有缓存效果的,当依赖的值没有发生变化,此时也会执行 getters 里面的方法的。测试如下:

...
 state: {
        age: 10,
        a: 1
    },
getters: {
        getAge(state) {
            console.log('getAge执行了');
            return state.age
        }
    },
...
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      satte.a:{{$store.state.a}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">点击a+10</button>
  </div>
</template>

<script>

export default {
  name: 'App',
  methods: {
    handle() {
    // 只修改a的值
      this.$store.state.a += 10
    }
  }
}
</script>

4.gif age的值并没有改变,此时getters的getAge方法却一直被执行,这是因为 state的数据发生了变化,会更新页面,并且在页面中一直获取getters的getAge的值,此时会一直触发get方法,重新执行 getAge方法,这不是我们想要看到的效果。所以,我们需要对getters做缓存。那么如何做缓存呢?回顾一些vue的computed中是不是有缓存的效果,vuex就巧妙的使用vue的computed的缓存特性,来解决vuex getters的缓存问题,所以这我们常说的,vuex的getters属性就相当于vue中的computed属性,vuex的state属性就相当于vue中的data,其实vuex内部通过实例一个vue,然后getters是借助vue的computed来实现缓存的,而state是借助vue中的data响应式来实现响应式的。(可能面试会问到,为什么我们经常说vuex的state、getters就是vue中的data和computed,你是如何理解的)

实现 getters 缓存

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        // console.log(options);
        // 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
        let state = options.state
        // Object.create和{}区别:
        // Object.create(null)原型链指向null {}原型链指向Object
        // 将用户定义的getters存储起来
        this.getters = Object.create(null)
        
        //缓存 利用vue的computed属性 计算属性会将自己的属性放到实例上
        const computed = {}
        // getters 写的方法 获取的时候是属性
        // 遍历options.getters里面的key  getAge是key  函数是属性value   获取方法里面的属性
        Object.keys(options.getters).forEach(key => {
            // 缓存 通过vue计算属性实现懒加载
            computed[key] = () => {
                return options.getters[key](this.state)
            }
            Object.defineProperty(this.getters,key,{
                get:()=> {
                    return this._vm[key]
                }
            })
        })
        this._vm = new Vue({
            // 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
            // 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
            data() {
                return {// $$ 内部状态
                    $$state: state
                }
            },
            computed  // 计算属性会将自己的属性放到实例上 this._vm.a其实就是获取计算属性的a
        })        
    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
}
const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}

vue中的 computed属性会将自己身上的属性放到vue实例上,可以直接通过实例.属性获取computed的值。
测试结果:

5.gif 完美,我们实现了getters的缓存效果。

Mutations

完成了getters的功能之后,我们继续来完善mutations的方法,在 vuex 内部通过发布订阅模式,将用户定义的 mutations 和 actions 存储起来,当触发 commit 的时候就找订阅 mutations方法,当触发 dispatch 的时候就找订阅 actions方法。

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        // 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
        let state = options.state
        // Object.create和{}区别:
        // Object.create(null)原型链指向null {}原型链指向Object
        // 将用户定义的getters存储起来
        this.getters = Object.create(null)
        
        //缓存 利用vue的computed属性 计算属性会将自己的属性放到实例上
        const computed = {}
        // getters 写的方法 获取的时候是属性
        // 遍历options.getters里面的key  getAge是key  函数是属性value   获取方法里面的属性
        Object.keys(options.getters).forEach(key => {
            // 缓存 通过vue计算属性实现懒加载
            computed[key] = () => {
                return options.getters[key](this.state)
            }
            Object.defineProperty(this.getters,key,{
                get:()=> {
                    return this._vm[key]
                }
            })
        })
        this._vm = new Vue({
            // 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
            // 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
            data() {
                return {// $$ 内部状态
                    $$state: state
                }
            },
            computed  // 计算属性会将自己的属性放到实例上 this._vm.a其实就是获取计算属性的a
        })
        // 发布订阅模式 将用户定义的mutations和actions存储起来 当调用commit就找订阅mutations方法 
        // 调用dispatch就找订阅actions方法

        // mutations方法
        this._mutations = Object.create(null)
        Object.keys(options.mutations).forEach(key => {
            // 发布订阅模式
            this._mutations[key] = (playload) => {
                // 第一个参数指向state 
                // call 确保this指向永远都是当前store实例
                options.mutations[key].call(this,this.state,playload)
            }
        })
    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
    commit = (type,playload) => {
        // 触发commit会触发_mutations里面的方法
        this._mutations[type](playload)
    }
}
const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
// APP.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      satte.a:{{$store.state.a}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">点击a+10</button>
      <button @click="$store.commit('changeAge',5)">mustation方法</button>
  </div>
</template>
...
mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
...

6.gif

Actions

actions方法 和 mutations方法 是一样的思路,都是通过 发布订阅 将其存储起来,触发 dispatch的时候再找订阅的 actions方法。

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        // 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
        let state = options.state
        // Object.create和{}区别:
        // Object.create(null)原型链指向null {}原型链指向Object
        // 将用户定义的getters存储起来
        this.getters = Object.create(null)
        
        //缓存 利用vue的computed属性 计算属性会将自己的属性放到实例上
        const computed = {}
        // getters 写的方法 获取的时候是属性
        // 遍历options.getters里面的key  getAge是key  函数是属性value   获取方法里面的属性
        Object.keys(options.getters).forEach(key => {
            // 缓存 通过vue计算属性实现懒加载
            computed[key] = () => {
                return options.getters[key](this.state)
            }
            Object.defineProperty(this.getters,key,{
                get:()=> {
                    return this._vm[key]
                }
            })
        })
        this._vm = new Vue({
            // 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
            // 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
            data() {
                return {// $$ 内部状态
                    $$state: state
                }
            },
            computed  // 计算属性会将自己的属性放到实例上 this._vm.a其实就是获取计算属性的a
        })
        // 发布订阅模式 将用户定义的mutations和actions存储起来 当调用commit就找订阅mutations方法 
        // 调用dispatch就找订阅actions方法

        // mutations方法
        this._mutations = Object.create(null)
        Object.keys(options.mutations).forEach(key => {
            // 发布订阅模式
            this._mutations[key] = (playload) => {
                // 第一个参数指向state 
                // call 确保this指向永远都是当前store实例
                options.mutations[key].call(this,this.state,playload)
            }
        })
        
        // actions方法
        this._actions = Object.create(null)
        Object.keys(options.actions).forEach(key => {
            this._actions[key] = (playload) => {
                //第一个参数为this 因为这里我们经常解构出{commit}
                options.actions[key].call(this, this, playload)
            }
        })
    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
    commit = (type,playload) => {
        // 触发commit会触发_mutations里面的方法
        this._mutations[type](playload)
    }
    dispatch = (type,playload) => {
        this._actions[type](playload)
    }
}
const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
//App.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      satte.a:{{$store.state.a}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">点击a+10</button>
      <button @click="$store.commit('changeAge',5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>
...
actions: {
        changeAge({commit},playload) {
            setTimeout(()=>{
                commit('getAge',playload)
            },1000)
        }
    }
...

7.gif 我们看到,actions 的变化在1s之后就执行了,这跟我们在 store 中写的定时器执行效果符合,此时就完成了 actions 功能的实现。
此时,vuex 的主要实现机制就完成了,但是我们还没有实现 modules 的功能,但是 vuex 主要的逻辑已经跑通了。已经将 vuex 理解的70%了,下面我们继续来探究 modules 是如何是实现的

modules基本用法

在手写 modules 之前,我们先来了解一下在 vuex 中 modules 的基本用法。

import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,
        a: 1
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge执行了');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(()=>{
                commit('getAge',playload)
            },1000)
        }
    },
    modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('b中的c更新了')
                }
            }
        },
        d: {
            state: {
                e:100
            },
            mutations: {
                changeAge() {
                    console.log('d中的e更新了')
                }
            }
        }
    }
})

在modules中添加,两个子模块,b模板和d模块,并且里面的 mutations方法 是和 根模块的mutations 重名的,看一下测试效果:

8.gif 此时你会发现,默认情况下,当mutations重名的时候,模块之间没有作用域的,此时函数并不会覆盖,而是会依次将函数存放到一个数组里。当触发commit的时候会依次执行数组里面的函数。

此时,向d模块中又添加一个子模块e,你会发现此时的模块名字与d模块中的state下的e重名了。测试结果:

// APP.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      getters: {{$store.getters.getAge}}
      b模块的c {{$store.state.b.c}}
      d模块的e {{$store.state.d.e}}
      <button @click="handle">点击a+10</button>
      <button @click="$store.commit('changeAge',5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge执行了');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(()=>{
                commit('getAge',playload)
            },1000)
        }
    },
    modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('b中的c更新了')
                }
            }
        },
        d: {
            state: {
                e:300
            },
            mutations: {
                changeAge() {
                    console.log('d中的e更新了')
                }
            },
            modules: {
                e: {
                    state: {
                        g:500
                    },
                    mutations: {}
                }
            }
        }
    }
})

image.png 此时发现,当模块名和状态名重名的时候,$store.state.d.e获取的不是d模块状态的e,而是获取的是d模下面的子模块状态。所以,建议模块名不要与状态名相同。

向d模块添加一个getters属性,此时你会发现

import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge执行了');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(()=>{
                commit('getAge',playload)
            },1000)
        }
    },
    modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('b中的c更新了')
                }
            }
        },
        d: {
            state: {
                e:300
            },
            // 新增代码
            getters: {
                getD(state) {
                    return state.e += 50
                }
            },
            mutations: {
                changeAge() {
                    console.log('d中的e更新了')
                }
            },
            modules: {
            }
        }
    }
})
// APP.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      getters: {{$store.getters.getAge}}
      b模块的c {{$store.state.b.c}}
      d模块的e {{$store.state.d.e}}
      getD: {{$store.getters.d.getD}}
      <button @click="handle">点击a+10</button>
      <button @click="$store.commit('changeAge',5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>

image.png 如果你直接通过模块.属性获取 getters 的值是无法获取的。我们前面说过,computed计算属性会将自己的属性放到实例上,而且 computed 和 getters 的是一样的,也就是说,getters的属性也会直接将自己的属性直接挂载到实例上,所以,默认情况下我们是可以直接通过getters.属性获取getters的值,而不需要写模块名。
上面我写法中,我们都没有加入namespaced命名空间属性,下面我们加入namespaced看一下代码会发生怎样的变化。

import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge执行了');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(()=>{
                commit('changeAge',playload)
            },1000)
        }
    },
    modules: {
        b: {
            // 新增代码
            namespaced: true,
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('b中的c更新了')
                }
            }
        },
        d: {
            state: {
                e:300
            },
            getters: {
                getD(state) {
                    return state.e += 50
                }
            },
            mutations: {
                changeAge() {
                    console.log('d中的e更新了')
                }
            },
            modules: {
            }
        }
    }
})

使用规则:模块名/状态或方法或getters

// App.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      getters: {{$store.getters.getAge}}
      b模块的c {{$store.state.b.c}}
      d模块的e {{$store.state.d.e}}
      getD: {{$store.getters.getD}}
      <button @click="handle">点击a+10</button>
      <button @click="$store.commit('b/changeAge',5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>

9.gif 跟之前全部都会执行的结果完成不同,当前只会执行有namespaced:true模块的mutations方法,你会发现,当为模块添加了 namespaced 为ture 之后,此时,会将模块的getters、state、mutations、actions都封装到当中作用域下。

那么,此时我又向子模块的子模块添加命名空间,比如:我在d模块下的f模块添加了命名空间,获取值的时候是通过 d/f/changeAge获取吗?我们来测试一下看看效果:

import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge执行了');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(()=>{
                commit('changeAge',playload)
            },1000)
        }
    },
    modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('b中的c更新了')
                }
            }
        },
        d: {
            state: {
                e:300
            },
            getters: {
                getD(state) {
                    return state.e += 50
                }
            },
            mutations: {
                changeAge() {
                    console.log('d中的e更新了')
                }
            },
            modules: {
                // 新增代码
                f: {
                    namespaced: true,
                    state: {
                        g:700
                    },
                    mutations: {
                        changeAge() {
                            console.log('d下f中的g更新了')
                        }
                    }
                }
            }
        }
    }
})
//App.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      getters: {{$store.getters.getAge}}
      b模块的c {{$store.state.b.c}}
      d模块的e {{$store.state.d.e}}
      getD: {{$store.getters.getD}}
      <button @click="handle">点击a+10</button>
      <button @click="$store.commit('d/f/changeAge',5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>

image.png 测试发现提示找不到该方法,通过d/f/changeAge是无法获取到mutation的,此时有namespaced的子模块会往上一层父级查找,看看是否有namespaced属性,如果没有就以当前模块为一个作用域,如果有就嵌套,所以,可以直接通过 f/changeAge 的方法获取 mutations 里的方法。修改之后测试结果:

10.gif

此时去除f模块的namepaced,为d模块添加namespace,

import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge执行了');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(()=>{
                commit('changeAge',playload)
            },1000)
        }
    },
    modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('b中的c更新了')
                }
            }
        },
        d: {
            namespaced: true,
            state: {
                e:300
            },
            getters: {
                getD(state) {
                    return state.e += 50
                }
            },
            mutations: {
                changeAge() {
                    console.log('d中的e更新了')
                }
            },
            modules: {
                f: {
                    state: {
                        g:700
                    },
                    mutations: {
                        changeAge() {
                            console.log('d下f中的g更新了')
                        }
                    }
                }
            }
        }
    }
})
//App.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      getters: {{$store.getters.getAge}}
      b模块的c {{$store.state.b.c}}
      d模块的e {{$store.state.d.e}}
      getD: {{$store.getters.getD}}
      <button @click="handle">点击a+10</button>
      <button @click="$store.commit('d/changeAge',5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>

11.gif 此时,通过 d/changeAge触发,你会发现,执行了d的e更新了,也执行了d下的f模块g也更新了,其实这也不难理解,当模块有 namespaced 的时候,此时会为当前模块封装一个作用域,在d模块下的子模块如果没有 namespaced属性 ,则剩下的子模块全部都归属当前声明 namespaced 模块管理。

上面说了modules的基本用法,大概总结如下:

  • 默认情况下模块,没有作用域问题。
  • 状态名不要和模块名相同。
  • 默认情况下计算属性,直接可以通过getters取值。
  • 如果增加namespaced:true 会将这个模块的属性,都封装到这个作用域下。
  • 默认会找当前模块上是否有 namespaced,再往上查找父级是否有namespaced属性,如果有则一同算上,封装成一个作用域,没有则以当前模块为一个作用域。 好了,知道 modules 的基本用法之后,我们现在正式手写 modules

Modules

在上面的 state、getters、mutations、actions中,我们只做成了根模块,并没有考虑到 modules,所以,之前写的代码中是不能嵌套递归的,我们重新写一个Store类,但是前面的工作我们也不是白费的,有了前面的知识铺垫,我们知道了内部机制之后,我们就可以很轻松的写出一个完整的Store类,也可以很容易的理解 vuex 内部核心。

moduleCollection类

这个类主要用来收集模块,然后将参数模块格式化为模块嵌套的树形结构,方便我们后续的操作。

//根模块
this.root = {   // 模块的配置 包含当前模块的getters、mutations、actions、state
            _raw:xxx,
            // 子模块
            _children: {
                a: {
                    _raw: xxx,
                    _children: {}
                    state: xxx.state
                },
                b: {
                    _raw: xxx,
                    _children: {},
                    state: xxx.state
                }
            },
            state: xxx.state
}

好了知道将用户传入的参数格式化成以上的树形结构之后,现在着手来现实 moduleCollection 这个类。

import forEach from "./utils";

export default class ModlueCollection {
    constructor(options) {
        // 注册模块 []表示路径 递归注册模块
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = {
            _raw: rootModule,
            _children: {},
            state: rootModule.state
        }
        // 判断如果数组长度为了,则为根模块
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
            let parent = path.slice(0,-1).reduce((pre, next) => {
                // 逐级找父节点
                return pre._children[next]
            },this.root)
            parent._children[path[path.length-1]]  = newModlue
        }
        // 注册子模块
        if(rootModule.modules) {
            forEach(rootModule.modules,(moduleValue, moduleName)=>{
                // 递归组成子模块 将模块名字和模块值传入
                this.register([...path,moduleName],moduleValue)
            })
        }

    }
}

image.png 通过测试,我们将传入的参数整理成了嵌套的树形结构。上面的代码还是有点冗余,下面我们将代码提取出来每一个modules创建一个类。

// modules.js
export default class module {
    constructor(rootModule) {
        this._raw = rootModule,
        this._children = {}
        this.state = rootModule.state
    }
    // 找模块的子模块
    getChild(key) {
        return this._children[key]
    }
    // 向模块追加子模块
    addChild(key, module) {
        this._children[key] = module
    }

}
// module-collection.js
import forEach from "./utils";
import Module from './module.js'
export default class ModlueCollection {
    constructor(options) {
        // 注册模块 []表示路径 递归注册模块
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = new Module(rootModule)
        // {
        //     _raw: rootModule,
        //     _children: {}
        //     state: rootModule.state
        // }
        // 判断如果数组长度为了,则为根模块
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
            let parent = path.slice(0,-1).reduce((pre, next) => {
                // 逐级找父节点
                // return pre._children[next]
                return pre.getChild(next)
            },this.root)
            // parent._children[path[path.length-1]]  = newModlue
            parent.addChild(path[path.length-1], newModlue)  
        }
        // 注册子模块
        if(rootModule.modules) {
            forEach(rootModule.modules,(moduleValue, moduleName)=>{
                // 递归组成子模块 将模块名字和模块值传入
                this.register([...path,moduleName],moduleValue)
            })
        }

    }
}

抽离了代码之后,效果依旧不变,但是看到思路就比较清晰了,比如 getchild 就是找子模块,addChild 就是向子模块追加模块,现在我们在每个模块的 Module 中添加每个模块的方法了。 上面将用户传入的参数递归变成树形结构之后,此时,又回到了前面我们所写的Store类中,我们需要在Store上注册所有模块的 state、mutations、actions、getters方法。

自定义 forEachValue 方法

/**
 * 
 * @param {*} obj 传入的对象 
 * @param {*} callback 回调函数 两个参数分别对应解构出对象的value 和 key
 */
const forEachValue = (obj = {}, callback) => {
    Object.keys(obj).forEach(key => {
        // 第一个参数是值,第二个参数是键
        callback(obj[key],key)
    })
}
export default forEachValue

封装这个方法的目的就是为了方便我们获取对象的键值对。

installModlue

installModlue方法 的作用:将创建的树形结构上的状态、方法安装到Store实例上,才可以通过$store方式获取到对应的数据。

// module.js
import forEachValue from "./utils"

export default class module {
    constructor(rootModule) {
        this._raw = rootModule,
        this._children = {}
        this.state = rootModule.state
    }
    // 找模块的子模块
    getChild(key) {
        return this._children[key]
    }
    // 向模块追加子模块
    addChild(key, module) {
        this._children[key] = module
    }
    // 遍历当前模块的mutations
    forEachMutations(fn) {
        if(this._raw.mutations) {
            forEachValue(this._raw.mutations,fn)
        }
    }
    // 遍历当前模块的actions
    forEachActions(fn) {
        if(this._raw.actions) {
            forEachValue(this._raw.actions,fn)
        }
    }
    // 遍历当前模块的getters
    forEachGetters(fn) {
        if(this._raw.getters) {
            forEachValue(this._raw.getters,fn)
        }
    }
    // 遍历当前模块的child
    forEachChild(fn) {
        forEachValue(this._children,fn)
    }

}

安装所有模块的目的就是将当前模块的方法、状态都安装到当前的 Store实例身上,前面我们已经声明了一个 module类 来创建每一个子模块,刚好,我们就可以在这个类中使用 自定义forEachValue 遍历当前类的所有 mutations、actions、getters、还有子模块,将参数作为 forEachValue函数 的callback,方便我们在Store类通过传入的函数中进行安装。

重写 Store 类

import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
let Vue
function installModule(store, rootState, path, module) {
    // 收集所有模块的状态
    if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
        let parent = path.slice(0,-1).reduce((pre, next) => {
            return pre[next]
        }, rootState)
        // 将属性设置为响应式 可以新增属性
        // 如果本身对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
        Vue.set(parent,path[path.length -1 ],module.state)
    }
    module.forEachMutations((mutations, type)=>{
        // 收集所有模块的mutations 存放到 实例的store._mutations上
        // 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
        store._mutations[type] = (store._mutations[type] || [])
        store._mutations[type].push((payload) => {
            // 函数包装 包装传参是灵活的
            // 使this 永远指向实例 当前模块状态 入参数
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type)=>{
        store._actions[type] = (store._actions[type] || [])
        store._actions[type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key)=>{
        // 同名计算属性会覆盖 所以不用存储
        store._wrappedGetters[key] = () => {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key)=>{
        installModule(store, rootState, path.concat(key), child)
    })
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // 存放所有模块的mutation
        this._actions = Object.create(null)     // 存放所有模块的actions
        this._wrappedGetters = Object.create(null)  // 存放所有模块的getters
        // 安装所有模块到Store实例上
        let state = this._modules.root.state
        // this当前实例、根状态、路径、根模块
        installModule(this, state, [], this._modules.root)

    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
    commit = (type,playload) => {
        // 触发commit会触发_mutations里面的方法
        this._mutations[type](playload)
    }
    // 
    dispatch = (type,playload) => {
        this._actions[type](playload)
    }
}
const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}

image.png 经过上面的分析,我们已经树形结构的全部方法和状态都安装到了Store实例身上了,下面我们为数据增加响应式的效果。

resetStoreVm

resetStoreVm方法 作用:将状态放到Vue实例身上,以此来现实响应式,实现getters的缓存。

import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
    // 收集所有模块的状态
    if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
        let parent = path.slice(0,-1).reduce((pre, next) => {
            return pre[next]
        }, rootState)
        // 将属性设置为响应式 可以新增属性
        // 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
        Vue.set(parent,path[path.length -1 ],module.state)
    }
    module.forEachMutations((mutations, type)=>{
        // 收集所有模块的mutations 存放到 实例的store._mutations上
        // 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
        store._mutations[type] = (store._mutations[type] || [])
        store._mutations[type].push((payload) => {
            // 函数包装 包装传参是灵活的
            // 使this 永远指向实例 当前模块状态 入参数
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type)=>{
        store._actions[type] = (store._actions[type] || [])
        store._actions[type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key)=>{
        // 同名计算属性会覆盖 所以不用存储
        store._wrappedGetters[key] = () => {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key)=>{
        installModule(store, rootState, path.concat(key), child)
    })
}
function resetStoreVm (store, state) {
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    store.getters = Object.create(null)
    // 通过使用vue的computed实现缓存 懒加载
    forEachValue(wrappedGetters, (fn, key) => {
        computed[key] = ()=> {
            return fn()
        }
        // 代理
        Object.defineProperty(store.getters, key, {
            get: ()=> store._vm[key]
        })
    })
    // 将状态实现响应式
    store._vm = new Vue({
        data() {
            return {
                $$state: state
            }
        },
        computed
    })
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // 存放所有模块的mutation
        this._actions = Object.create(null)     // 存放所有模块的actions
        this._wrappedGetters = Object.create(null)  // 存放所有模块的getters
        // 注册所有模块到Store实例上 
        // this当前实例、根状态、路径、根模块
        let state = this._modules.root.state
        installModule(this, state, [], this._modules.root)
        
        //实现状态响应式
        resetStoreVm(this,state)

    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
    commit = (type,payload) => {
        // 触发commit会触发_mutations里面的方法
        this._mutations[type].forEach(fn => fn(payload))
    }
    // 
    dispatch = (type,payload) => {
        this._actions[type].forEach(fn => fn(payload))
    }
}
const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}

将状态放到vue实例身上之后,我们来看一下代码运行的结果: 12.gif 运行效果跟一开始的情况一模一样,虽然实现了 modules 的嵌套,但是,还没有实现 namespaced命名空间的功能。下面继来完善modules的namespaced的模块。

namespaced

import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
    constructor(options) {
        // 注册模块 []表示路径 递归注册模块
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = new Module(rootModule)
        // {
        //     _raw: rootModule,
        //     _children: {}
        //     state: rootModule.state
        // }
        // 判断如果数组长度为了,则为根模块
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
            let parent = path.slice(0,-1).reduce((pre, next) => {
                // 逐级找父节点
                // return pre._children[next]
                return pre.getChild(next)
            },this.root)
            // parent._children[path[path.length-1]]  = newModlue
            parent.addChild(path[path.length-1], newModlue)  
        }
        // 注册子模块
        if(rootModule.modules) {
            forEachValue(rootModule.modules,(moduleValue, moduleName)=>{
                // 递归组成子模块 将模块名字和模块值传入
                this.register([...path,moduleName],moduleValue)
            })
        }
    }
    // 新增代码
    getNamespaced(path) { //获取命名空间
        let root = this.root
        return path.reduce((pre, next)=>{
            //[b,c]
            // 获取子模块 查看是否有namespaced属性
            root = root.getChild(next)
            // 有namespace属性就拼接上
            return pre + (root.namespaced ? next + '/' :'')
        }, '')
    }
}
function installModule(store, rootState, path, module) {
    // 新增代码 获取命名空间
    let namespaced  = store._modules.getNamespaced(path)
    console.log(namespaced);
    // 收集所有模块的状态
    if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
        let parent = path.slice(0,-1).reduce((pre, next) => {
            return pre[next]
        }, rootState)
        // 将属性设置为响应式 可以新增属性
        // 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
        Vue.set(parent,path[path.length -1 ],module.state)
    }
    module.forEachMutations((mutations, type)=>{
        // 收集所有模块的mutations 存放到 实例的store._mutations上
        // 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) => {
            // 函数包装 包装传参是灵活的
            // 使this 永远指向实例 当前模块状态 入参数
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type)=>{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key)=>{
        // 同名计算属性会覆盖 所以不用存储
        store._wrappedGetters[key] = () => {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key)=>{
        installModule(store, rootState, path.concat(key), child)
    })
}
import forEachValue from "./utils"

export default class module {
    constructor(rootModule) {
        this._raw = rootModule,
        this._children = {}
        this.state = rootModule.state
    }
    // 新增代码 属性访问器
    get namespaced () {
        return this._raw.namespaced
    }
    // 找模块的子模块
    getChild(key) {
        return this._children[key]
    }
    // 向模块追加子模块
    addChild(key, module) {
        this._children[key] = module
    }
    // 遍历当前模块的mutations
    forEachMutations(fn) {
        if(this._raw.mutations) {
            forEachValue(this._raw.mutations,fn)
        }
    }
    // 遍历当前模块的actions
    forEachActions(fn) {
        if(this._raw.actions) {
            forEachValue(this._raw.actions,fn)
        }
    }
    // 遍历当前模块的getters
    forEachGetters(fn) {
        if(this._raw.getters) {
            forEachValue(this._raw.getters,fn)
        }
    }
    // 遍历当前模块的child
    forEachChild(fn) {
        forEachValue(this._children,fn)
    }

}

在实现,namespaced的时候,就是在模块安装的时候遍历每个模块是否有namespaced属性,然后再将路径拼接起来。 image.png

registerModule

Vuex 和 Vue-Router一样都支持动态的添加模块和路由,所以,我们需要在store中添加动态添加模块的 registerModule 方法。

// module-collection.js
import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
    constructor(options) {
        // 注册模块 []表示路径 递归注册模块
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = new Module(rootModule)
        // {
        //     _raw: rootModule,
        //     _children: {}
        //     state: rootModule.state
        // }
        // 判断如果数组长度为了,则为根模块
        //映射当前module实例 为了动态添加模块
        rootModule.rawModule = newModlue
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
            let parent = path.slice(0,-1).reduce((pre, next) => {
                // 逐级找父节点
                // return pre._children[next]
                return pre.getChild(next)
            },this.root)
            // parent._children[path[path.length-1]]  = newModlue
            parent.addChild(path[path.length-1], newModlue)  
        }
        // 注册子模块
        if(rootModule.modules) {
            forEachValue(rootModule.modules,(moduleValue, moduleName)=>{
                // 递归组成子模块 将模块名字和模块值传入
                this.register([...path,moduleName],moduleValue)
            })
        }
    }
 ...
}

新增 rootModule.rawModule = newModlue 原因:在new Modlue模块的之后,我们又为当前模块实例添加了一个属性映射到自己身上,这是为了在动态注册模块时,我们需要将注册好的模块安装到 Store实例 身上,安装需要 Module 实例中的遍历 mutations 、 actions 、 getters 的方法,但是添加的模块中并没有 Module实例 ,所以需要在注册模块的时候为新增模块添加一个属性,映射 到当前模块实例。

// store.js
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
    // 获取命名空间
    let namespaced  = store._modules.getNamespaced(path)
    console.log(namespaced);
    // 收集所有模块的状态
    if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
        let parent = path.slice(0,-1).reduce((pre, next) => {
            return pre[next]
        }, rootState)
        // 将属性设置为响应式 可以新增属性
        // 如果本身对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
        Vue.set(parent,path[path.length -1 ],module.state)
    }
    module.forEachMutations((mutations, type)=>{
        // 收集所有模块的mutations 存放到 实例的store._mutations上
        // 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) => {
            // 函数包装 包装传参是灵活的
            // 使this 永远指向实例 当前模块状态 入参数
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type)=>{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key)=>{
        // 同名计算属性会覆盖 所以不用存储
        store._wrappedGetters[key] = () => {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key)=>{
        installModule(store, rootState, path.concat(key), child)
    })
}
function resetStoreVm (store, state) {
    // 存储上一个实例
    let oldVm = state._vm
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    store.getters = Object.create(null)

    forEachValue(wrappedGetters, (fn, key) => {
        computed[key] = ()=> {
            return fn()
        }
        // 代理
        Object.defineProperty(store.getters, key, {
            get: ()=> store._vm[key]
        })
    })
    // 将状态实现响应式
    store._vm = new Vue({
        data() {
            return {
                $$state: state
            }
        },
        computed
    })
    // 判断是否有上一个vue实例 如果存在则下一次dom更新的时候销毁实例
    if(oldVm) {
        Vue.nextTick(()=> oldVm.$destoryed())
    }
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // 存放所有模块的mutation
        this._actions = Object.create(null)     // 存放所有模块的actions
        this._wrappedGetters = Object.create(null)  // 存放所有模块的getters
        // 注册所有模块到Store实例上 
        // this当前实例、根状态、路径、根模块
        let state = this._modules.root.state
        installModule(this, state, [], this._modules.root)
        // console.log(this._module);
        //实现状态响应式
        resetStoreVm(this,state)

    }
    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
    commit = (type,payload) => {
        // 触发commit会触发_mutations里面的方法
        this._mutations[type].forEach(fn => fn(payload))
    }
    // 
    dispatch = (type,payload) => {
        this._actions[type].forEach(fn => fn(payload))
    }
    
    // 新增代码
    registerModule (path,rawModule) {
        // 封装成一个数组
        if(!Array.isArray(path)) path = [path]
        // 注册模块
        this._modules.register(path,rawModule)
        // 安装模块 注册模块需要使用到模块实例上的方法 在模块收集的地方做了一个模块映射
        installModule(this,this.state,path,rawModule.rawModule)
        //重置getters
        resetStoreVm(this, this.state)
    }
}
const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}

安装好模块之后,此时添加模块的mutations、actions、state都可以向其他模块一样正常使用了,但是getters的方法目前还不可以使用,这是因为我们是在 resetStoreVm方法 对 getters 进行处理的,所以我们还需要执行一次 resetStoreVm函数,才可以使用 添加模块的getters。但是,前面我们设置模块 状态响应式 的时候 执行了resetStoreVm方法 创建了一个 vue实例,添加模块的时候 又执行resetStoreVm,此时就有 两个vue实例,我们需要通过 nextTick方法 在 下一次DOM更新 的时候执行 上一个实例的 $destoryed()方法 将上一个vue实例销毁掉。
测试结果:

// store/store.js
...
store.registerModule(['h'],{
    namespaced:true,
    state: {
        l: 9
    },
    getters: {
        getL(state) {
            return state.l
        }
    }
})
...

image.png 测试发现,根模块就动态的添加了一个h模块。完成了动态注册模块。

plugins 状态持久化

vuex内部也支持插件的使用,在创建Store的时候可以注册一些插件。

import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
function persists(store) {
    
}
const store =  new Vuex.Store({
    plugins: [
        persists
    ],
    state: {
        age: 10,
    },
})

export default store

当执行插件的时候,内部会提供一个Store参数,就是一个Store实例,这样可以借助实例写出想要的插件。 所以现在自定义的vuex也要实现插件的功能。

class Store{
    constructor(options){
        ....
        // 存储插件
        this._subscriber = []
        // 遍历插件并执行
        options.plugins.forEach(fn => fn(this))
        ....
    }
    // 发布订阅
    subscriber(fn) {
        //当状态发生变化的时候自动触发
        this._subscriber.push(fn)
    }
    // 更新状态
    replaceState(newState) {
        this._vm._data.$$state = newState
    }
    ....
    // 省略后面代码
}
import Vue from 'vue'
import Vuex from '../vuex/index.js'
// import Vuex from 'vuex'
Vue.use(Vuex)
function persists(store) {
    let local = localStorage.getItem('VUEX')
    if (local) {
        // 每次页面刷新 从localStorage中获取最新的值 展示到页面
        store.replaceState(JSON.parse(local))
    }
    store.subscriber((mutations,state)=>{
        // 将状态存储在localStorage中
        localStorage.setItem('VUEX',JSON.stringify(state))
    }) 
}
const store =  new Vuex.Store({
    plugins: [
        persists
    ],
    state: {
        age: 10,
    },

   ...
   // 省略后面代码
})

export default store
// store.js
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
// 新增代码 获取最新值
function getState(store, path) {
    return path.reduce((pre, next) => {
        return pre[next]
    },store.state)
}
function installModule(store, rootState, path, module) {
    // 获取命名空间
    let namespaced  = store._modules.getNamespaced(path)
    // 收集所有模块的状态
    if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
        let parent = path.slice(0,-1).reduce((pre, next) => {
            return pre[next]
        }, rootState)
        // 将属性设置为响应式 可以新增属性
        // 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
        Vue.set(parent,path[path.length -1 ],module.state)
    }
    module.forEachMutations((mutations, type)=>{
        // 收集所有模块的mutations 存放到 实例的store._mutations上
        // 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) => {
            // 函数包装 包装传参是灵活的
            // 使this 永远指向实例 当前模块状态 入参数
            // 实现插件 ,这里需要更新state的值 内部可能会替换状态 这里如果一直使用module.state 可能是旧的值 所以需要获取最新的值
            mutations.call(store, getState(store, path), payload) //状态更改触发subscriber
            // 新增代码
            // 第一个参数是一个对象 记录mutations 和 触发commit名称 第二个参数是最新的状态值
            store._subscriber.forEach(sub => sub({mutations,type},store.state))
        })
    })
    module.forEachActions((actions, type)=>{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key)=>{
        // 同名计算属性会覆盖 所以不用存储
        store._wrappedGetters[key] = () => {
            return getters(getState(store, path))
        }
    })
    module.forEachChild((child, key)=>{
        installModule(store, rootState, path.concat(key), child)
    })
}

通过上面我们自己实现了一个持久化状态的一个插件,每一页面刷新都是读取localStorage中的最新值。

13.gif

区分 actions 和 mutations

vuex中在严格模式下只允许通过mutations修改状态,其他情况修改状态是不允许的并且会报错。下面,来实现这个功能:

import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
// 获取最新值
function getState(store, path) {
    return path.reduce((pre, next) => {
        return pre[next]
    },store.state)
}
function installModule(store, rootState, path, module) {
    // 获取命名空间
    let namespaced  = store._modules.getNamespaced(path)
    console.log(namespaced);
    // 收集所有模块的状态
    if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
        let parent = path.slice(0,-1).reduce((pre, next) => {
            return pre[next]
        }, rootState)
        // 将属性设置为响应式 可以新增属性
        // 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
        store._WithCommitting(()=>{
            Vue.set(parent,path[path.length -1 ],module.state)
        })
    }
    module.forEachMutations((mutations, type)=>{
        // 收集所有模块的mutations 存放到 实例的store._mutations上
        // 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) => {
            // 函数包装 包装传参是灵活的
            // 使this 永远指向实例 当前模块状态 入参数
            // 实现插件 ,这里需要更新state的值 内部可能会替换状态 这里如果一直使用module.state 可能是旧的值
            store._WithCommitting(()=>{
                mutations.call(store, getState(store, path), payload) //状态更改触发subscriber
            })
            // 第一个参数是一个对象 记录mutations 和 触发commit名称 第二个参数是最新的状态值
            store._subscriber.forEach(sub => sub({mutations,type},store.state))
        })
    })
    module.forEachActions((actions, type)=>{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key)=>{
        // 同名计算属性会覆盖 所以不用存储
        store._wrappedGetters[key] = () => {
            return getters(getState(store, path))
        }
    })
    module.forEachChild((child, key)=>{
        installModule(store, rootState, path.concat(key), child)
    })
}
function resetStoreVm (store, state) {
    let oldVm = state._vm
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    store.getters = Object.create(null)

    forEachValue(wrappedGetters, (fn, key) => {
        computed[key] = ()=> {
            return fn()
        }
        // 代理
        Object.defineProperty(store.getters, key, {
            get: ()=> store._vm[key]
        })
    })
    // 将状态实现响应式
    store._vm = new Vue({
        data() {
            return {
                $$state: state
            }
        },
        computed
    })
    if(oldVm) {
        Vue.nextTick(()=> oldVm.$destoryed())
    }
    // 开启严格模式
    if (store.strict) {
        store._vm.$watch(()=>store._vm._data.$$state,()=>{
            console.assert(store._committing,'只能通过mutations更改状态')
        },{deep:true,sync:true}) // 同步只要状态一变化会立即执行
    }
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // 存放所有模块的mutation
        this._actions = Object.create(null)     // 存放所有模块的actions
        this._wrappedGetters = Object.create(null)  // 存放所有模块的getters
        // 注册所有模块到Store实例上 
        // this当前实例、根状态、路径、根模块
        let state = this._modules.root.state
        installModule(this, state, [], this._modules.root)
        // console.log(this._module);
        //实现状态响应式
        resetStoreVm(this,state)
        this.strict = options.strict
        // 插件
        this._subscriber = []
        // 遍历插件并执行
        options.plugins.forEach(fn => fn(this))
        //同步Watcher
        this._committing = false
    }

    // 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
    get state() {
        // 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
        // 此时会是响应式的
        return this._vm._data.$$state
    }
    // 新增代码 同步Watcher 只能通过mutations更改状态
    _WithCommitting(fn) {
        let committing = this._committing
        this._committing = true //切片 在函数调用前 标识_committing为true
        fn()
        this._committing = committing
    }
    commit = (type,payload) => {
        // 触发commit会触发_mutations里面的方法
        this._mutations[type].forEach(fn => fn(payload))
    }
    // 
    dispatch = (type,payload) => {
        this._actions[type].forEach(fn => fn(payload))
    }

    registerModule (path,rawModule) {
        // 封装成一个数组
        if(!Array.isArray(path)) path = [path]
        // 注册模块
        this._modules.register(path,rawModule)
        // 安装模块 注册模块需要使用到模块实例上的方法 在模块收集的地方做了一个模块映射
        installModule(this,this.state,path,rawModule.rawModule)
        //重置getters
        resetStoreVm(this, this.state)
    }
    // 发布订阅
    subscriber(fn) {
        //当状态发生变化的时候自动触发
        this._subscriber.push(fn)
    }
    // 更新状态
    replaceState(newState) {
        this._WithCommitting(()=>{
            this._vm._data.$$state = newState
        })
    }
    
}
const install = (_Vue) => {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}

设置了严格模式下,需要在Store内部设置一个 _committing标识符,如果是 mutations 修改状态,就将 _committing标识符 改为 true,其他全部修改状态情况置为 false。这里使用了 _WithCommitting函数 将 _committing标识 变更,在 resetStoreVm 内部判断是否开启严格模式,开启严格模式就使用 $watch同步深度监听 状态的变化,根据设置的 _committing标识 来判定用户是否是通过 mutations 修改状态。

辅助函数 helper

上面vuex的源码的主干部分已经完成了,下面写一下vuex中的四个辅助函数实现原理。

//App.vue
<template>
  <div id="app">
      state.age: {{age}}
      getters: {{$store.getters.getAge}}
      b模块的c {{$store.state.b.c}}
      d模块的e {{$store.state.d.e}}
      <button @click="$store.state.age+=10"></button>
      <button @click="$store.commit('changeAge',5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>

<script>
const mapState = (arrayList) => {
  const obj = {}
  for(let i = 0; i < arrayList.length; i++) {
    obj[arrayList[i]] = function() {
      console.log(this);
      return this.$store.state[arrayList[i]]
    }
  }
  return obj
}
export default {
  name: 'App',
  computed: {
    ...mapState(['age']),
    // age(){
    //   return this.$store.state.age
    // }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
  使用...mapState(['age']) 其实就是相当于 age(){return this.$store.state.age}
  使用...mapGetters([getAge]) 相当于 getAge(){return this.$store.getters.getAge}

我们知道传入的是一个数组,返回的是一个对象,所以代码如下:

const mapState = (arrayList) => {
  const obj = {}
  for(let i = 0; i < arrayList.length; i++) {
    obj[arrayList[i]] = function() {
      console.log(this);
      return this.$store.state[arrayList[i]]
    }
  }
  return obj
}

运行结果如下:跟原来没区别,剩下的三种辅助函数都可以通过这种思路来书写。 image.png 辅助函数的代码如下:

// helper.js
const mapState = (arrayList) => {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function () {
            console.log(this);
            return this.$store.state[arrayList[i]]
        }
    }
    return obj
}
const mapGetters = (arrayList) => {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function () {
            return this.$store.getters.getAge
        }
    }
    return obj
}
const mapMutations = (arrayList) => {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function (payload) {
            return this.$store.commit(arrayList[i], payload)
        }
    }
    return obj
}
const mapActions = (arrayList) => {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function (payload) {
            return this.$store.dispatch(arrayList[i], payload)
        }
    }
    return obj
}
export {
    mapState,
    mapGetters,
    mapActions,
    mapMutations
}
// App.vue
<template>
  <div id="app">
      state.age: {{age}}
      getters: {{getAge}}
      b模块的c {{$store.state.b.c}}
      d模块的e {{$store.state.d.e}}
      <button @click="changeAge(5)">mustation方法</button>
      <button @click="$store.dispatch('changeAge',10)">actions方法</button>
  </div>
</template>

<script>
import {mapState,mapGetters,mapMutations} from './vuex/index.js'
export default {
  name: 'App',
  computed: {
    ...mapState(['age']),
    ...mapGetters(['getAge'])
  },
  methods: {
    ...mapMutations(['changeAge']),
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

测试结果如下: image.png

我个人对Vuex的理解

vuex 是个状态管理模式,其基本工作流程是:当用户修改状态的时候,如果是 同步 修改状态,会先提交(commit)触发 mutations方法,改变 Store实例中的状态,这也是 唯一 修改状态的途径,如果是 异步 的修改状态的方法,比如发起数据请求等,会提交 dispatch 触发 actions方法,然后在 actions方法 中通过 commit 触发 mutations方法 进而 更新Store实例的状态,然后再将 状态更新 到 Vue组件中。如果我们的项目是大型的单页面应用,那么我们就需要使用vuex在组件外部管理状态,通过这个状态管理模式我们可以很轻松的实现组件之间的通信,这也是选择vuex的原因之一,当然vuex也有自己的缺陷,就是状态不能够持久化。Vuex实现响应式原理 通过在 install方法 内通过 vue的 mixin方法 将 Store实例 注入到每个组件内,在 resetStoreVm方法内部主要是借助 vue本身的响应式原理 来实现状态的响应式,借助computed来实现 getters的缓存 效果,然后通过 发布订阅的思想 将用户定义的 mutations 和 actions 方法存储起来,当用户触发 commit、dispatch 的时候就去 订阅mutations 和 actions 找出对应的方法。如果存在 模块嵌套 的情况下,首先,vuex内部会通过一个 moduleCollection类 将所有模块 格式化 为一个 树形结构,其次,通过 installModule方法 将格式化好的树形结构 安装 到 Store实例上,最后,通过 resetStoreVm方法 借助 vue内部响应式的原理 将所有模块的状态都置为 响应式。vuex是 响应式 的同时还支持 插件 的使用,Store内部有 subscribe方法 和 replaceState方法,通过这两个方法我们实现 vuex的状态的持久化,以上说明了 vuex是高度依赖 vue响应式 和 插件系统的。以上就是我个人对vuex的理解。

如果对 Vue响应式原理 的不是很理解的小伙伴可以去之前发布的文章阅读 Vue响应式原理

综上,vuex源码的主干部分基本已经在上面全部撸完了。vuex 的核心基本已经实现了,希望对细心阅读的小伙伴们探究 vuex 核心机制有所帮助,如有错误,敬请各位大佬在评论区指正。相关的源码已经上传到gitee,感兴趣的小伙伴可以去获取源码 戳我获取源码!!!!!。