逐行级源码分析系列(一) Vuex源码

前言

首先这篇文章也是本人第一次发这种技术文章,错别字,分析错误,不知道的东西在所难免,希望大家指正,目前本人还是一位即将大四的学生,写这个系列的目的也是为了记录在源码中学习,通过写博客让我更加熟悉了源码及其内部完完整整的实现,通过这篇文章也让我对vuex的源码变得非常熟悉,在写这完篇文章之前,因为时间原因,断断续续写了两个星期,虽然已经看完了,但是要全部分析完并写出来,太耗费精力和时间。然后这个系列我打算按照这个顺序来写,我会坚持写下来。排版可能有点差,我会慢慢学习,如果里面有错误,大佬轻喷。。

  • 逐行级源码分析系列(一) Vuex源码
  • 逐行级源码分析系列(二) Redux和React-Redux源码
  • 逐行级源码分析系列(三) Vue-Router源码
  • 逐行级源码分析系列(四) React-Router-Dom源码
  • 逐行级源码分析系列(五) Express源码
  • 逐行级源码分析系列(六) Koa核心源码
  • 逐行级源码分析系列(七) Typescript版Axios源码

install

当我们使用Vue.use会调用vuex的install方法,它的实现如下

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

这个方法传入了Vue构造函数,然后判断如果_Vue === Vue,则说明已经安装过了就直接返回,不做处理。然后调用了applyMixin(Vue)方法,我们来看下applyMixin方法实现

