# 源码阅读vuex

# Vue.use

在vue项目中使用路由时,需要通过Vue.use(Vuex)注册状态管理。

在vue源码文件vue/src/core/index.js (opens new window)中,调用了initGlobalAPI(Vue)方法, 在vue/src/core/global-api/index.js (opens new window)中,定义并导出了initGlobalAPI方法, 在initGlobalAPI方法中调用了initUse(Vue),在vue/src/core/global-api/use.js (opens new window)中,找到initUse, 在这里定义了Vue.use

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 获取已经注册的插件列表
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 避免重复注册
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // 获取其他参数
    const args = toArray(arguments, 1)
    // 向参数数组首部推入当前vue实例
    args.unshift(this)
    // 当组件有install方法时调用install,或者当组件本身是一个可调用的方法时,调用组件方法,以此完成注册动作
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 向已注册数组增加注册组件
    installedPlugins.push(plugin)
    
    return this
  }
}

vue/src/shared/util.js (opens new window)中,找到toArray方法,用于将一个类数组对象转换成数组。

export function toArray (list: any, start?: number): Array<any> {
  start = start || 0
  let i = list.length - start
  const ret: Array<any> = new Array(i)
  while (i--) {
    ret[i] = list[i + start]
  }
  return ret
}

# install + applyMixin

在vuex源码vuex/src/store.js (opens new window)中导出了install方法。

import applyMixin from './mixin'
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
  applyMixin(Vue)
}

install 做了重复注册校验,并调用了applyMixin方法,applyMixin来自vuex/src/mixin.js (opens new window)

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.
   */

  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
    }
  }
}

applyMixin 做了一个版本判断,如果vue版本大于等于2,则在vue的beforeCreate生命周期混入一个vuexInit方法,如果版本小于2,则重写了vue的_init方法,将vuexInit加到_init参数options的init属性上,然后执行原来的_init内容。

vuexInit的内容也很简单,判断当前vue实例上有没有store,有的话就将$store指向this.$options.store,如果这个store是一个方法,就执行拿到它的结果再赋值,如果当前vue实例没有store,就从父组件拿。这就保证了全局只有一个sotre,达到所有组件共用状态的目的。

# Store

来看一下通常我们是怎么使用vuex的:

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})
new Vue({
  el: '#app',
  store
})

可以看到Vue实例化传入的store对象,是通过new Vuex.Store({})实例化生成的,那么接下来,就看一下Store构造函数。

源码跟install方法在同一个文件中:vuex/src/store.js (opens new window)

export class Store {
  constructor (options = {}) {
    // 如果还未注册,在有window.Vue的情况下,会自动注册
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
    // 开发环境下的一些报错
    if (__DEV__) {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)
    }

    const {
      // 插件数组,vuex 插件暴露出每次 mutation 的钩子
      plugins = [],
      // 严格模式设置,在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。
      strict = false
    } = options

    // 各种内部属性初始化
    this._committing = false // 用来判断严格模式下是否是用mutation修改state
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options) // 从根module开始注册每一个module,并且在这里进行实例化
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue() // 实例化一个vue,来使用侦听器
    this._makeLocalGettersCache = Object.create(null)

    // 为dispatch、commit绑定当前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

    // state指向根module的state属性
    const state = this._modules.root.state

    // 初始化根module,递归注册所有子modle
    // 在 registerGetter 方法中会收集所有module的getter到 _wrappedGetters
    installModule(this, state, [], this._modules.root)

    // 通过vm重设store,新建Vue实例使用Vue内部的响应式实现注册state以及computed
    resetStoreVM(this, state)

    // 调用插件,Vuex 插件就是一个函数,它接收 store 作为唯一参数
    plugins.forEach(plugin => plugin(this))

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

  // get、set、一堆其他方法
}

ModuleCollection方法在vuex/src/module/module-collection.js (opens new window)

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }

    // 实例化Module
    const newModule = new Module(rawModule, runtime)
    // 将根module挂到 store._modules.root上
    if (path.length === 0) {
      this.root = newModule
    } 
    // 对于子module
    else {
      // 给父module添加子module
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 递归注册嵌套子module
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  // ... 其他方法
}

Module类在vuex/src/module/module.js (opens new window)

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) || {}
  }
  getChild (key) {
    return this._children[key]
  }
  addChild (key, module) {
    this._children[key] = module
  }
  // ... 其他方法
}

关于插件的部分可以看APIvuex插件 (opens new window)

另外可以看到构造函数主要执行了installModule和resetStoreVM方法,来分别看一下。

# installModule

Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
function installModule (store, rootState, path, module, hot) {
  // 从 Store 构造函数中,我们可以看到根module传入的path是一个空数组
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path) // 获取当前module的命名空间

  if (module.namespaced) {
    // 防止重复命名空间
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    // 在_modulesNamespaceMap中存数 namespace - module 键值对
    store._modulesNamespaceMap[namespace] = module
  }

  // 设置子state
  if (!isRoot && !hot) {
    // 获取path中最后一个module的父state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 获取path中最后一个module的名称
    const moduleName = path[path.length - 1]
    // 使用_withCommit执行回调是为了避免enableStrictMode设置的观察者触发回调报错,在resetStoreVM的部分会进行设置
    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('.')}"`
          )
        }
      }
      // 关联父子state,通过Vue.set将module设置成响应式的
      Vue.set(parentState, moduleName, module.state)
    })
  }

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

  // 遍历注册mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  // 遍历注册action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  // 遍历注册getter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  // 遍历子module,递归调用installModule注册所有子modle
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
function getNestedState (state, path) {
  return path.reduce((state, key) => state[key], state)
}
  • forEachXXX

