vuex原理之由浅入深手写vuex

1,873 阅读3分钟

在大型的vue应用中,基本上都会使用vuex来作状态管理。我们就来一步步手写一个vuex,加深理解对vuex的理解,用起来也更加得心应手。

实现基础用法

先来看看vuex最基础的的使用。

  • store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 12
    },
    mutations: {
        increment(state){
            state.age ++ 
        }
    },
})
  • index.js
import Vue from "vue"
import App from "./App.vue"
import store from "./store"
var vm = new Vue({
    el'#root',
    store,
    render: h => h(App)
})
  • app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
    </div>
    <button @click="$store.commit('increment',2)">increment</button> 
</template>

(1)在 store/index.js 中,引入vuex后,做了两件事:Vue.use(Vuex)new Vuex.Store(options)

(2)在index.js中,导入new Vuex.Store(options)生成的实例,然后new Vue的时候作为选项参数传入。

(3)在app.vue中,也就是vm的子组件实例都挂载了一个$store属性,可以直接通过this.$store访问。

整体框架

按照以上功能,首先来写vuex的整体框架。

Vue.use会调用vuex.install方法;new Vuex.store表示Vuex.store是个类。

class Store{
    ...
}

const install = (_Vue) => {
    ...
}

export default {
  install,
  Store
}

整体框架搭好后,下面分别来完善installStore的功能。

intall方法

当我们在new Vue(options)的选项中传入store实例后,它每一个子组件上面都有一个$store属性。挂载全局$store就是在install方法完成,借助Vue.mixin()实现。

Vue.mixin(mixin)。全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
let Vue
const install = (_Vue) => {
    Vue = _Vue  //install方法调用时,会将Vue作为参数传入
    Vue.mixin({
        beforeCreate(){
            //通过this.$options可以获取new Vue(options)传递的参数options
            if(this.$options.store){//说明this是根实例
                this.$store = this.$options.store
            }else{
                //子组件$store属性指向父组件的$store属性
                //这样每个组件的$store,并且都指向根实例的$store
                this.$store = this.$parent && this.$parent.$store
            }
        }
    })
}

state

我们通过this.$store.state访问数据。说明Store类有一个state属性。

class Store{
    constructor(options){
        this.state = options.state
    }
}
  • app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
    </div>
    <button @click="$store.state.age++">increment</button> 
</template>

这样写能拿到$store.state.age的值,但是点击button修改值不能响应式的反映到视图上。实现数据响应,vuex借助了vue的响应机制。

class Store{
    constructor(options){
        this.vm = new Vue({ //vue对data中的数据进行了依赖收集,因此data中的数据是响应式的
            data() {
                return {
                    state: options.state
                }
            }
        })
    }
    get state(){ //es6写法,访问state属性会执行此方法
        return this.vm.state
    }
}

getters

getters类似于Vue中的computed,返回基于state的计算值。

class Store{
    constructor(options){
        ...
        let getters = options.getters
        
        //这里对象主要用来存储,Object.create(null)比起{}少了原型对象,可以提升一点点性能
        this.getters = Object.create(null)
        
        //options传入的每一个getter是函数,而我们要访问的getter是一个值,类似computed
        Object.keys(getters).forEach( (getterName) => {
            Object.defineProperty(this.getters, getterName, {
                get: () => {
                    return getters[getterName](this.state)
                }
            })   
        })
    }
}

测试:

//store/index.js
export default new Vuex.Store({
    ...
    getters: {
        computedAge(state){
            return state.age + 10
        }
    }
})
//app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
        <div>{{$store.getters.computedAge}}</div>
        <button @click="$store.state.age++">increment</button> 
    </div>
</template>

mutation和commit

我们使用 vuex 改变数据时,是触发 commit 方法,像这样:this.$store.commit('increment',2)。这里的increment对应的是mutations中的事件类型。

export default new Vuex.Store({
    state: {
        age: 12
    },
    mutations: {
        increment(state,payload){
            state.age +=  payload
        }
    }
})

这其实是一个事件的订阅和发布的过程,拿到options.mutations,进行事件订阅,执行commit进行事件发布。

