Vuex 源码分析

本文解读的Vuex版本为2.3.1javascript

Vuex代码结构

Vuex的代码并很少,但麻雀虽小,五脏俱全,下面来看一下其中的实现细节。vue

源码分析

入口文件

入口文件src/index.js:java

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

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}复制代码

这是Vuex对外暴露的API,其中核心部分是Store,而后是install,它是一个vue插件所必须的方法。Store
和install都在store.js文件中。mapState、mapMutations、mapGetters、mapActions为四个辅助函数,用来将store中的相关属性映射到组件中。react

install方法

Vuejs的插件都应该有一个install方法。先看下咱们一般使用Vuex的姿式:vuex

import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)复制代码

install方法的源码:编程

export function install (_Vue) {
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}复制代码

方法的入参_Vue就是use的时候传入的Vue构造器。
install方法很简单,先判断下若是Vue已经有值,就抛出错误。这里的Vue是在代码最前面声明的一个内部变量。数组

let Vue // bind on install复制代码

这是为了保证install方法只执行一次。
install方法的最后调用了applyMixin方法。这个方法定义在src/mixin.js中:浏览器

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { 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 = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}复制代码

方法判断了一下当前vue的版本,当vue版本>=2的时候,就在Vue上添加了一个全局mixin,要么在init阶段,要么在beforeCreate阶段。Vue上添加的全局mixin会影响到每个组件。mixin的各类混入方式不一样,同名钩子函数将混合为一个数组,所以都将被调用。而且,混合对象的钩子将在组件自身钩子以前。数据结构

来看下这个mixin方法vueInit作了些什么:
this.$options用来获取实例的初始化选项,当传入了store的时候,就把这个store挂载到实例的$store上,没有的话,而且实例有parent的,就把parent的$store挂载到当前实例上。这样,咱们在Vue的组件中就能够经过this.$store.xxx访问Vuex的各类数据和状态了。app

Store构造函数

Vuex中代码最多的就是store.js, 它的构造函数就是Vuex的主体流程。

constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

    const {
      plugins = [],
      strict = false
    } = options

    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    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()

    // bind commit and dispatch to self
    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)
    }

    // strict mode
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }复制代码

依然,先来看看使用Store的一般姿式,便于咱们知道方法的入参:

export default new Vuex.Store({
  state,
  mutations
  actions,
  getters,
  modules: {
    ...
  },
  plugins,
  strict: false
})复制代码

store构造函数的最开始,进行了2个判断。

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是util.js里的一个方法。

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

先判断一下Vue是否存在,是为了保证在这以前store已经install过了。另外,Vuex依赖Promise,这里也进行了判断。
assert这个函数虽然简单,但这种编程方式值得咱们学习。
接着往下看:

const {
  plugins = [],
  strict = false
} = options

let {
  state = {}
} = options
if (typeof state === 'function') {
  state = state()
}复制代码

这里使用解构并设置默认值的方式来获取传入的值,分别获得了plugins, strict 和state。传入的state也能够是一个方法,方法的返回值做为state。

而后是定义了一些内部变量:

// store internal state
this._committing = false
this._actions = Object.create(null)
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._committing 表示提交状态,做用是保证对 Vuex 中 state 的修改只能在 mutation 的回调函数中,而不能在外部随意修改state。
this._actions 用来存放用户定义的全部的 actions。
this._mutations 用来存放用户定义全部的 mutatins。
this._wrappedGetters 用来存放用户定义的全部 getters。
this._modules 用来存储用户定义的全部modules
this._modulesNamespaceMap 存放module和其namespace的对应关系。
this._subscribers 用来存储全部对 mutation 变化的订阅者。
this._watcherVM 是一个 Vue 对象的实例,主要是利用 Vue 实例方法 $watch 来观测变化的。
这些参数后面会用到,咱们再一一展开。

继续往下看:

// bind commit and dispatch to self
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)
}复制代码

如同代码的注释同样,绑定Store类的dispatch和commit方法到当前store实例上。dispatch 和 commit 的实现咱们稍后会分析。this.strict 表示是否开启严格模式,在严格模式下会观测全部的 state 的变化,建议在开发环境时开启严格模式,线上环境要关闭严格模式,不然会有必定的性能开销。

构造函数的最后:

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))复制代码
Vuex的初始化核心

installModule

使用单一状态树,致使应用的全部状态集中到一个很大的对象。可是,当应用变得很大时,store 对象会变得臃肿不堪。

为了解决以上问题,Vuex 容许咱们将 store 分割到模块(module)。每一个模块拥有本身的 state、mutation、action、getters、甚至是嵌套子模块——从上至下进行相似的分割。

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)复制代码

在进入installModule方法以前,有必要先看下方法的入参this._modules.root是什么。

this._modules = new ModuleCollection(options)复制代码

这里主要用到了src/module/module-collection.js 和 src/module/module.js

module-collection.js:

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.root = new Module(rawRootModule, false)

    // register all nested modules
    if (rawRootModule.modules) {
      forEachValue(rawRootModule.modules, (rawModule, key) => {
        this.register([key], rawModule, false)
      })
    }
  }
  ...
}复制代码

module-collection的构造函数里先定义了实例的root属性,为一个Module实例。而后遍历options里的modules,依次注册。

看下这个Module的构造函数:

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  ...
}复制代码

这里的rawModule一层一层的传过来,也就是new Store时候的options。
module实例的_children目前为null,而后设置了实例的_rawModule和state。

回到module-collection构造函数的register方法, 及它用到的相关方法:

register (path, rawModule, runtime = true) {
  const parent = this.get(path.slice(0, -1))
  const newModule = new Module(rawModule, runtime)
  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)
    })
  }
}

