vuex源码解析

282 阅读10分钟

date: 2021-11-15 19:20:44
tags: vuex 源码解析
author: coder@mc

版本说明

此博客主要是针对 vuex@3.6.2做一次源码解析,主要研究以下几点:

  1. vuex的初始化过程
  2. dispatchcommit的实现
  3. 辅助函数mapStatemapGettersmapActionsmapMutations createNamespacedHelpers的实现
  4. 插件的补充

思维导图

Vuex源码解析

一、初始化过程

​ 我们平时使用 vuex 时,可以通过 import {Store} from 'vuex' 或者 import vuex from 'vuex'的方式导入,了解过 ES6 import 导入模块的同学,相信一眼就能想到 vuex 中入口文件必然是使用了默认导出和大括号导出,它的源码在src/index.js:

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'

// 对应 import vuex from 'vuex'
export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

// 对应 import {Store} from 'vuex'
export {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

其中, Store 是我们重点研究的内容,因为初始化是在 Store 构造函数中实现,另外一个需要关注的是 install函数,因为 vuex 是一个插件,想要在项目中使用,必须通过 Vue.use(vuex) 的方式注册,读过vue官方文档的都知道,Vue.use(option) 传递的参数必须是函数或者是包含 install方法的对象。

1.依赖安装

// Vue.use 安装插件时使用的
export function install(_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }

  // 缓存 Vue 供全局使用
  Vue = _Vue
  // 混入`beforeCreate`,使每个组件都能通过 `this.$store` 访问到 `store` 实例
  applyMixin(Vue)
}

该源码在src/store.js,做了三件事,首先,判断是否已经依赖过了,依赖过了直接打印错误返回;其次,缓存 Vue 供全局使用;最后,通过混入beforeCreate方式,使每个组件都能通过 this.$store 访问到 store 实例,它的源码在src/mixin.js

/**
 * 分别对 Vue 1 和 2 版本进行处理
 * 1. 针对 Vue2 版本,每个组件的 beforeCreate 生命周期都挂载一个 vuexInit 函数
 * 2. 针对 Vue1 版本,重写原型 _init 函数
 * @param {*} Vue 
 */
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  // 将 store 挂载到 $store 上,使得每个组件可以通过 this.$store 访问
  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
    }
  }
}

2. Store 构造函数

该构造函数是创建vuex实例的核心方法,其源码位于src/store.js,接下来,具体看初始化过程都做了哪些事。

2.1 异常检测

如果是在开发环境,会做一些检查:

// 异常检测
if (__DEV__) {
  // 必须通过 Vue.use 调用 Vuex 
  assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
  // Promise 必须兼容
  assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
  // 必须通过 new 来实例化 Store
  assert(this instanceof Store, `store must be called with the new operator.`)
}

export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}

2.2 初始化一些内部变量

定义了一些内部变量,绑定在 this 上下文中:

// 初始化内部变量
const {
  plugins = [],
  strict = false
} = options

// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
// ModuleCollection 内部会递归注册所有的子模块
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
// 包装 dispatch 和 commit,确保 this 始终指向 store 实例
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

其中有两点需要强调一下,下面一一讲解。

2.2.1 构造 modules

this._modules = new ModuleCollection(options)

通过 new ModuleCollection(options)得到modules,其内部会递归注册所有的子模块。最终,得到了一个这样的数据结构:

{
	root: {
		runtime: false,
		state: {...},
		_rawModule: {
		  actions: {...},
		  getters: {...},
		  mutations: {...},
		  plugins: [...],
		  state: {...}
		},
		_children: {
	       subModule1: {runtime: false, state: {...}, _rawModule: {...}, ...},
		   subModule2: {runtime: false, state: {...}, _rawModule: {...}, ...}
		},
		namespaced: false
	}
}

接下来,详细看看是如何得到这一数据结构的,先来看 ModuleCollection 构造函数,该码在./src/module/module-collection.js中:

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  // 将 module 实例放到 root 属性上,存在子模块则递归调用
  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }

    // 返回一个 {runtime, _children, _rawModule, state}
    const newModule = new Module(rawModule, runtime)

    if (path.length === 0) {
      // 根组件挂载
      this.root = newModule
    } else {
      // 子组件放在 module._children 上
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    // 存在子模块,递归调用该函数
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

rawRootModule其实就是 new vuex.Store(options)中的options,如果options.modules存在,会递归调用register这个方法。而基础数据结构是在 Module 类中,该构造函数在./src/module/module.js

// 存储模块的基本数据结构,包与一些属性和方法
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  
   addChild (key, module) {
    this._children[key] = module
  }
}

