Vuex3.x源码解析

115 阅读4分钟

vuex作为vue官方出品的状态管理插件,以及其简单API设计、便捷的开发工具支持,在vue项目中得到很好的应用。

了解其底层实现原理有助于我们更好的项目开发,理解与运用。

我们知道,vuex是作为vue框架的一个插件而存在,vuex只能使用在vue上,很大的程度是因为其高度依赖于vue的响应式系统以及其插件系统。每一个vue插件都需要有一个公开的install方法,vuex也不例外。

我们首先查看Vuex的入口文件src/index.js源码:

// src/index.js
# 导出一个store插件对象
export default {
  Store, # store类对象
  install, # 安装方法
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

可以看到导出的store插件对象里面包含了两个最重要的内容:

  • Store类。
  • install方法。

1,Store 类

我们平时在项目中使用Vuex:

import Vue from 'vue'
# 这个vuex就是上面src/index.js导出的默认对象,里面包含了Store和Install
import Vuex from 'vuex'
// 注册vuex插件 调用Install
Vue.use(Vuex)
​
# 初始化一个Store实例  参数为选项对象
export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  },
  plugins: []
})

我们继续看Store类的定义:

// src/store.js
​
# store类的定义
export class Store {
  # 主要看构造器:参数为选项对象
  constructor (options = {}) {
​
    # 从参数对象中取出plugins插件列表,默认为一个空数组
    const {plugins = [],strict = false} = options
​
    // store internal state
    # 初始化store上的一些实例属性
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)
​
    // bind commit and dispatch to self
    # 将commit和dispatch方法绑定为正确的调用
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }
​
    // 严格模式
    this.strict = strict
    // 取出state对象数据
    const state = this._modules.root.state
​
    # 重点:创建响应式的state
    resetStoreVM(this, state)
​
    # 循环注册插件
    plugins.forEach(plugin => plugin(this))
    # 启用devtool
    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }
 
  # 定义state访问器属性
  get state () {
    return this._vm._data.$$state
  }
​
  set state (v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }
  
  # 同步提交方法,立即生效
  commit (_type, _payload, _options) {
    // check object-style commit
    ...
  }
  # 异步提交方法,需要等待Promsie状态确定后才修改state数据,所以我们使用数据需要根据Promise来操作
  dispatch (_type, _payload) {
    // check object-style dispatch
    ...
  }
​
  subscribe (fn, options) {
    return genericSubscribe(fn, this._subscribers, options)
  }
​
  subscribeAction (fn, options) {
    const subs = typeof fn === 'function' ? { before: fn } : fn
    return genericSubscribe(subs, this._actionSubscribers, options)
  }
​
  watch (getter, cb, options) {
    if (__DEV__) {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }
}

可以看见class Store里面定义了很多的内容,比如我们最熟悉的commit和dispatch方法。所以我们在通过new Store初始化一个Store实例后,可以通过store.commit()来操作state数据。

然后我们重点看constructor构造器的内容,这里面的内容虽然比较多,但实际上只有三个重点:

  • 初始化一些实例属性。
  • 创建响应式的state【重点】。
  • 循环安装插件【比如数据持久化插件:将存储在内存的state数据同步到sessionStorage】。

我们重点看对state的处理:

resetStoreVM(this, state)
// src/store.js
function resetStoreVM (store, state, hot) {
  ...
  
  // 创建了一个vue实例存储到_vm属性
  # 本质就是创建了一个隐藏的Vue组件,因为vue2的Vue实例可以作为组件来使用,Vue3的createApp不行
  store._vm = new Vue({
    # state 作为组件的data
    data: {
      $$state: state
    }
  })
}

省略一些边缘代码后,我们直接看resetStoreVM方法核心内容,为store对象定义了一个_vm属性,这个属性值为一个Vue实例。熟悉Vue2的源码都知道,vue2的组件构造器继承自Vue构造器,所以Vue可以当成一个组件来使用,它也可以传入data选项,来创建响应式的数据【只是平时我们的项目并不需要这样使用】,这里在data中只定义了一个响应式数据$$state,它的值就是我们传入的state对象。

所以resetStoreVM函数最重要的作用就是:给store对象定义了一个_vm属性来存储响应式的state数据

我们再回头看看Store类中定义的访问器属性state:

# 定义state访问器属性
get state () {
   // 实际访问  
   return this._vm._data.$$state
}

所以我们在组件中使用this.$store.state实际上访问的就是响应式的store._vm._data.$$state

注意: 这里为啥是_vm._data.$$state,而不是直接访问_vm.$$state,熟悉Vue2源码响应式原理的可以知道,组件的响应式数据真正是存储在_data属性上的。我们在组件中可以直接通过this.count访问变量,是因为组件的data选项初始化时创建了对应的访问器属性count,代理到了_data属性,即this.count等同于访问this._data.count。所以上面也是同理。

我们现在已经知道了new Vuex.Store()初始化后Store实例的内容了:它是一个对象,存储了响应式的state数据,以及操作sate数据的方法。注意: 但是我们为啥能够在每个组件上使用this.$store来访问store里面的内容呢?这就和Install方法有关了。

2,install 方法

我们查看install源码:

// src/store.js
export function install (_Vue) {
  ...
  
  # 调用混入
  applyMixin(Vue)
}

我们继续查看applyMixin源码:

// src/mixins.js
# 对应applyMixin方法
export default function (Vue) {
  // 获取vue的版本
  const version = Number(Vue.version.split('.')[0])
  // 如果版本大于2,
  if (version >= 2) {
    # 则使用Vue.mixin混入一个全局对象 
    // 注意:一个混入对象可以包含任意组件选项,这里为每个组件混入了beforeCreate生命钩子函数
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // for 1.x backwards compatibility.
    ...
  }
​
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  # vuex初始化方法
  function vuexInit () {
    const options = this.$options
    // 对每个组件实例注入$store属性 即store对象
    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
    }
  }
}

applyMixin方法内容比较简单,主要就是使用Vue.mixin全局混入了一个对象,即为每个组件混入了beforeCreate钩子函数。

注意: 当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

1,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先

2,同名钩子函数将合并为一个数组,因此都将被调用。并且混入对象的钩子将在组件自身钩子之前调用。

这个钩子函数的内容就是vuexInit方法,而它的内容其实就是为当前组件实例设置一个$store属性。所以每个组件在初始化时,其组件实例都被注入了一个$store属性【这个属性优先会从自身的$options上取值,如果没有就会从父级上取值】,而最开始的注入就是从根组件开始:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'new Vue({
  router,
  store, # 将store对象注入到根组件
  render: h => h(App)
}).$mount('#app')

最终通过install方法:将 store 对象从根组件中注入到所有的子组件里,这也是为什么每个组件都可以使用this.$store来获取store对象的数据。

最后一句话总结Vuex原理: 通过install方法为每个组件混入了beforeCreate生命周期钩子函数,函数的内容就是为当前组件实例设置一个$store属性,值为new Vue.Store生成的store对象,这样在每个组件内都可以通过this.$store访问state数据。

根据Vuex源码可以推测vue-router插件使用了相同的原理。