vuex源码原理分析

118 阅读5分钟

首先要确认版本,vue2搭配vuex3,vue3搭配vuex4,我们这篇文章说的的vuex3。

手写源码 :gitee.com/rootegg/fro…

手写源码视频:vuex手写视频

学习视频:vuex源码分析

image.png

Vuex就是Vue的一个实例,可认为跟其他组件一样

相信看这篇文章的同学,都是对vuex有使用过一段时间,想深入了解原理,下面几点是看了源码后总结的:

  • 1、vuex利用mixin混入beforeCreate生命周期实现$store挂载

  • 2、vuex中state利用new Vue创建实例实现双向绑定

  • 3、getter利用Object.defineProperty实现响应式,利用Vue的computed实现缓存

  • 4、子模块modules默认是透明的没有命名空间限制

  • 5、子模块透明模式下,state是需要有子模块层级调用,getter重名会报错且只返回最后一个的值,mutation重名是都会执行,action重名是都会执行,有命名空间后,getters\mutations\actions都按命名空间层级调用

  • 6、设置strict严格模式下,只能mutation里修改state状态,外部直接修改state状态会报错错,因为有监听state深入且同步变化

  • 7、action是异步,因为执行了Promise.all方法后,返回了Promise对象;mutation是同步的,直接forEach同步执行的

  • 8、其他实例函数有 replaceState,watch,replaceState,subscribeAction,registerModule,unregisterModule,hasModule,hotUpdate

  • 9、其他辅助函数有 mapState,mapGetters,mapMutations,mapActions,createNamespacedHelpers

后面逐条分析上面总结。

vuex使用过程

import Vue from 'vue'
import Vuex from 'vuex'

// 必须先use
Vue.use(Vuex)

// 实例化Store
const store = new Vuex.Store({
    state:{},
    getters:{},
    mutations:{},
    actions:{
        actions: {
          // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
          someAction ({ dispatch, commit, getters, rootGetters }) {
              dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
              commit('someMutation', null, { root: true }) // -> 'someMutation'
          }
        },
    },
    strict: true, // 【可选】开启严格模式,生产环境建议关闭
    plugins: [],   // 【可选】插件
    modules:{      // 【可选】子模块
        post:{ 
            namespaced: true,
            actions: {
                someAction: {
                  root: true,  // 子模块注册全局 action
                  handler (namespacedContext, payload) { ... } // -> 'someAction'
                }
            }
        }
    },    
})

// store属性给根vue实例
new Vue({
    store
})

mixin混入beforeCreate实现$store挂载

从上面看到,第一步是 Vue.use(Vuex),里面是执行了什么呢?

use方法实际上就是执行了install方法,所以Vuex会导出install方法

  • 1、给Vue构造函数混入了beforeCreate方法,那么我们项目里面每个vue文件都会执行这个beforeCreate方法,特别注意:很多人搞混了认为我们整个项目就一个Vue实例就是入口main.js里面的new Vue,实际上我们每个组件,每个子组件都是一个new Vue的实例,所以你的项目里有成千上万个new Vue实例

  • 2、每个组件在实例化之前都会来执行 beforeCreate 方法

  • 3、上面我们在根组件山挂载了 store 对象,所以根组件会来执行 this.$store = typeof options.store ==='function' ? options.store() : options.store 这句,根上就有了 $store 属性了

  • 4、子组件在实例化的时候会执行 this.$store = options.parent.$store 这句,因为子组件上面没有 options.store 会判断为false,所以就去从 parent 上级组件中获取 $store 属性,所以整个项目里成千上万的vue实例都是用的同一个 $store 对象

  • 5、特别的install方法可以改成一句代码,不用这么复杂,这里是不是可以用原型链方式修改 Vue.prototype.$store = options.store呢?

  • 6、 在vue4版本中使用的provide/inject方式,就没有用beforeCreate方式了

思考:可以改为原型链方式Vue.prototype方式吗,对客户端渲染没问题。如果是服务端渲染时,那就会导致各用户store对象交叉感染,所以我想原型链方式是不行的,所以不能用原型链

export function install (_Vue) {
  Vue.mixin({ beforeCreate: vuexInit })
}

function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }

state和getters实现原理

state利用new Vue创建实例实现双向绑定

getter利用Object.defineProperty实现响应式,用computed计算属性做缓存

// 获取配置的 state, getters对象
const { state, getters } = options

// getters利用Vue计算属性做缓存
const computed = {}

// getter利用define.Property实现响应式
forEachValue(getters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
})

// state利用new Vue创建实例实现双向绑定
store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
})

// 返回state属性可供 this.$store.state使用
get state () {
    return this._vm._data.$$state
}
  • 1、很明显了,state是通过new Vue实例化的data属性,所以双向绑定就成了

  • 2、getter是处理方式,是Object.definePropertycomputed

难以理解的是getter是处理方式,为什么是Object.definePropertycomputed呢?