这里的每一个forEachXXX方法都接收了一个回调函数,都是调用的forEachValue方法,这个方法在vuex/src/util.js (opens new window)里。

export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

这个方法对Object实例的每个属性遍历调用回调,传入值和属性名。

也就是说,遍历子module递归调用installModule时,path数组拼接的其实就是module的名称,也就是上面modules: { a: moduleA, b: moduleB }的a、b。

  • registerMutation
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = []) // 获取或初始化当前namespace + key构成的type的_mutations数组,推入包装过的mutation函数
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}
  • registerAction
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []) // 获取或初始化当前namespace + key构成的type的_actions数组,推入包装过的actions函数
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // action内部执行异步操作,不是Promise对象时,需要用Promise.resolve包装结果
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    //  使用了devtool插件时,需要把error提交给插件
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}
  • registerGetter
function registerGetter (store, type, rawGetter, local) {
  // 重复getter校验
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 把每个getter包装一层方法后,挂载到store._wrappedGetters对象上
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
  • getNestedState

对于非根module的情况,getNestedState方法接收根state和path数组下标0~length-1的数组,通过reduce方法,从根state开始嵌套查找子state,该方法最终用于返回实际path最后一个module的父state。

  • makeLocalContext 为同一个命名空间初始化自己的 state、mutation、action、getter

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  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)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
  • unifyObjectStyle 用于格式化参数
function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (__DEV__) {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}

Vuex.Store 实例方法 (opens new window)中,我们可以看到dispatch和commit的传参有两种格式,unifyObjectStyle方法用于标准化参数对象。

api

# resetStoreVM

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

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

  // 遍历
  forEachValue(wrappedGetters, (fn, key) => {
    // 将每个wrappedGetter函数包装成()=>fn(store),并挂载到computed对象上
    computed[key] = partial(fn, store)
    // 通过Object.defineProperty重写getters,使得当我们使用例如:this.$store.getters.user时,是通过store._vm[user]来获取的
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true 
    })
  })

  // 创建一个vue实例,来保存state,并将上面的computed设置为计算属性
  // 在实例化vue期间将Vue.config.silent设为true,是为了避免实例化过程中的警告
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // 严格模式下,只能通过mutation修改state
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // 使用_withCommit避免enableStrictMode设置的观察者回调报错,解除$$state对state的引用
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    // 销毁旧vue实例
    Vue.nextTick(() => oldVm.$destroy())
  }
}
  • partial

partial方法在vuex/src/util.js (opens new window)里。

export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}
  • enableStrictMode
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 })
}

通过vue的$watch创建一个watcher,当$$state的set被触发时,会触发notify通知watcher调用回调,也就是上面的第二个入参,当_committing为true,会报do not mutate vuex store state outside mutation handlers.

  • 在回过来看Store的 get、set、_withCommit 就很好理解了
export class Store {
  constructor (options = {}) {
    // ...
  }

  get state () {
    return this._vm._data.$$state
  }

  set state (v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

  // 在_committing为true的情况下执行回调,然后回复_committing
  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
}

# Store 的其他方法

# commit 和 subscribe

commit (_type, _payload, _options) {
  // check object-style commit
  // 格式化参数
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type] // 获取type对应的wrappedMutationHandler数组
  // 开发环境无当前type报错
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  // 使用_withCommit避免enableStrictMode设置的观察者回调报错
  this._withCommit(() => {
    // 遍历执行当前type的_mutations数组的每一个wrappedMutationHandler
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  // 通知所有订阅者 
  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'
    )
  }
}
subscribe (fn, options) {
  return genericSubscribe(fn, this._subscribers, options)
}
function genericSubscribe (fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    // prepend 配置执行顺序
    options && options.prepend
      ? subs.unshift(fn)
      : subs.push(fn)
  }
  // 返回的函数用于停止订阅
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

关于subscribe (opens new window)

subscribe(handler: Function, options?: Object): Function

订阅 store 的 mutation,handler 会在每个 mutation 完成后调用。

# dispatch 和 subscribeAction

dispatch (_type, _payload) {
  // check object-style dispatch
  // 格式化参数
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  const entry = this._actions[type] // 获取type对应的wrappedActionHandler数组
  // 开发环境无当前type报错
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }

  try {
    this._actionSubscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .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)
    }
  }

  // type对应的wrappedActionHandler数组有多个处理函数时,要用Promise.all处理全部异步函数
  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

  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)
    })
  })
}
subscribeAction (fn, options) {
  const subs = typeof fn === 'function' ? { before: fn } : fn
  return genericSubscribe(subs, this._actionSubscribers, options)
}

关于subscribeAction (opens new window)

# watch

watch (getter, cb, options) {
  // getter 必须为函数
  if (__DEV__) {
    assert(typeof getter === 'function', `store.watch only accepts a function.`)
  }
  // 利用Vue的watch侦听器,对getter进行响应式的侦听
  return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}