1. Vuex 引言

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。那到底什么是状态管理模式呢?

1.1 基本状态管理模式

以下是一个简单的计数应用:

  1. new Vue({
  2. // state
  3. data () {
  4. return {
  5. count: 0
  6. }
  7. },
  8. // view
  9. template: `
  10. <div>{{ count }}</div>
  11. `,
  12. // actions
  13. methods: {
  14. increment () {
  15. this.count++
  16. }
  17. }
  18. })

其中:

  • state: 驱动应用的数据源
  • view: 以声明方式将 state 映射到视图
  • actions: 响应在 view 上的用户输入导致的状态变化

基本的状态管理模式是一个单向数据流,但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态

这时候,可以把组件的共享状态抽取出来,在一个全局的单例模式下进行管理,这便是 vuex 背后的思想

1.2 Vuex 简介及结构

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新
  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用

下面是 Vuex 常用方式的结构及运行机制:
vuex运行机制.png

  1. // ChildMod.js
  2. // state
  3. const state = {
  4. }
  5. // getters
  6. const getters = {
  7. }
  8. // actions
  9. const actions = {
  10. }
  11. // mutations
  12. const mutations = {
  13. }
  14. export default {
  15. namespaced: false,
  16. state,
  17. getters,
  18. actions,
  19. mutations
  20. }

接着统一在 index.js 中进行管理

  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. import ChildMod from './modules/ChildMod'
  4. Vue.use(Vuex)
  5. const debug = process.env.NODE_ENV !== 'production'
  6. export default new Vuex.Store({
  7. modules: {
  8. ChildMod,
  9. },
  10. strict: debug,
  11. })

2. Vuex 源码解析(建议结合源码阅读)

下面将对 Vuex 的源码进行一些解读,主要包括:

  • Vuex 引入及使用
  • Vuex 实例化过程(建立一个仓库)

2.1 Vuex 引入及使用

首先我们要使用 Vuex ,第一件要做的事便是

  1. import Vuex from 'vuex'
  2. Vue.use(Vuex)

在源码的 /src/index.js 中可以看到,导出的是这么一个对象

  1. export default {
  2. Store,
  3. install,
  4. version: '__VERSION__',
  5. mapState,
  6. mapMutations,
  7. mapGetters,
  8. mapActions,
  9. createNamespacedHelpers
  10. }

根据官网对于 Vue.use() 方法的介绍,导入 Vuex 后会执行其导出的 install 方法
Vue.use.png
我们来看看 install 方法的具体实现:

  1. // /src/store.js
  2. export function install (_Vue) {
  3. if (Vue && _Vue === Vue) { // 保证了只会进行一次安装
  4. if (__DEV__) {
  5. console.error(
  6. '[vuex] already installed. Vue.use(Vuex) should be called only once.'
  7. )
  8. }
  9. return
  10. }
  11. Vue = _Vue
  12. applyMixin(Vue) // 见下方
  13. }

其中 applyMixin() 方法的具体实现如下,实现了在每个 Vue 实例中可以通过 this.$store 的方式获取 store:

  1. // /src/mixin.js
  2. export default function (Vue) {
  3. const version = Number(Vue.version.split('.')[0])
  4. if (version >= 2) {
  5. Vue.mixin({ beforeCreate: vuexInit }) // 在每个 Vue 实例的 beforeCreate 生命周期前执行 vuexInit 方法
  6. } else {
  7. // 覆盖旧的用法,向后兼容 1.x 版本
  8. const _init = Vue.prototype._init
  9. Vue.prototype._init = function (options = {}) {
  10. options.init = options.init
  11. ? [vuexInit].concat(options.init)
  12. : vuexInit
  13. _init.call(this, options)
  14. }
  15. }
  16. // 给每个 vue 实例注入 $store
  17. function vuexInit () {
  18. const options = this.$options
  19. // store injection
  20. if (options.store) {
  21. this.$store = typeof options.store === 'function'
  22. ? options.store()
  23. : options.store
  24. } else if (options.parent && options.parent.$store) { // 寻找父级的 store
  25. this.$store = options.parent.$store
  26. }
  27. }
  28. }

这里首先通过 Vue.mixin() 方法对每个 Vue 实例进行初始化
Vue.mixin.png
接着,可以看出每个组件均可以从其父级组件获取 store ,并且由于根组件在创建时,即:

  1. new Vue({
  2. router,
  3. store,
  4. render: h => h(App)
  5. }).$mount('#app')

在 new Vue() 过程中,便会在根组件上的 $options 中注入 store ,因此便能实现在每个 Vue 实例上注入 $store 参数