这里做个说明:对于为什么用Object.defineProperty呢,为什么不简单的写成下面直接属性呢

forEachValue(getters, (fn, key) => {
    computed[key] = partial(fn, store)
    store.getters[key] = store._vm[key]
})

因为关键在 enumerable: true 以及 Object.defineProperty 没写的默认值 configurable: falsewritable: false

如果采用store.getters[key] = store._vm[key]这种方式,那么属性修改,重设,删除 都是允许的,我们就可以在外面修改getter熟属性了

然后用vuex的Object.defineProperty写法,那属性修改,重设,删除 都是被禁止的,外面不能修改getter属性,即下面这三种操作都会自动报错

this.$store.getters.getA = 123
Object.defineProperty(this.$store.getters,'getA',{
    get:()=>return 123
})
delete this.$store.getters.getA

第一点为什么用Object.defineProperty就说清楚了,为了属性不被修改

第二点为什么用computed呢,因为是做缓存,如果不做缓存,我们会写成如下:

forEachValue(getters, (fn, key) => {
    Object.defineProperty(store.getters, key, {
      get: () => partial(fn, store),
      enumerable: true // for local getters
    })
})

那么如partial调用,每次页面都会执行get方法,执行partial里面的逻辑,相比用了computed计算属性就不用每次重复计算了,只会在getters中以来的state变化后才会重新计算逻辑。

strict严格模式为什么只能mutation里修改state状态

如果strict设置为true,就表示开启严格模式,默认不开启。我们先看下源码实现:

// 严格模式下才执行enableStrictMode
if (store.strict) {
    enableStrictMode(store)
}

// enableStrictMode里面监听了state变化,deep: true, sync: true
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

// 断言false就报错
function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}
  

从源码就可以看到,严格模式下会监听state深层次变化,如果state变了,此时就可能是commit改变的或者外部直接修改的state

watch的state变化,如果_committing判断到是false,就报错了

我们看看_committing是什么,因为执行mutations是commit调用的,内部封装了commit会执行_withCommit函数,先_committing置为true,再实际执行我们的mutations方法去修改state,所以上面assert(store._committing)在commit中就会为true,而外部直接修改state的情况下,没有设置_committing为true,所以严格模式下就报错了,以此达到了目的

_withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}

为什么actions是异步而mutations是同步

我们回答 actions 和 mutations 的区别时候,都会说两点:

1、数据是从dispatch调用actions中函数,commit执行mutations函数

2、actions是异步函数,mutations是同步函数

看看源码实现过程,自然就理解了:

  • dispatch 调用Promise.all 返回 Promise,actions是数组,在多子模块下,透明模式下的actions是都挂载根

  • commit是同步forEach遍历执行,mutations是数组,在多子模块下,透明模式下的mutations是都挂载根节点下


// dispatch 调用Promise.all 返回 Promise,actions是数组,在多子模块下,透明模式下的actions是都挂载根节点下
dispatch (_type, _payload) {
    const action = { type, payload }
    const entry = this._actions[type]
    
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
      
     return new Promise((resolve, reject) => {
        result.then(res => {
            resolve(res)
        })
     })
}

// commit是同步forEach遍历执行,mutations是数组,在多子模块下,透明模式下的mutations是都挂载根节点下
commit (_type, _payload, _options) {
    const mutation = { type, payload }
    const entry = this._mutations[type]
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
  }

modules子模块和namespaced命名空间

把结论说一下:

  • 子模块modules默认是透明的没有命名空间限制

  • 子模块透明模式下,state是需要有子模块层级调用,getter重名会报错且只返回最后一个的值,mutation重名是都会执行,action重名是都会执行

  • 有命名空间后,getters\mutations\actions都按命名空间层级调用

  • registerModule 函数动态注册子模块

这个地方,只用看registerModule函数是怎么实现的:

function registerModule (path, rawModule, options = {}) {
    // path 可以是字符串,或者路劲数组
    if (typeof path === 'string') path = [path]

    // rawModule 原始格式重新格式化,做成 new Module 类实例
    this._modules.register(path, rawModule)
    
    // 这里是重点,里面子模块初始化state,getters,mutations,actions
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    
    // 重新执行new Vue,绑定state和getters
    resetStoreVM(this, this.state)
}


function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

const namespacedType = namespace + key 这句就是计算命名空间的,可以看到取的时候加上了命名空间

看下 registerMutation ,看到type类型后面根的数组函数,registerAction 同理是数组函数

registerGetter 稍有不同,不接受同名会直接报错

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

plugins 插件

源码中就一句

plugins.forEach(plugin => plugin(this))

所以插件就是一个函数,store实例就是参数,通常用法都是订阅

function createLogger(store){
    // 订阅mutation完成后调用
    store.subscribe((mutation, state) => {
      console.log(mutation)
      console.log(state)
    })
    // 订阅mutation完成后调用
    store.subscribeAction((action, state) => {
      console.log(action.type)
      console.log(action.payload)
    })
}