class Store{
    constructor(options){
        ...
        let mutations = options.mutations
        this.mutations = Object.create(null)
        
        //订阅事件
        Object.keys(mutations).forEach((type) => {
            //为每一个事件(type)维护一个订阅函数数组,每次订阅事件就往该数组里面添加订阅函数
            const handlers = this.mutations[type] || (this.mutations[type] = [])
            handlers.push((payload) => {
                mutations[type](this.state,payload)
            })
        })
    }
    
    commit = (type,payload) => {
        //发布事件,执行订阅函数数组里面的每一个函数
        this.mutations[type].forEach((fn) => {
            fn(payload)
        })
    }
}

action和dispatch

vuex提倡 mutation是同步函数;要异步改变state就用action, 通过dispatch方法触发。(在非严格模式下,mutation是异步函数其实也不会报错)

//store/index.js
export default new Vuex.Store({
    state: {
        age: 12
    },
    actions: {
        incrementAsync(store,payload){
            setTimeout(() => {
                store.commit('increment',payload)
            },1000)
        }
    }
})
//app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
        <button @click="$store.dispatch('incrementAsync',5)">incrementAsync</button> 
    </div>
</template>

这也是一个事件的订阅和发布的过程,拿到options.actions,进行事件订阅,执行dispatch进行事件发布。类比就是action--mutation,dispatch--commit。

class Store{
    constructor(options){
        ...
        let actions = options.actions
        this.actions = Object.create(null)
        
        //订阅事件
        Object.keys(actions).forEach((type) => {
            //为每一个事件(type)维护一个订阅函数数组,每次订阅事件就往该数组里面添加订阅函数
            const handlers = this.actions[type] || (this.actions[type] = [])
            handlers.push((payload) => {
                actions[type](this,payload) //action订阅函数的第一个参数是store
            })
        })
    }
    
    dispatch = (type,payload) => {
        //发布事件,执行订阅函数数组里面的每一个函数
        this.actions[type].forEach((fn) => {
            fn(payload)
        })
    }
}

加module功能

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

随着项目规模变大,多人协作,分模块会使数据管理更清晰。

//store/index.js
const ModuleA = {
    // namespaced: true,
    state: {
        age: 11
    },
    mutations: {
        increment(state){
            console.log('module A')
            state.age ++ 
        }
    },
    getters: {
        computedAgeAFromA(state){
            return state.age + ' aFromA'
        }
    }
}

export default new Vuex.Store({
    modules: {
        a: ModuleA
    },
    state: {
        age: 12
    },
    getters: {
        computedAge(state){
            return state.age + 10
        },
        computedAgeA(state){
           return state.a.age + 10
        }
    },
    mutations: {
        increment(state,payload){
            console.log('module root')
            state.age += payload
        }
    }
})

//app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
        <div>{{$store.getters.computedAge}}</div>
        <div>{{$store.getters.computedAgeA}}</div>
        <div>{{$store.getters.computedAgeAFromA}}</div>
        <button @click="$store.commit('increment',2)">increment</button> 
    </div>
</template>
  • (1)在上面例子中,A模块定义的数据age通过state.a.age访问,根模块的数据age通过state.age访问
  • (2)定义在A模块和根模块中的getters都会直接挂载到store上
  • (3)执行store.commit时,A模块和根模块的同名mutation都会执行。(store.dispatch也同理)

也就是说,除了state保持树状机构,getters、mutations、actions都进行了扁平化,直接挂载在store上。

实现上述module功能,主要分两步:1、根据选项,创建模块树。 2、遍历模块树,安装模块,实现各个模块state、getters、mutations、actions的挂载。

创建模块树

vuex定义树中的每个module节点如下:

class Module { //module节点
    constructor(moduleOption){
        this._rawModule = moduleOption //传入的module选项
        this._children = Object.create(null) //子节点,这里用对象存储子节点,大概是要拿到模块名吧,如果是数组,数组的每一项需要增加一个字段来表示模块名。
        this.state = moduleOption.state //state
    }

    get namespaced () {
        return !!this._rawModule.namespaced //模块是否使用namespace
    }
}

根据选项生成module节点后,就可以来创建module树了。

export default class ModuleCollection { //this.root 整个module树的根
    constructor (rootModuleOption) {
        this.register([], rootModuleOption)
    }