2.2 实例化过程(建立一个仓库)

我们是通过以下方式新建一个仓库(store):

  1. const store = new Vuex.Store(options)

这里的 Store 方法定义在 /src/store.js 中,Store 是一个 class ,这里重点看其构造函数:

  1. // /src/store.js
  2. export class Store {
  3. constructor (options = {}) {
  4. // 当通过外链式引入 vue 时手动对其进行 install
  5. if (!Vue && typeof window !== 'undefined' && window.Vue) {
  6. install(window.Vue)
  7. }
  8. // 开发环境时的一些断言
  9. // 未执行 Vue.use()
  10. // 不支持 promise 时
  11. // 是否使用 new 的方式来执行
  12. if (__DEV__) {
  13. assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
  14. assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
  15. assert(this instanceof Store, `store must be called with the new operator.`)
  16. }
  17. const {
  18. plugins = [],
  19. strict = false
  20. } = options
  21. // store 中一些内部状态的初始化
  22. this._committing = false
  23. this._actions = Object.create(null)
  24. this._actionSubscribers = []
  25. this._mutations = Object.create(null)
  26. this._wrappedGetters = Object.create(null)
  27. this._modules = new ModuleCollection(options) // 初始化module
  28. this._modulesNamespaceMap = Object.create(null)
  29. this._subscribers = []
  30. this._watcherVM = new Vue()
  31. this._makeLocalGettersCache = Object.create(null)
  32. // 改变 dispatch 和 commit 中 this 的上下文,指向当前 new 的 Store 对象
  33. const store = this
  34. const { dispatch, commit } = this
  35. this.dispatch = function boundDispatch (type, payload) {
  36. return dispatch.call(store, type, payload)
  37. }
  38. this.commit = function boundCommit (type, payload, options) {
  39. return commit.call(store, type, payload, options)
  40. }
  41. // 严格模式
  42. this.strict = strict
  43. const state = this._modules.root.state
  44. // 递归安装整个模块,包括 actions、mutations、wrappedGetters
  45. installModule(this, state, [], this._modules.root)
  46. // 将 state 建立响应式关系,以及 wrappedGetters 与 state 的依赖关系
  47. resetStoreVM(this, state)
  48. // 遍历 plugins ,执行对应逻辑
  49. plugins.forEach(plugin => plugin(this))
  50. // 是否使用 devtool
  51. const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
  52. if (useDevtools) {
  53. devtoolPlugin(this)
  54. }
  55. }
  56. }

构造函数中较为繁琐的几个地方如下:

  • 初始化 module,即 (this._modules = new ModuleCollection(options))
  • 递归安装整个模块,包括actions、mutations、wrappedGetters,即(installModule(this, state, [], this._modules.root))

2.2.1 初始化 module

我们知道,Vuex 在使用时,在工程逐渐庞大后,必然会进行多层 module 的嵌套来进行解耦使用。具体如何实现,让我们一起看看构造函数中的 new ModuleCollection(options) 方法。

  1. import ChildMod from './modules/ChildMod'
  2. export default new Vuex.Store({
  3. modules: {
  4. ChildMod,
  5. },
  6. state: {},
  7. getters: {},
  8. mutations: {},
  9. actions: {},
  10. strict: debug,
  11. })

首先 options 便是该函数中的这个对象,接着 ModuleCollection 的构造函数中,主要调用的是以下几个方法:

  1. // /src/module/module-collection.js
  2. export default class ModuleCollection {
  3. constructor (rawRootModule) {
  4. // 递归注册 modules, 建立一个树状结构的 modules
  5. this.register([], rawRootModule, false)
  6. }
  7. // 注册方法
  8. register (path, rawModule, runtime = true) {
  9. if (__DEV__) {
  10. assertRawModule(path, rawModule)
  11. }
  12. // 实例化 module 对象
  13. const newModule = new Module(rawModule, runtime)
  14. if (path.length === 0) {
  15. // 建立根 module
  16. this.root = newModule
  17. } else {
  18. // 获取当前 module 的父级 module
  19. const parent = this.get(path.slice(0, -1))
  20. // 下级,即 _children 对象增加对应的 key-module 键值对
  21. parent.addChild(path[path.length - 1], newModule)
  22. }
  23. // 注册嵌套的 modules
  24. if (rawModule.modules) {
  25. // 遍历当前对象中的 modules 对象,进行逐个注册,
  26. forEachValue(rawModule.modules, (rawChildModule, key) => {
  27. this.register(path.concat(key), rawChildModule, runtime)
  28. })
  29. }
  30. }
  31. get (path) {
  32. return path.reduce((module, key) => {
  33. return module.getChild(key)
  34. }, this.root)
  35. }
  36. }