2.2.2 封装 dispatchcommit

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始终指向store实例。

2.3 初始化 module

// 初始化 module,存在子模块则递归调用,收集所有的模块 getters 放进 _wrappedGetters 里
installModule(this, state, [], this._modules.root)

只需要传入上面_modules.root,方法内部会自己检测是否存在子模块,若存在子模块递归初始化,否则只初始化根模块。installModule 的实现可以分为四部分,下面一一介绍。

  1. 获取到命名空间的名称的名称namespace,将module映射到store._modulesNamespaceMap[namespace]上:

    // 通过 getNamespace 拿到命名空间的名称,其实就是在模块名后面加 /,根节点 namespace 是 '',
    // 例如:subModule/
    const namespace = store._modules.getNamespace(path)
    
    // 做映射
    if (module.namespaced) {
      if (store._modulesNamespaceMap[namespace] && __DEV__) {
        console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
       }
      store._modulesNamespaceMap[namespace] = module
    }
    
  2. 通过 Vue.set方法将子模块的 state 设置到父模块的 state

      const isRoot = !path.length
      
      if (!isRoot && !hot) {
        const parentState = getNestedState(rootState, path.slice(0, -1))
        const moduleName = path[path.length - 1]
        store._withCommit(() => {
          if (__DEV__) {
            if (moduleName in parentState) {
              console.warn(
                `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
              )
            }
          }
          Vue.set(parentState, moduleName, module.state)
        })
      }
    
  3. 构造当前模块的上下文,也就是 actionctx 参数。

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

    makeLocalContext的实现很简单,首先,定义了 local对象,初始化时根据命名空间的名称定义 dispatchcommit两个函数;其次,通过Object.defineProperties方法响应式设置了gettersstate属性,其代码如下:

    function makeLocalContext(store, namespace, path) {
      const noNamespace = namespace === ''
      // 据命名空间的名称定义 `dispatch`和`commit`两个函数  	
      const local = {
        dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options)
          const { payload, options } = args
          let { type } = args
    
          if (!options || !options.root) {
            type = namespace + type
            if (__DEV__ && !store._actions[type]) {
              console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
              return
            }
          }
    
          return store.dispatch(type, payload)
        },
    
        commit: noNamespace ? store.commit : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options)
          const { payload, options } = args
          let { type } = args
    
          if (!options || !options.root) {
            type = namespace + type
            if (__DEV__ && !store._mutations[type]) {
              console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
              return
            }
          }
    
          store.commit(type, payload, options)
        }
      }
    
      // 通过`Object.defineProperties`方法响应式设置了`getters`和`state`属性
      Object.defineProperties(local, {
        getters: {
          get: noNamespace
            ? () => store.getters
            : () => makeLocalGetters(store, namespace)
        },
        state: {
          get: () => getNestedState(store.state, path)
        }
      })
    
      return local
    }
    
  4. mutations 的每一项放到 store._mutations 中,将 actions 的每一项放到 store._actions ,将 getters 的每一项放到 store._wrappedGetters

     // 将 mutations 的每一项放到 store._mutations 中
      module.forEachMutation((mutation, key) => {
        const namespacedType = namespace + key
        registerMutation(store, namespacedType, mutation, local)
      })
    
      // 将 actions 的每一项放到 store._actions 中
      module.forEachAction((action, key) => {
        const type = action.root ? key : namespace + key
        const handler = action.handler || action
        registerAction(store, type, handler, local)
      })
    
      // 将 getters 中的每项放到 store._wrappedGetters 中
      module.forEachGetter((getter, key) => {
        const namespacedType = namespace + key
        registerGetter(store, namespacedType, getter, local)
      })
    

    上面的代码其实很简单,以mutation 为例,遍历_rawModule.mutations拿到每项mutationvaluekey,调用registerMutation函数,将value做了层封装存储到store._mutations[key]中:

    // 将 mutation 存放到 store._mutations 里面
    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)
      })
    }
    

    同理,actionsgetters的处理也与mutations相似。

  5. 存在子模块递归调用

    // 递归处理 modules 里面的数据
    module.forEachChild((child, key) => {
      installModule(store, rootState, path.concat(key), child, hot)
    })
    

2.4 初始化stategetters

​ 初始化完module以后就会处理state数据将其变为响应式数据,并且也会处理前面经过包装的getters,让其变为computed

resetStoreVM(this, state)

下面是 resetStoreVM 的实现:

// 1. 处理 state 里面的数据,变成响应式数据
// 2. 处理前面包装的 getter,使它变成类似 computed 意义的东西
function resetStoreVM(store, state, hot) {
  const oldVm = store._vm

  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  // 遍历 store 里面的 getters 属性,getter 本质其实是 computed
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    // 通过数据劫持,让其访问到 store.state 数据
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // silent 取消 Vue 的所有日志与警告
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  // 对 store._vm 启用严格模式
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

2.4.1 处理 state

通过new Vue实例让 state变为响应式

store._vm = new Vue({
  data: {
    $$state: state
  },
  computed
})

// Store 构造函数中有取 state 的代码,其实就是访问的 this._vm._data.$$state 的值
export class Store {
  ...
  get state() {
   	return this._vm._data.$$state
  }
}

2.4.2 处理 getters

  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}

 // 遍历 store 里面的 getters 属性,getter 本质其实是 computed
 forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    // 通过数据劫持,让其访问到 store.state 数据
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
 })

可以看到遍历getters数据,将getters数据放到computed中,然后通过Object.defineProperty数据劫持,实际访问的是store._vm[key]的值。

2.5 调用所有插件

遍历plugins,依次执行传入过来的插件,并将this当做参数传递。

// 调用所有插件,传入 this
plugins.forEach(plugin => plugin(this))

2.6 浏览器 vuex 的调试模式数据变化

// devtools 
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
  devtoolPlugin(this)
}

以上就是 vuex 的初始化的大致过程。

二、dispatchcommit

​ 看完了 vuex 的初始化过程,接下来,了解的就是如何改变 state 的数据,看过 Vuex 官方文档的同学,肯定对下图十分了解,这是Vuex 改变数据的官方步骤:

vuex

dispatch(actions) -> commit(mutations) -> 改变 state -> 更新视图

1. dispatch 方法

一般我们会使用下面方式调用dispatch 函数:

this.$store.dispatch('subModule/action', payload);

首先会进入前面看到的包装后的 dispatch 函数:

this.dispatch = function boundDispatch(type, payload) {
    return dispatch.call(store, type, payload)
}

然后调用真正的 dispatch 方法:

  /**
   * 主要做了 三件事
   * 1. 解析 dispatch 的参数,并定义 action 对象
   * 2. 从 初始化 module 中取出 对应的 action 方法,并执行
   * 3. 返回 promise
   * @param {*} _type action
   * @param {*} _payload payload
   */
  dispatch(_type, _payload) {
    // _type 可以是对象也可以是字符串,但是必须要有 type 属性,
    // 例如:1. _type = {type: 'action', xxx},返回值 type === _type.type, payload === _type
    //       2. _type = 'action', 返回值 _type = type, payload === payload 
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    // action 最终是对象形式
    const action = { type, payload }
    // 从 初始化 module 中取出 对应的 action
    const entry = this._actions[type]

    // 异常检测
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    // 调用 浏览器 Vuex 调试模式函数,让插件中的数据改变
    try {
      this._actionSubscribers
        .slice() // 浅拷贝,以防止在订阅者同步调用取消订阅时迭代器失效
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (__DEV__) {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

    // 调用对应的 action 的方法,走 commit 函数
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    // 返回 promise
    return new Promise((resolve, reject) => {
      result.then(res => {
        try {
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in after action subscribers: `)
            console.error(e)
          }
        }
        resolve(res)
      }, error => {
        try {
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in error action subscribers: `)
            console.error(e)
          }
        }
        reject(error)
      })
    })
  }

2. commit 方法

使用写法:

this.$store.commit('mutation', action);

dispatch 一样,commit 也做了一层封装,目的也是让其 this 始终指向 store 实例,其真正调用的方法:


   /**
   * 主要做了三件事:
   * 1. 解析传入的参数,定义 mutation
   * 2. 从 初始化 module 中取出 对应的 _mutations 方法
   * 3. 因为可能存在同名的 mutation,依次调用它
   */
  commit(_type, _payload, _options) {
    // 同 dispatch 一样,_type 可以是对象也可以是字符串,_type 为对象时必须包含 type 属性
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    // 定义 mutation
    const mutation = { type, payload }

    // 获取 _mutations 数组
    const entry = this._mutations[type]

    // 异常检测,没有存在 commit(action) 对应的 mutation
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }

    // 可能存在多个同名的 mutation,依次调用它
    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {
        handler(payload)
      })
    })

    // 浏览器存在 devtools 插件,该步骤用于记录
    this._subscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .forEach(sub => sub(mutation, this.state))

    if (
      __DEV__ &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

这段代码中需要注意的是_withCommit这个函数:

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

看起来十分简单,就是将_committing变为 true并执行传递的函数,执行完后committing再变为初始值。为什么要多此一举,直接执行不好吗?其实是为了在严格模式下,如果没有通过commit函数来改变state直接会报错。

// 对 store._vm 启用严格模式
if (store.strict) {
  enableStrictMode(store)
}

// 必须通过 commit 函数来改变 state 否则会报错
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 })
}

三、辅助函数

1. mapState

官网描述:该方法为组件创建计算属性以返回 Vuex store 中的状态。该函数的源码位于src/helpers:

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  // 异常检测,mapState 传递的参数必须是 数组 或者 对象
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }

  // normalizeMap 函数作用是将 map 解析成数组 [{key, val}]
  normalizeMap(states).forEach(({ key, val }) => {
  	// 为什么是函数?因为 computed 的属性可以是函数
    res[key] = function mappedState () {
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

可以看到 mapState 方法其实是 normalizeNamespace 封装了一层方法,normalizeNamespace方法具体是干什么的?这个方法是解析命名空间的,具体实现:

/**
 * 解析命名空间
 * @param {Function} fn
 * @return {Function}
 */
function normalizeNamespace (fn) {
  return (namespace, map) => {
  	// 没有传入命名空间,会将命名空间置为空,这也就是为什么可以传一个参数的原因
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

返回一个函数,这个函数其实就是 mapState 函数,从上可以看出可以传两个参数,但是当传一个参数时,会将命名空间置为'',第一个参数作为map,作为 fn 的参数,fn是上面定义mapState变量的内置函数,该函数做了两件事,一是检测传入的state必须是数组或者对象,二是根据key值映射对象,并返回(组件可以通过{computed: ...mapState(...)}解构的原因)。

2. mapGetters

官网描述:为组件创建计算属性以返回 getter 的返回值。流程与上 mapState一致。

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  if (__DEV__ && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (__DEV__ && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

3. mapActions

官网描述:创建组件方法分发 action。流程与上 mapState一致。

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  if (__DEV__ && !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

4. mapMutations

官网描述:创建组件方法分发 action。流程与上 mapState一致。

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  if (__DEV__ && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

5. createNamespacedHelpers

官网描述:创建基于命名空间的组件绑定辅助函数。其返回一个包含 mapStatemapGettersmapActionsmapMutations 的对象。它们都已经绑定在了给定的命名空间上。

export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})

四、插件补充

vuex中可以自定义插件,这个插件其实就是一个函数,它接收 store 作为唯一参数。Vuexstore 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。

const myPlugin = store => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
  })
}

然后像这样使用:

const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

可以看看subscribe方法具体做了什么,

subscribe(fn, options) {
   return genericSubscribe(fn, this._subscribers, options)
}

function genericSubscribe(fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    options && options.prepend
      ? subs.unshift(fn)
      : subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

可以清楚的看到,该方法将传递的fn,根据options配置放到store._subscribers中,并返回一个可以删除该插件的函数。为什么说会暴露出每次 mutation 的钩子,其实是在commit方法中有这一段代码:

this._subscribers
  .slice() // 浅层复制
  .forEach(sub => sub(mutation, this.state));

到这里vuex 的源码大致走了一遍,有的细节可以自己去看,这篇博客哪里写的有问题欢提出,共同学习~

参考链接