applyMixin

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])
  if (version >= 2) {
    // 混入beforeCreate,vuexInit方法
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    const _init = Vue.prototype._init
    // 重写_init方法,把vuexInit方法,挂载到options中
    Vue.prototype._init = function (options = {}) {
      // 这里做了兼容处理,如果有其他库也使用了init方法,就把vuexInit添加到Init数组中
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

   // 这个方法的作用就是可以让每个组件都能通过this.$store放问到store对象
  function vuexInit () {
    // 获取mergeoptios选线
    const options = this.$options
    // 如果存在store属性
    if (options.store) {
      // 如果store是一个方法,就调用store,否则直接使用
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 获取父亲的$store属性
      this.$store = options.parent.$store
    }
  }
}

其实整个函数看起来似乎有点复杂

Vue.mixin({ beforeCreate: vuexInit })

其实只是调用了这段代码,因为这是vue2.0版本及以上才有的方法,我们这里只讨论vue2.0的情况,关于mixin的用法,这里不做介绍,它为所有的组件添加beforeCreate生命周期钩子

下面我们看一下vuexInit方法的实现

   // 这个方法的作用就是可以让每个组件都能通过this.$store放问到store对象
  function vuexInit () {
    // 获取mergeoptions的选项
    const options = this.$options
    // 这段if逻辑其实实在根组件中,添加了一个store属性,并赋给this.$store
    if (options.store) {
      // 如果store是一个方法,就调用store,否则直接使用
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 获取父亲的$store属性
      this.$store = options.parent.$store
    }
  }

首先,获取了`this.store,通过这种方式,能够在组件之间形成一种链式查找,其实本质上是引用了,根组件中的store`,举个例子

new Vue({
  router,
  store,   // $store实际最终指向的都是这里的store
  render: h => h(App)
}).$mount('#app')

new Vuex.Store(options)

安装install完成之后,我们来看看new Vuex.Store(options)发生了什么,由于源码太多,就只截取构造函数中的代码,一起来看,vuex进行了哪些初始化操作

  constructor (options = {}) {
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      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 {
      plugins = [],
      strict = false //使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
    } = options


    this._committing = false   // 正在提交
    this._actions = Object.create(null)  // actions对象
    this._actionSubscribers = []  // actions订阅数组
    this._mutations = Object.create(null)  
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)  // 收集modules,
    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

    // 根module的state属性
    const state = this._modules.root.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.forEach(plugin => plugin(this))

    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      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 {
     //一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)
      plugins = [],


      strict = false //使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
    } = options

上面这段代码,获取了我们传入的配置pluginsstrict,上面代码中标注有每个属性的作用,关于详细的使用可以到官网查看,以后会有讲解

    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._modules = new ModuleCollection(options) 

看到这段代码,我们肯定能立马想到,我们传入的modules配置,我们来看看modules做了哪些初始化

new ModuleCollection(options)

  constructor (rawRootModule) {
    this.register([], rawRootModule, false)
  }

这个类的构造函数只有简简单单的一行代码,它的参数rawRootModule,是我们给Vuex.Store(options)传入的完整的options,接下来看看register方法做了什么

  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }
    // 创建Module对象,初始runtime为false
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {
      // 如果path = ['user', 'login'], path.slice(0, -1) = ['user']   会去掉最后一个
      // parent是根模块
      const parent = this.get(path.slice(0, -1)) 
      // 把模块添加到根Module对象的_children对象中,形式如下
      // _children = {
      //   user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }
    // 如果options中存在modules属性
    if (rawModule.modules) {
      // 遍历modules都西昂
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // 获取每个module对应的options
        /*{
          modules: {
            user: {
              state,
              mutations
            },
            login
          },
          state: {

          },
          mutations: {

          }
        }*/

        // 看到上面的形式,如果modules里有options,继续递归遍历, 
        // path = ['user', 'login']
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
const newModule = new Module(rawModule, runtime)

代码一上来就创建了一个Module对象,并把options作为参数传入,我们继续看看Module这个类中做了哪些操作

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    // 获取state
    const rawState = rawModule.state

    // 如果state是个方法就调用
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

   // ...其他方法
}

上面的构造函数进行了一些初始化,this.runtime记录了是否是运行时,this._children初始化为空对象,它主要是用来,保存当前模块的子模块,this._rawModule记录了,当前模块的配置,然后又对state进行了些处理。然后我们大概知道了new Module做了什么

  1. 创建了一个_children对象
  2. _rawModule记录模块配置

其他并不是很重要,我们先不提,再回到new ModuleCollection(options),构造函数中

const newModule = new Module(rawModule, runtime)

这里拿到了Module对象

    if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {
      // 如果path = ['user', 'login'], path.slice(0, -1) = ['user']   会去掉最后一个
      // parent是根模块
      const parent = this.get(path.slice(0, -1)) 
      // 把模块添加到根Module对象的_children对象中,形式如下
      // _children = {
      //   user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }

这是一段逻辑判断,而这个path是在ModuleCollection构造函数中,传入的,初始时为空

this.register([], rawRootModule, false)  

/**
   * 
   * @param {*} path 初始为空数组
   * @param {*} rawModule options
   * @param {*} runtime  初始为false
   */
  register (path, rawModule, runtime = true) {...}

    if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {...}

他把ModuleCollection对象的root属性设置为一个Module对象,也就是代表根module,而else中的逻辑我们暂时不看,因为后面会有递归,下个周期时会进入else分支

    // 如果options中存在modules属性
    if (rawModule.modules) {
      // 遍历modules
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // 获取每个module对应的options

        /*{
          modules: {
            user: {
              state,
              mutations
            },
            login
          },
          state: {

          },
          mutations: {

          }
        }*/

        // 看到上面的形式,如果modules里有options,继续递归遍历, 
        // path = ['user', 'login']
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }

这段代码,拿到了当前模块的配置,注意:根模块的配置其实就是options, 然后判断是否存在modules,如果存在,就遍历每个模块,这个forEachValue方法,其实实现非常简单,感兴趣的可以去看一下,最终回调函数遍历到每个module,并获取到module对象和它的模块对象的key,也就是模块名。

之后再次调用了下register方法,递归执行

this.register(path.concat(key), rawChildModule, runtime)

注意path.concat(key), path本来是空数组,在每次递归时都会拼接模块的名字,这段代码非常关键,后面的namespace会有用到

然后我们再次回到register方法的开始

    // 创建Module对象,初始runtime为false
    const newModule = new Module(rawModule, runtime)

    if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {
      // 如果path = ['user', 'login'], path.slice(0, -1) = ['user']   会去掉最后一个
      // parent是根模块
      const parent = this.get(path.slice(0, -1)) 
      // 把模块添加到根Module对象的_children对象中,形式如下
      // _children = {
      //   user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }

依然是创建了Module对象,此时的Module已经是子Module了, if-else判断也会执行到else

    if (path.length === 0) {
         //...
    } else {
      // 如果path = ['user', 'login'], path.slice(0, -1) = ['user']   会去掉最后一个
      // parent是根模块
      const parent = this.get(path.slice(0, -1)) 
      // 把模块添加到根Module对象的_children对象中,形式如下
      // _children = {
      //   user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }

假如我们有两个module,它会获取到除了最后一个的所有module的key列表,并调用get方法

  get (path) {
    return path.reduce((module, key) => {
      // 获取子模块
      return module.getChild(key)
    }, this.root)
  }

这段是get方法的实现,它其实是返回path对应模块的子模块

parent.addChild(path[path.length - 1], newModule)

从最后,把模块添加到,当前模块的_children对象中

  addChild (key, module) {
    this._children[key] = module
  }

最后,通过ModuleCollection对象的root,就可以拿到Module对象树

类似这样

new Vuex.Store({
    modules:{
        user: {
           modules:{
               login
           }
        },
        cart: {

        }
    }
})
// 模拟一下
ModuleCollection = {
    root = 根Module: {
            _children: {
                子module(user): {
                    _children: {
                        子module(login)
                    }
                 },
                子module(cart)
            }
        }
}

小总结:new ModuleCollection(options)在root这个属性上挂载了一个由module对象组成的树

我们回到new Vuex.Store(options)时的构造函数

this._modules = new ModuleCollection(options)

this._modules拿到了模块的集合

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

这段代码,重写了dispatchcommit方法,其实相当于调用了bind方法,我个人认为也可以改写成这样

this.dispatch = this.dispatch.bind(store, type, payload)
this.commit = this.commit.bind(store, type, payload)

继续后面的步骤

this.strict = strict

strict使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误

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

保存根模块的state属性

installModule(this, state, [], this._modules.root)

这段代码虽然简短,但是非常重要,我们来具体分析installModule方法

installModule

/**
 * 
 * @param {*} store store对象
 * @param {*} rootState 根module的state对象
 * @param {*} path 初始为空数组
 * @param {*} module 根module对象
 * @param {*} hot 
 */
function installModule (store, rootState, path, module, hot) {

}

它的参数如上

  // 如果是空数组,说明是根module
  const isRoot = !path.length

判断是否是根模块

  // 返回由module名字 拼接成的字符串
  const namespace = store._modules.getNamespace(path)

这段代码很有意思,我们来看下getNamespace方法,它在ModuleCollection类中

  getNamespace (path) {
    // 根module
    let module = this.root

    return path.reduce((namespace, key) => {
      // 获取子module
      module = module.getChild(key)
      // 如果模块的namespace存在, 举个列子: 一层模块 user/, 二层模块: user/login/
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

直接做一个简单的例子,如果我们在每个模块中使用了namespaced,设置为true,当我们调用commit,dispatch等方法时,我们需要这样做

this.$store.dispatch('count/increment')

this.$store.commit('count/INCREMENT')

getNamespace要做的其实就是获取到count/increment前面的count/,并返回

  // 如果namespaced存在
  if (module.namespaced) {
    // 初始时store._modulesNamespaceMap[namespace]是不存在的
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    // namespace对应module
    store._modulesNamespaceMap[namespace] = module
  }

这段代码做的事情,就是把namespacemodule作为key,value保存在store对象的_modulesNamespaceMap属性上,关于这个属性在什么地方用,可以参考helper.jsgetModuleByNamespace方法,这个方法是实现mapActionsmapMutations的关键,以后也会讲到

然后是这段代码

  // 如果不是根root module   ,初始时hot也不存在, 初始时hot为ture,所以不会执行下面的
  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)
    })
  }

isRoot想必不用多说,就是判断是否是根模块,而hot这个变量又是哪里来的呢,他是installModule方法传入的一个参数,初始时他是空的,但这又有什么用处呢;emmm,由于我自己很少用到,我就不多做详细介绍了(因为菜,所以没用过),具体用法官方文档有详细介绍

我们继续,前面说到,hot是不存在的,而当前又是根节点,所以也不会执行这个if逻辑,但是我们还是要讲一下,不然一会还要回来讲,首先看一下getNestedState方法实现

const parentState = getNestedState(rootState, path.slice(0, -1))

// 具体实现
function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

首先它的第一个参数是state,也就是当前模块的state,注意不一定是rootState,不要被调用参数误解,实际上是递归引用的传递,这个函数就是判断当前path是否为空,如果为空,表示它是根模块的state,不为空表示为子模块的state,要注意的是path.slice(0, -1),它获取了除了本身模块名之前的模块名数组,getNestedState函数直接来说就是用来获取父模块的state,从字面意思也可以理解,至于reduce的一些操作就不详细讲解了。

const moduleName = path[path.length - 1]

然后就是获取了当前模块名,接下来关键来了

    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })

从字面意思,好像是跟随commit调用?没错就是这样。。

  _withCommit (fn) {

    const committing = this._committing
    this._committing = true
    fn()
    // 重新设置之前的提交状态
    this._committing = committing
  }

它就简单的调用了传入的回调函数,设置了前后的状态,然后来看下回调函数的内部

parentState:父模块的state
moduleName:当前模块名
module.state:当前模块的state

Vue.set(parentState, moduleName, module.state)

关于Vue.set方法的介绍:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性

也就是说,它可以在把每个state属性变为响应式,在commit之前,为什么在之前呢,因为这是初始化阶段,我们没有主动调用commit

我们继续后面的代码

  // 重写了dispatch, commit ,getter,state等方法,全部挂载到了当前模块的context属性上
  const local = module.context = makeLocalContext(store, namespace, path)

下面我将详细讲解makeLocalContext方法

makeLocalContext

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

  const local = {
    // 如果不存在namespace,就重写dispatch方法
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {
        // 使用namespace拼接action的类型
        type = namespace + type
        // 如果不使用 namespace/action的形式调用action就会报错
        if (process.env.NODE_ENV !== 'production' && !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 (process.env.NODE_ENV !== 'production' && !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
}

这面代码返回了一个local对象,并且这些对象对dispatch,commit等方法还有state,getter进行了包装

const noNamespace = namespace === ''

这段代码用来判断是否存在命名空间namespace,然后我们再来看下dispatch

1.dispatch
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {
        // 使用namespace拼接action的类型
        type = namespace + type
        // 如果不使用 namespace/action的形式调用action就会报错
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }
      return store.dispatch(type, payload)
    },

首先判断是否有命名空间,如果没有就是正常的dispatch,如果存在,则先统一对象风格unifyObjectStyle

先来看下unifyObjectStyle实现,具体讲解就写在注释里了

// 统一对象风格
function unifyObjectStyle (type, payload, options) {
    // 
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (process.env.NODE_ENV !== 'production') {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}

在看这段代码之前,先说一下,一般来说我们都是这样使用dispatch

store.dispatch('incrementAsync', {
  amount: 10
})

但其实也可以这样,并且官方文档也有例子

store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

知道这些我们就继续往下分析

  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

这里是对参数进行了简单的处理,统一处理成了我们平常使用的模式,最后返回了相应的type, payload, options

接下来,回到makeLocalContext方法

    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {
        // 使用namespace拼接action的类型
        type = namespace + type
        // 如果不使用 namespace/action的形式调用action就会报错
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }
      return store.dispatch(type, payload)
    },

统一这些参数以后,又是一个if判断,第三个参数用的也很少,但是官方文档是有说明的,options 里可以有 root: true,它允许在命名空间模块里提交根的 mutation或action,然后返回了调用store.dispatch方法的返回值,然后我们来看看包装后的commit

2.commit
    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 (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }
      store.commit(type, payload, options)
    }

这段代码和dispatch的实现非常相似,就不讲解了,所做的事情就是对参数进行统一

  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

然后这段代码是把stategetter代理到了local对象上,

3.getter

判断当前模块是否有命名空间,如果不是,就不做任何处理,否则调用makeLocalGetters方法,并传入store对象和namespace完整模块字符串,至于这个namespace是什么,可以往前翻一翻,有具体的讲解。比如user/login,表示user模块下的login模块的namespace。然后我们来看看makeLocalGetters做了什么

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}
  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // 截取getter中的namespace,如果不相等,就不做处理
    if (type.slice(0, splitPos) !== namespace) return

    // 获取getter 的namespace后面的字符串
    const localType = type.slice(splitPos)


    Object.defineProperty(gettersProxy, localType, {
      // 把getters中的属性方法,代理到新的对象中
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

这个函数被调用说明一定是有namespace的,然后遍历getter,此时的getter的属性名是包含有namespace的,至于为什么会有,这个在以后的registerGetters中会有讲解。然后获取到namespace后面真实的getter属性名,并被代理到一个新的对象中,并且被获取时,仍然是使用了完整的namespace,举个例子

假设模块:  user/todo
store.getters.doSomething()   
等价于
store.getters['user/todo/doSomething']()

看完这些相信大家都明白了

4.state

调用了getNestedState方法,这个方法想必不用多说,前面也有讲过,用来获取模块的父模块state,并返回

我们再回到一开始,调用makeLocalContext的位置, 返回的local对象,最终放在了模块的context属性上

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

接下来我们继续分析,后面的内容

registerMutation

  // 遍历mutations
  module.forEachMutation((mutation, key) => {
    // 把namespace和mutation名进行拼接
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

这段代码,简单来说就是遍历了,当前模块的所有mutations,并对每个mutation调用了registerMutation方法,传入了store对象,完整的namespace + commit名mutation函数,以及local对象,接下来看看registerMutation方法实现,至于forEachMutation方法,大家可以自己看一下,实现也很简单

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    // 调用mutation, 并传入state和参数
    handler.call(store, local.state, payload)
  })
}

这个函数,实际上是把当前模块的mutation放在了一个_mutations对象中,那这个属性在哪定义的呢

this._mutations = Object.create(null)

实际上在Store类的构造函数的时候已经初始化为了一个空对象,registerMutation所做的事情,就是把mutationsnamespaceType,形成一个映射关系,并且mutations是一个数组,比如这样

{
    'user/todo/INCREMENT': [
        function() {...}
    ]
}

这里之所以用数组的形式存储函数,我觉得是为了防止重复定义mutation,因为调用之后只有最后一个会生效

  entry.push(function wrappedMutationHandler (payload) {
    // 调用mutation, 并传入state和参数
    handler.call(store, local.state, payload)
  })

然后就是把mutation的调用放在一个函数中,传入了state,payload,在真正调用commit的时候才会循环调用,真实的mutation

下面我们继续看后面的代码

registerAction

  module.forEachAction((action, key) => {
    // namespace + type
    const type = action.root ? key : namespace + key

    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

这里和前面的处理差不多,只是有个判断,如果action存在root说明是根模块,所以直接用key就好了,options 里可以有 root: true,它允许在命名空间模块里提交根的 mutation,否则就使用namespacekey拼接成的action名,然后我们来看registerAction是实现

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
      // 这是给devTool用的,可以不用关心
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

我们暂且不看wrappedActionHandler函数里面的内容,它的处理依旧和mutation的处理一样,也是把action放在_actions对象中,然后再看wrappedActionHandler里的内容,它调用了action,并且让他this指向了store,传入了,local对象中的dispatch,commit等方法还有state,getter,这不就是我们之前看到的,经过处理后的API方法吗。

然后它拿到action调用之后的返回值,最终返回了一个Promise.resolve(res),也就是一个Promise

通过上面这些代码,我们能在实际中这么用

注意:commit, dispatch,getters,state都是当前模块里的方法和对象

{
    actions: {
        async increment({ commit, dispatch, getters,state, rootGetters, rootState }) {
             return await getData()
        }
    }
}

说完了registerAction,我们来说一说registerGetter

registerGetter

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

不多废话,直接看registerGetter的实现

function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

一上来就是一个判断,简单点来说就是,不允许有重复定义的getters,我们之前是看到actionsmutation是可以重复定义的。然后再来看其他的,它和之前的处理有所不同,但也相差不大,因为不允许有重复,所以就不需要push一个函数了,直接调用了getter方法,传入了state,getters,根state,根getters,我们可以这样用

{
    ['INCREMENT']: function(state, getters, rootState, rootGetters){
        //...
    }
}

讲完这些installModule基本上要结束了,我们看最后一段代码

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })

没错,是个递归,它拿到了子模块进行了递归,大家可以翻到前面梳理一下流程

installModule方法我们也讲完了,我们要回到Store类的构造函数中,看看还有些什么初始化操作

resetStoreVM(this, state)

plugins.forEach(plugin => plugin(this))

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

接下来分析resetStoreVM

resetStoreVM

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm
  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure enviroment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  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
  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())
  }
}