    register(path, moduleOption){
        const module = new Module(moduleOption)
        if(path.length === 0){
            this.root = module
        }else{
            let parentModule = path.slice(0,-1).reduce((module,key) => {
                return module._children[key]
            },this.root)

            parentModule._children[path.pop()] = module
        }
        if(moduleOption.modules){
            Object.keys(moduleOption.modules).forEach( (key) => {
                this.register(path.concat(key),moduleOption.modules[key])
            })
        }
    }
}

register(path, moduleOption)中path主要用于定位当前module节点的父节点。path为空数组时,说明当前节点是根节点,否则,借助slice和reduce找到当前节点的父节点。 和下面这种树的注册方式相比,传入path让每个module节点具名,安装模块时会用到。后面的namespace也是基于path。

register(parentModule, moduleOption){
   const module = new Module(moduleOption)
   if(!parentModule){
      this.root = module
   }else{
      parentModule._children.push(module)//此时module._children是数组
   }
   if(moduleOption.modules){
      Object.keys(moduleOption.modules).forEach( (key) => {
          this.register(module,moduleOption.modules[key])
      })
   }
}

安装模块

class Store {
    constructor(options){
        ...
        this.getters = Object.create(null)
        this.mutations = Object.create(null)
        this.actions = Object.create(null)

        this._modules = new ModuleCollection(options)   //拿到module树
        installModule(this,this.state,[],this._modules.root)   //安装模块
    }
    ...
}

function installModule(store,state,path,module){
    let rawModule = module._rawModule
    //state是保持树结构,挂载方法类似创建module树
    /****
      store.state = {
	age: 1,
        a: {
            age: 2,
            c: {
                age: 4
            }
        },
        b: {
            age: 3
        }
      }
    ***/
    if(path.length > 0){
        let parentState = path.slice(0,-1).reduce((state,key) => {
            return state[key]
        },state)
        Vue.set(parentState,path.pop(),rawModule.state)
    }
    
    //挂载getters
    if(rawModule.getters){
        Object.keys(rawModule.getters).forEach((getterName) =>{
            Object.defineProperty(store.getters, getterName, {
                get: () => {
                    return rawModule.getters[getterName](rawModule.state)
                }
            })
        })
    }
    
    //挂载mutations
    if(rawModule.mutations){
        Object.keys(rawModule.mutations).forEach((type) => {
            const handlers = store.mutations[type] || (store.mutations[type] = [])
            handlers.push((payload) => {
                rawModule.mutations[type](rawModule.state,payload)
            })
        })
    }
    
    //挂载actions
    if(rawModule.actions){
        Object.keys(rawModule.actions).forEach((type) => {
            const handlers = store.actions[type] || (store.actions[type] = [])
            handlers.push((payload) => {
                rawModule.actions[type](store,payload)
            })
        })
    }
    
    // 递归
    if(module._children){
        Object.keys(module._children).forEach((key) => {
            installModule(store,state,path.concat(key),module._children[key])
        })
    }
}

加namespace

没有namespace的话,当执行store.commit时,A模块和根模块的同名mutation都会执行。要避免重复执行,得在模块里写mutation之前要去其他模块查看一下mutation名称是否已经存在。使用namespace可以避免这个问题。需要在module里配置namespaced: true

const ModuleA = {
    namespaced: true,
    state: {},
    mutations: {}
}

namespace是每个模块的唯一标志。vuex以模块节点的访问路径作为模块的namespace。

