Vuex核心源码解析之手写Vuex

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之后,我们继续来来实现 Vuexgetters 方法。说到这里,不知道小伙伴们在使用 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.definePropertyget方法 来进行代理,实现方法如下:

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的缓存问题,所以这我们常说的,vuexgetters属性就相当于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 内部通过发布订阅模式,将用户定义的 mutationsactions 存储起来,当触发 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计算属性会将自己的属性放到实例上,而且 computedgetters 的是一样的,也就是说,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

在上面的 stategettersmutationsactions中,我们只做成了根模块,并没有考虑到 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上注册所有模块statemutationsactionsgetters方法。

自定义 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 遍历当前类的所有 mutationsactionsgetters、还有子模块,将参数作为 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 实例中的遍历 mutationsactionsgetters 的方法,但是添加的模块中并没有 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
}
复制代码

安装好模块之后,此时添加模块的mutationsactionsstate都可以向其他模块一样正常使用了,但是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 是个状态管理模式,通过这个状态机我们可以实现组件之间的通信,因为在 install方法 内通过 vue的 mixin方法Store实例 注入到每个组件内,在 resetStoreVm方法内部主要是借助 vue本身的响应式原理 来实现状态的响应式,借助computed来实现 getters的缓存 效果,然后通过 发布订阅的思想 将用户定义的 mutationsactions 方法存储起来,当用户触发 commitdispatch 的时候就去 订阅mutationsactions 找出对应的方法。如果存在 模块嵌套 的情况下,首先,vuex内部会通过一个 moduleCollection类 将所有模块 格式化 为一个 树形结构,其次,通过 installModule方法 将格式化好的树形结构 安装Store实例上,最后,通过 resetStoreVm方法 借助 vue内部响应式的原理 将所有模块的状态都置为 响应式。vuex是 响应式 的同时还支持 插件 的使用,Store内部有 subscribe方法replaceState方法,通过这两个方法我们实现 vuex的状态的持久化,以上说明了 vuex是高度依赖 vue响应式插件系统的。以上就是我个人对vuex的理解。

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

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

猜你喜欢

转载自juejin.im/post/7079239065278611486