首先看一下store._vm是什么,如果有注意到这个函数中间的一段代码的话可以看到,_vm是又创建了一个Vue实例,这个我们后面讲。然后在store上定义了一个对象getters,然后遍历之前,registerGetters注册的getter,然后是这段代码

  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure enviroment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })


// partial函数实现
export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}

首先是遍历所有getters,调用partial函数,返回了一个新函数,并把它放入computed对象中,后面的代码其实是做了这件事

$store.getter
等价于
$store._vm.getter

getter代理到了一个新的Vue实例的computed对象上,这在后面的代码有所体现

  const silent = Vue.config.silent
  // 启动Vue的日志和警告
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      // 把state放在Vue的data中
      $$state: state
    },
    computed  // 把所有getter放在了computed中
  })

这段代码相信不会陌生,vuex之所以能够响应式,原因就在这里,我们通过调用mutation,修改了state,会触发页面更新,其实是Vue的帮助

strict

我们继续看后面的代码

  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
     // 强制getters重新计算
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    // 防止重复创建Vue实例(个人理解)
    Vue.nextTick(() => oldVm.$destroy())
  }

首先是判断strict是否为true, 表示是严格模式,如果直接更改state,会报错,我们看一下它的实现

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

很关键的是中间的箭头函数,我们可以直接看一下Vue源码的实现,它是如何实现修改state报错

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}

    options.user = true  // 很关键的属性

    const watcher = new Watcher(vm, expOrFn, cb, options)

    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