function getNamespace(root,path){ 
    let module = root
    return path.reduce((namespace, key) => {
      module = module._children[key]
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
}
//path=[] --> ''
//path=['a'] --> 'a/'
//path=['a','c'] --> 'a/c/'

得到namespace后,在安装getters、mutations、actions时就把namespace加上,就ok了。

function installModule(store,state,path,module){
    ...
    let namespace = getNamespace(store._modules.root,path)

    if(rawModule.getters){
        Object.keys(rawModule.getters).forEach((getterName) =>{
            let namespacedGetter = namespace + getterName  //<----- 这里
            Object.defineProperty(store.getters, namespacedGetter, {
                get: () => {
                    return rawModule.getters[getterName](rawModule.state)
                }
            })
        })
    }
    if(rawModule.mutations){
        Object.keys(rawModule.mutations).forEach((type) => {
            let namespacedType = namespace + type   //<----- 这里
            const handlers = store.mutations[namespacedType] || (store.mutations[namespacedType] = [])
            handlers.push((payload) => {
                rawModule.mutations[type](rawModule.state,payload)
            })
        })
    }
    if(rawModule.actions){
        Object.keys(rawModule.actions).forEach((type) => {
            let namespacedType = namespace + type  //<----- 这里
            const handlers = store.actions[namespacedType] || (store.actions[namespacedType] = [])
            handlers.push((payload) => {
                rawModule.actions[type](store,payload)
            })
        })
    }
    ...
}

源码

  • vuex/index.js
import ModuleCollection from "./module-collection.js"
let Vue

function getNamespace(root,path){ 
    let module = root
    return path.reduce((namespace, key) => {
      module = module._children[key]
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
}

function installModule(store,state,path,module){
    let rawModule = module._rawModule
    let namespace = getNamespace(store._modules.root,path)
    if(path.length > 0){
        let parentState = path.slice(0,-1).reduce((state,key) => {
            return state[key]
        },state)
        Vue.set(parentState,path.pop(),rawModule.state)
    }
    if(rawModule.getters){
        Object.keys(rawModule.getters).forEach((getterName) =>{
            let namespacedGetter = namespace + getterName
            Object.defineProperty(store.getters, namespacedGetter, {
                get: () => {
                    return rawModule.getters[getterName](rawModule.state)
                }
            })
        })
    }
    if(rawModule.mutations){
        Object.keys(rawModule.mutations).forEach((type) => {
            let namespacedType = namespace + type
            const handlers = store.mutations[namespacedType] || (store.mutations[namespacedType] = [])
            handlers.push((payload) => {
                rawModule.mutations[type](rawModule.state,payload)
            })
        })
    }
    if(rawModule.actions){
        Object.keys(rawModule.actions).forEach((type) => {
            let namespacedType = namespace + type
            const handlers = store.actions[namespacedType] || (store.actions[namespacedType] = [])
            handlers.push((payload) => {
                rawModule.actions[type](store,payload)
            })
        })
    }
    
    if(module._children){
        Object.keys(module._children).forEach((key) => {
            installModule(store,state,path.concat(key),module._children[key])
        })
    }
}
class Store {
    constructor(options){
        this.vm = new Vue({
            data() {
                return {
                    state: options.state
                }
            }
        })


        this.getters = Object.create(null)
        this.mutations = Object.create(null)
        this.actions = Object.create(null)

        this._modules = new ModuleCollection(options)
        installModule(this,this.state,[],this._modules.root)
        console.log(this)
    }
    get state(){
        return this.vm.state
    }

    commit = (type,payload) => {
        this.mutations[type].forEach((fn) => {
            fn(payload)
        })
    }

    dispatch = (type,payload) => {
        this.actions[type].forEach((fn) => {
            fn(payload)
        })
    }
}

const install = (_Vue) => {
    Vue = _Vue

    Vue.mixin({
        beforeCreate(){
            if(this.$options.store){
                this.$store = this.$options.store
            }else{
                this.$store = this.$parent && this.$parent.$store
            }
        }
    })
}

export default {
    install,
    Store
}
  • vuex/module-collection.js
class Module { //module节点
    constructor(moduleOption){
        this._rawModule = moduleOption
        this._children = Object.create(null)
        this.state = moduleOption.state
    }

    get namespaced () {
        return !!this._rawModule.namespaced
    }
}

export default class ModuleCollection { //this.root 整个module树的根
    constructor (rootModuleOption) {
        this.register([], rootModuleOption)
    }

    register(path, moduleOption){
        const module = new Module(moduleOption)
        if(path.length === 0){
            this.root = module
        }else{
            let parentModule = path.slice(0,-1).reduce((module,key) => {
                return module._children[key]
            },this.root)

            parentModule._children[path.pop()] = module
        }
        if(moduleOption.modules){
            Object.keys(moduleOption.modules).forEach( (key) => {
                this.register(path.concat(key),moduleOption.modules[key])
            })
        }
    }
}

vue源码系列文章:

vue2.0的响应式原理

vue编译流程分析

vuex原理之由浅入深手写vuex

vue组件从构建VNode到生成真实节点树