以下为 Module 类中的方法

  1. // /src/module/module.js
  2. export default class Module {
  3. constructor (rawModule, runtime) {
  4. this.runtime = runtime
  5. // 用于保存子 module
  6. this._children = Object.create(null)
  7. this._rawModule = rawModule
  8. const rawState = rawModule.state
  9. this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  10. }
  11. addChild (key, module) {
  12. this._children[key] = module
  13. }
  14. getChild (key) {
  15. return this._children[key]
  16. }
  17. getNamespace (path) {
  18. let module = this.root
  19. return path.reduce((namespace, key) => {
  20. module = module.getChild(key)
  21. return namespace + (module.namespaced ? key + '/' : '')
  22. }, '')
  23. }
  24. }

到此为止,我们 Store 类中的 this._module 便完成了树状结构的初始化构建

2.2.2 递归安装整个模块

上一节完成了对 module 的初始化,接着我们需要继续对 Vuex 的actions、mutations、wrappedGetters来进行初始化,让我们一起来看一下 installModule(this, state, [], this._modules.root) 方法

  1. // /src/store.js
  2. function installModule (store, rootState, path, module, hot) {
  3. const isRoot = !path.length
  4. // 获取拼接好的 namespace,若模块的 namespace 设为 true 则进行拼接
  5. const namespace = store._modules.getNamespace(path)
  6. // 注册 namespace 集合
  7. if (module.namespaced) {
  8. store._modulesNamespaceMap[namespace] = module
  9. }
  10. // set state
  11. if (!isRoot && !hot) {
  12. const parentState = getNestedState(rootState, path.slice(0, -1))
  13. const moduleName = path[path.length - 1]
  14. store._withCommit(() => {
  15. Vue.set(parentState, moduleName, module.state)
  16. })
  17. }
  18. // 获取当前模块上下文 context,具体下面即进行说明
  19. const local = module.context = makeLocalContext(store, namespace, path)
  20. module.forEachMutation((mutation, key) => {
  21. const namespacedType = namespace + key
  22. registerMutation(store, namespacedType, mutation, local)
  23. })
  24. module.forEachAction((action, key) => {
  25. const type = action.root ? key : namespace + key
  26. const handler = action.handler || action
  27. registerAction(store, type, handler, local)
  28. })
  29. module.forEachGetter((getter, key) => {
  30. const namespacedType = namespace + key
  31. registerGetter(store, namespacedType, getter, local)
  32. })
  33. module.forEachChild((child, key) => {
  34. installModule(store, rootState, path.concat(key), child, hot)
  35. })
  36. }

以上有一个非常重要的 local 参数,来帮助我们在使用 context 时,获取的是当前 module 的 context,即在 namespace 设为 true 时,调用 dispatch, commit, getters 和 state 等,调用的均为当前模块的方法或变量。具体实现我们一起看看 makeLocalContext(store, namespace, path) 函数

  1. function makeLocalContext (store, namespace, path) {
  2. const noNamespace = namespace === ''
  3. const local = {
  4. dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
  5. // 处理使用 dispatch 时,采用对象方式分发情况
  6. const args = unifyObjectStyle(_type, _payload, _options)
  7. const { payload, options } = args
  8. let { type } = args
  9. // 解决调用方法模块化的重点即在此
  10. // 使用 dispatch 调用方法时,vuex 对 namespace 进行了拼接,使用者无需再手动写上 namespace 路径
  11. if (!options || !options.root) {
  12. type = namespace + type
  13. }
  14. return store.dispatch(type, payload)
  15. },
  16. // commit 处理方式与以上 dispatch 同理
  17. commit: noNamespace ? store.commit : (_type, _payload, _options) => {
  18. const args = unifyObjectStyle(_type, _payload, _options)
  19. const { payload, options } = args
  20. let { type } = args
  21. if (!options || !options.root) {
  22. type = namespace + type
  23. }
  24. store.commit(type, payload, options)
  25. }
  26. }
  27. // getters 和 state 的获取也与 dispatch 和 commit 有异曲同工之妙,这里不做多述
  28. // 区别在于 getters 和 state 需要调用 Object.defineProperty 方法对值进行绑定
  29. Object.defineProperties(local, {
  30. getters: {
  31. get: noNamespace
  32. ? () => store.getters
  33. : () => makeLocalGetters(store, namespace)
  34. },
  35. state: {
  36. get: () => getNestedState(store.state, path)
  37. }
  38. })
  39. return local
  40. }