这段代码有个地方很关键,options.user = true,它被传入了Watcher对象中,还有我们传入了箭头函数cb

我们看看Watcher哪里有使用到user属性

class Watcher {

  // ...

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

    // ...
}

我先说一下,这个run方法在什么时机调用的,它是在set属性访问器内部调用notify之后,watcher会调用自身的update方法,然后run就会被调用,可能说的不太清楚,如果各位有时间可以看一下,这里只针对strict原理来讲

下面我们只看这段代码

        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }

我们知道之前传入的user属性为true, 如果调用回调是一定会抛出错误的

    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }

这就是strict模式下,直接修改state会报错的原因

讲完这些,其实后面的代码就简单略过了,也不是很重要(懒?)

然后我们来看Store构造函数中最后一点内容

    plugins.forEach(plugin => plugin(this))

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

首先调用了所有的plugin,并传入了store对象,关于plugin的用法官方文档都有介绍。然后关于useDevtools内容我就不讲解了,它和devTool相关

终于讲完了初始化,我们开始讲Vuex的一些API

API

我们按照官方文档一个个来

1. commit

commit的用法就不用介绍了,直接看源码

  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) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    this._withCommit(() => {
      // 遍历type对应的mutation数组
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 遍历所有订阅,并传入mutation对象和状态
    this._subscribers.forEach(sub => sub(mutation, this.state))

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

首先是调用unifyObjectStyle方法,统一对象风格,如果有看前面的内容的话,应该知道,这是用来处理以下两种情况的参数

commit(type: string, payload?: any, options?: Object)
commit(mutation: Object, options?: Object)

然后是下面这段

    const mutation = { type, payload }
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }

如果commitmutation不存在的话,就会报出警告,并返回不做处理

    this._withCommit(() => {
      // 遍历type对应的mutation数组
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })

_withCommit方法前面也有讲过,简单点说其实就是调用传入的回调函数,这里循环调用了mutation,至于为什么是数组,前面有讲到,是在registerMutation方法

我们继续来看

    // 遍历所有订阅,并传入mutation对象和状态
    this._subscribers.forEach(sub => sub(mutation, this.state))

    //  silent属性已经被删除,不让使用
    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }

this._subscribers属性也是在Store对象的构造函数初始化时创建的一个数组,看到这个数组的名字,不用多说肯定是发布订阅模式,然后循环调用订阅的回调函数,它是在mutation被调用后执行, 但是在哪里订阅的呢,其实是在subscribe方法,它也是Vuex的一个API,下面我们来具体讲讲

2. subscribe

订阅 store 的 mutation。handler 会在每个 mutation完成后调用,接收 mutation 和经过 mutation 后的状态作为参数

  subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
  }
function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

这就是一个简单的发布订阅模式的应用,把回调存储在了订阅数组中,其中genericSubscribe方法利用了闭包,返回了一个函数,调用它之后就可以取消订阅,其实还有其他的订阅方法,subscribeAction

3. subscribeAction

  subscribeAction (fn) {
    const subs = typeof fn === 'function' ? { before: fn } : fn
    return genericSubscribe(subs, this._actionSubscribers)
  }

判断是否是一个函数,如果是默认为before函数,也就是在dispatch调用action之前调用,如果是{after: fn}就会在action之后调用

4. dispatch

  // 执行了beforeActions所有回调
  // 执行所有actions,并拿到所有promise返回的结果
  // 执行了afterActions所有回调
  dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return result.then(res => {
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(`[vuex] error in after action subscribers: `)
          console.error(e)
        }
      }
      return res
    })
  }