get (path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}

addChild (key, module) {
  this._children[key] = module
}复制代码

get方法的入参path为一个数组,例如['subModule', 'subsubModule'], 这里使用reduce方法,一层一层的取值, this.get(path.slice(0, -1))取到当前module的父module。而后再调用Module类的addChild方法,将改module添加到父module的_children对象上。

而后,若是rawModule上有传入modules的话,就递归一次注册。

看下获得的_modules数据结构:

扯了一大圈,就是为了说明installModule函数的入参,接着回到installModule方法。

const isRoot = !path.length
const namespace = store._modules.getNamespace(path)复制代码

经过path的length来判断是否是root module。

来看一下getNamespace这个方法:

getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}复制代码

又使用reduce方法来累加module的名字。这里的module.namespaced是定义module的时候的参数,例如:

export default {
  state,
  getters,
  actions,
  mutations,
  namespaced: true
}复制代码

因此像下面这样定义的store,获得的selectLabelRule的namespace就是'selectLabelRule/'

export default new Vuex.Store({
  state,
  actions,
  getters,
  mutations,
  modules: {
    selectLabelRule
  },
  strict: debug
})复制代码

接着看installModule方法:

// register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }复制代码

传入了namespaced为true的话,将module根据其namespace放到内部变量_modulesNamespaceMap对象上。

而后

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, module.state)
  })
}复制代码

getNestedState跟前面的getNamespace相似,也是用reduce来得到当前父module的state,最后调用Vue.set将state添加到父module的state上。

看下这里的_withCommit方法:

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}复制代码

this._committing在Store的构造函数里声明过,初始值为false。这里因为咱们是在修改 state,Vuex 中全部对 state 的修改都会用 _withCommit函数包装,保证在同步修改 state 的过程当中 this._committing 的值始终为true。这样当咱们观测 state 的变化时,若是 this._committing 的值不为 true,则能检查到这个状态修改是有问题的。

看到这里,可能会有点困惑,举个例子来直观感觉一下,以 Vuex 源码中的 example/shopping-cart 为例,打开 store/index.js,有这么一段代码:

export default new Vuex.Store({
  actions,
  getters,
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})复制代码

这里有两个子 module,cart 和 products,咱们打开 store/modules/cart.js,看一下 cart 模块中的 state 定义,代码以下:

const state = {
  added: [],
  checkoutStatus: null
}复制代码

运行这个项目,打开浏览器,利用 Vue 的调试工具来看一下 Vuex 中的状态,以下图所示:

来看installModule方法的最后:

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 namespacedType = namespace + key
  registerAction(store, namespacedType, action, 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)
})复制代码

local为接下来几个方法的入参,咱们又要跑偏去看一下makeLocalContext这个方法了:

/** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */
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 (!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 (!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
}复制代码

就像方法的注释所说的,方法用来获得局部的dispatch,commit,getters 和 state, 若是没有namespace的话,就用根store的dispatch, commit等等

以local.dispath为例:
没有namespace为''的时候,直接使用this.dispatch。有namespace的时候,就在type前加上namespace再dispath。

local参数说完了,接来是分别注册mutation,action和getter。以注册mutation为例说明:

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})复制代码
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler(local.state, payload)
  })
}复制代码

根据mutation的名字找到内部变量_mutations里的数组。而后,将mutation的回到函数push到里面。
例若有这样一个mutation:

mutation: {
  increment (state, n) {
    state.count += n
  }
}复制代码

就会在_mutations[increment]里放入其回调函数。

commit

前面说到mutation被放到了_mutations对象里。接下来看一下,Store构造函数里最开始的将Store类的dispatch和commit放到当前实例上,那commit一个mutation的执行状况是什么呢?

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]
    if (!entry) {
      console.error(`[vuex] unknown mutation type: ${type}`)
      return
    }
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub => sub(mutation, this.state))

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

方法的最开始用unifyObjectStyle来获取参数,这是由于commit的传参方式有两种:

store.commit('increment', {
  amount: 10
})复制代码

提交 mutation 的另外一种方式是直接使用包含 type 属性的对象:

store.commit({
  type: 'increment',
  amount: 10
})复制代码
function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

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

  return { type, payload, options }
}复制代码

若是传入的是对象,就作参数转换。
而后判断须要commit的mutation是否注册过了,this._mutations[type],没有就抛错。
而后循环调用_mutations里的每个mutation回调函数。
而后执行每个mutation的subscribe回调函数。

Vuex辅助函数

Vuex提供的辅助函数有4个:

以mapGetters为例,看下mapGetters的用法:

代码在src/helpers.js里:

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (!(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
})


function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

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)
  }
}复制代码

normalizeNamespace方法使用函数式编程的方式,接收一个方法,返回一个方法。
mapGetters接收的参数是一个数组或者一个对象:

computed: {
// 使用对象展开运算符将 getters 混入 computed 对象中
  ...mapGetters([
    'doneTodosCount',
    'anotherGetter',
    // ...
  ])
}复制代码
mapGetters({
  // 映射 this.doneCount 为 store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})复制代码

这里是没有传namespace的状况,看下方法的具体实现。
normalizeNamespace开始进行了参数跳转,传入的数组或对象给map,namespace为'' , 而后执行fn(namespace, map)
接着是normalizeMap方法,返回一个数组,这种形式:

{
  key: doneCount,
  val: doneTodosCount
}复制代码

而后往res对象上塞方法,获得以下形式的对象:

{
  doneCount: function() {
    return this.$store.getters[doneTodosCount]
  }
}复制代码

也就是最开始mapGetters想要的效果:

by kaola/fangwentian

相关文章
相关标签/搜索