前面关于对象统一,以及是否存在action的判断就不讲了

    try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

然后过滤筛选获取到了订阅的一些before函数,也就是在调用action之前调用,并传入了action, action = { type, payload }以及state

5. watch

响应式地侦听 fn 的返回值,当值改变时调用回调函数。fn 接收 store 的 state 作为第一个参数,其 getter 作为第二个参数。最后接收一个可选的对象参数表示 Vue 的 vm.$watch 方法的参数。

  watch (getter, cb, options) {
    if (process.env.NODE_ENV !== 'production') {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }

// Store构造函数初始化时
this._watcherVM = new Vue()

这里给侦听函数里的,getter传入了stategetters, 当state发生变化时,侦听函数的返回值也发生了变化,值改变后就会触发cb回调函数, 关于`vm.watch`](https://cn.vuejs.org/v2/api/#vm-watch)

6. replaceState

替换 store 的根状态,仅用状态合并或时光旅行调试。

    this._withCommit(() => {
      this._vm._data.$$state = state
    })

直接替换掉了$$state原本状态

7. registerModule

可以注册模块,例子:

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})
  registerModule (path, rawModule, options = {}) {
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
      assert(path.length > 0, 'cannot register the root module by using registerModule.')
    }

    this._modules.register(path, rawModule)
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // reset store to update getters...
    resetStoreVM(this, this.state)
  }

首先时统一处理了一下path和一些断言,然后调用了register方法installModule方法,resetStoreVM方法,这几个方法前面都有讲到,相当于又创建了一个Store对象,流程也差不多

8. unregisterModule

卸载一个动态模块。

  unregisterModule (path) {
    if (typeof path === 'string') path = [path]
    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
    }

    this._modules.unregister(path)
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      Vue.delete(parentState, path[path.length - 1])
    })
    resetStore(this)
  }

前面是对path模块名进行了处理以及断言是否是数组,然后调用unregister

this._modules.unregister(path)


unregister (path) {
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    if (!parent.getChild(key).runtime) return

    parent.removeChild(key)
}

这里获取到了传入模块名,也就是path的父模块,然后获取子模块判断是否存在runtime属性,这个属性是干嘛的,我也不是很清楚,希望又大佬解惑(菜 !- -,没办法啊)

  parent.removeChild(key)

  removeChild (key) {
    delete this._children[key]
  }

最后删除了子模块,也就是我们要删除的模块

9. hotUpdate

热替换新的 action 和 mutation

官方的例子

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'

Vue.use(Vuex)

const state = { ... }

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

if (module.hot) {
  // 使 action 和 mutation 成为可热重载模块
  module.hot.accept(['./mutations', './modules/a'], () => {
    // 获取更新后的模块
    // 因为 babel 6 的模块编译格式问题,这里需要加上 `.default`
    const newMutations = require('./mutations').default
    const newModuleA = require('./modules/a').default
    // 加载新模块
    store.hotUpdate({
      mutations: newMutations,
      modules: {
        a: newModuleA
      }
    })
  })
}

热模块更新源码如下

  hotUpdate (newOptions) {
    this._modules.update(newOptions)
    resetStore(this, true)
  }

this._modules.update(newOptions)方法是在module-collection.js文件中定义

  update (rawRootModule) {
    update([], this.root, rawRootModule)
  }
function update (path, targetModule, newModule) {
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, newModule)
  }

  // update target module
  targetModule.update(newModule)

  // update nested modules
  if (newModule.modules) {
    for (const key in newModule.modules) {
       // 如果传入的配置中没有该模块就报错
      if (!targetModule.getChild(key)) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
}

以上代码总的来说就是递归遍历模块,并更新模块,其中涉及到三个update方法,大家不要弄混。

update([], this.root, rawRootModule)

主要传入了,一个空数组,原本的根模块对象,要用来替换的模块配置

function update (path, targetModule, newModule) {
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, newModule)
  }

  // update target module
  targetModule.update(newModule)

  // update nested modules
  if (newModule.modules) {
    for (const key in newModule.modules) {
       // 如果传入的配置中没有该模块就报错
      if (!targetModule.getChild(key)) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }

递归遍历,原本的模块树,使用新模块替换掉原本模块

以上代码中还有一个模块中的update方法,即targetModule.update(newModule)

// update target module
targetModule.update(newModule)

// module.js
update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

这个方法其实很简单,替换掉了原本的模块。

辅助函数

mapXXX方法都在helper.js文件中

// helper.js
export const mapState = normalizeNamespace((namespace, states) => {
  //..
})

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  // ..
})
// ...

可以看到他们都调用了normalizeNamespace方法,我们知道mapXxx是一个方法,所以它一定会返回一个方法

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

这个方法实际上是对参数进行了处理,判断如果namespace不是字符串,也就是说它可能不存在,namespace就设置为一个空字符串,比如这样

{
    computed: {
        ...mapState(['username'])
    }
}

如果传入了namespace字符串,并且最后没有斜杠,就自动帮它加上,最后才是调用真实的mapXXX,比如这样

{
    computed: {
        ...mapState('user/', ['username'])
    }
}

接下来我们看一下mapState实现

mapState

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    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
})

首先又是调用了一个normalizeMap方法,传入了我们需要获取的states,normalizeMap实现如下

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

这段代码看起来可能有点复杂,举个例子

normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
normalizeMap(['user', 'count']) => [ { key: 'user', val: 'user' }, { key: 'count', val: 'count' }]

normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]

然后我们回到之前的代码

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    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返回的是一个对象,其形式如下,其他mapMutations,mapActions都可以这样

mapState('user', ['username', 'password'])

{
    username: function(){},
    password: function(){}
}

mapMutation('count', ['increment'])

现在知道为啥mapState要写在computed里了吧!原因就在这里。为了方便我就直接用注释分析了

    res[key] = function mappedState () {
      // store对象中的state,这个state是根state 
      let state = this.$store.state
      // 根getters
      let getters = this.$store.getters
      // 如果传入了namespace
      if (namespace) {
        // 调用getModuleByNamespace方法,源码实现在下方,它返回namespace对应的模块
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        // 有看过前面源码应该记得,很多方法和对象都挂载到了context属性上
        state = module.context.state
        getters = module.context.getters
      }
      // 调用val或获取state
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
function getModuleByNamespace (store, helper, namespace) {
   // _modulesNamespaceMap属性是不是很眼熟?
   // 它是在Store类的installModule方法中使用到,记录了namespace对应的module 
  const module = store._modulesNamespaceMap[namespace]
  if (process.env.NODE_ENV !== 'production' && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}

上面这些代码有几个注意点

  1. getModuleByNamespace方法中的store._modulesNamespaceMap[namespace]是在installModules中进行的初始化

  2. mapState是可以传入回调函数的

    {
      computed: mapState({
        // 箭头函数可使代码更简练
        count: state => state.count,
    
        // 传字符串参数 'count' 等同于 `state => state.count`
        countAlias: 'count',
    
        // 为了能够使用 `this` 获取局部状态,必须使用常规函数
        countPlusLocalState (state) {
          return state.count + this.localCount
        }
      })
    }

mapMutations

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  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
})

其他相同的代码就不讲了,关键看下面的

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

这段代码其实和mapState里的相差不大,都是获取到commit,如果有namespace就获取模块里的commit,最后调用commit,它也可以传入一个回调函数,不过,举个例子

methods: {
    ...mapMutations(['increment']),
    //等价于
    ...mapMutations({
        add: function(commit, ...args){
           commit('increment', ...args)
        }
    }),
     // 等价于
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
}
// 组件中调用
this.add(1)

mapGetters

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
        // 如果namespace存在但是没有找到对应的模块 就直接返回,不做处理
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      // 如果没有找到对应的getter会报错并返回
      if (process.env.NODE_ENV !== 'production' && !(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
})

mapGetters和其它实现有所区别

  1. 所有模块的getters都被代理在store对象中,所以直接使用getterkeynamespace拼接获取到对应的getter;具体在哪代理可以参见

    // store.js 的makeLocalContext方法里的实现
    Object.defineProperties(local, {
        getters: {
          get: noNamespace
            ? () => store.getters
            : () => makeLocalGetters(store, namespace)
        },
        state: {
          get: () => getNestedState(store.state, path)
        }
      })
  1. getter不支持传入函数

mapActions

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  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
})

mapActions的实现和mutation的实现一模一样?确实是这样。。。下面只说下用法

methods: {
    ...mapActions(['increment']),
    //等价于
    ...mapActions({
        add: function(dispatch, ...args){
           dispatch('increment', ...args)
        }
    }),
     // 等价于
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
}
// 组件中调用
this.add(1)

createNamespacedHelpers

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

官方例子

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

对于这个createNamespacedHelpers如何实现,我想大家应该看的懂吧

总结

终于分析完了Vuex的源码,完成这篇文章也是没事抽出空闲时间写出来的,可能会有错别字,分析错误或者有些我不知道的,欢迎大家指正,阅读源码也使我学到了很多东西,让我从陌生,逐渐开始得心应手,一直到现在,我对于源码不再是单纯的为了面试,而是一种兴趣,谢谢大家观看

下一章

逐行级源码分析系列(二) Redux和React-Redux源码(正在写作)

未完待续。。。

全部评论

相关推荐

10-09 00:50
已编辑
长江大学 算法工程师
不期而遇的夏天:1.同学你面试评价不错,概率很大,请耐心等待;2.你的排名比较靠前,不要担心,耐心等待;3.问题不大,正在审批,不要着急签其他公司,等等我们!4.预计9月中下旬,安心过节;5.下周会有结果,请耐心等待下;6.可能国庆节前后,一有结果我马上通知你;7.预计10月中旬,再坚持一下;8.正在走流程,就这两天了;9.同学,结果我也不知道,你如果查到了也告诉我一声;10.同学你出线不明朗,建议签其他公司保底!11.同学你找了哪些公司,我也在找工作。
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务