伪前端带你不完全解析Vuex源码(一)

2018-12-30  本文已影响9人  万物论

Vuex是什么?
大家可以先把github上的 vuex项目 clone下来或者直接下载下来用编辑器打开

git clone https://github.com/vuejs/vuex.git
Vuex项目目录
项目的目录结构很清晰,重点会解析
module-collection.js module.js index.js mixin.js store.js这几个js文件。

当我们要使用vuex管理状态的时候就要规划好vuex的结构目录了,对于大型的vue应用官方也有推荐的项目结构:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

结构中store目录下面的index.js文件就是用来生成一个管理状态store对象
通常我们会在store/index.js文件中敲入这些代码

import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mucations from './mucations'
import * as actions from './actions'
import * as getters from './getters'

const debug = process.env.NODE_ENV !== 'production'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mucations,
  actions,
  getters,
  strict: debug
})

import Vuex from 'vuex' 就是导入Vuex项目中的入口文件index.js中的接口


到这边就要对Vuex项目中的源码进行不完全解析,让我们回到Vuex项目中

入口

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

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

这个就是Vuex对外暴露的接口,其中 Store就是Vuex提供的状态管理类,稍后会进行介绍,再来看看install,这个函数会在Vue.use(Vuex)的时候调用。

查看src/store.js里面的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.use(Vuex)调用install的时候传入的Vue引用,在index.js开头会定义一个局部的Vue引用,用于Vuex中使用

// ...
let Vue // bind on install
// ...

在执行install的时候会判断局部Vue引用是否跟传入的Vue引用是一样的,如果是一样的说明Vuex已经加载了,在非生产环境下会报错,否则让局部Vue也指向传来的Vue,便于Vuex内部使用Vue,函数的最后,又调用了applyMixin(Vue),这个函数是实现通过this.$store.xxx这种方式来访问数据和管理数据,具体怎么实现查看在src/mixin.js代码

// mixin.js
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)
    }
  }

由于vue1.x和vue2.x的生命周期钩子有不同,所以会进行版本的判断,让vue在初始化(vue2.x是beforeCreate,vue1.x是init)钩子函数数组中mixin一个vuexInit函数,实现Vue组件初始化的时候执行vueInit往vue组件中注入$store这样我们就可以在vue组件中使用 this.$store.xxx访问到store管理对象中的数据和方法,具体怎么在vue组件中注入$store属性,查看vuexInit函数中的代码

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

上面这段代码会在Vue组件初始化的时候被调用,他会从构造这个vue实例的options参数中判断是否含有store属性,如果有则把这个store注入到vue实例的$store属性中,就比如说我们vue应用的整个项目的入口文件是main.js里面的代码是

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store/index'

Vue.config.productionTip = false

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

main.js中创建的vue组件其实是最顶层的vue组件<Root></Root>,这里的if (options.store)就是判断在new Vue({...})传给Vue构造函数的参数也就是options中是否有store这个对象,如果有就注入到这个顶层vue实例的$store的属性中,这时候最顶层的vue组件<Root><App>...</App></Root>Root中就包含了$store,因为每个vue组件被创建之前都会调用beforeCreat/Init生命周期下的所有函数,包括被mixin到里面的vuexInit所以当App组件被创建的时候,他本身的options参数是没有store的,而
else if (options.parent && options.parent.$store)他会去判断是否含有父组件,这里App的父组件就是Root组件了,而Root有$store属性所以把父组件的这个属性的引用赋值给App这个组件的$store,这样就达到每个vue组件创建的时候都能拿到store,一级一级的传下去。
看到这里也不是说store只能在最顶层的组件出现,这样是为了在这个项目的所有vue组件都能通过这个状态管理来管理数据,store是可以在任何一个实例化vue组件的options中出现的,只不过这样只能在当前组件和其子孙组件中使用store而已。可以采用Bus等来实现没必要用Vuex。

到这边就实现了 为什么我们在组件中可以使用this.$store.xxx来访问数据,管理数据来。


Store类

接下来针对具体怎么实现状态管理来解析代码
回到store.js中对Store类进行解析,这个是用来实例化一个store对象的也就是this.$store.xxx中的store

  constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    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
    } = options

    // store internal state
    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()

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

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

在构建一个 store实例之前要确保Vuex已经被加载,所以一开始的

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

这段代码判断没加载就调用install函数加载Vuex原理和上面解析install的时候一样。

下面的代码是用来判断依赖

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

里面的assert是他们自己实现的一个断言函数,代码比较简单,可以查看utils.js里面的函数实现。
先判断Vue因为里面用到的比如刚才的往组件中注入$store属性或者等下要介绍的数据监听等等功能都依赖Vue。还要断言环境是否支持Promise,在actions中会用到Promise所以Promise也必须被支持,最后一个断言是 当前的store对象,必须是通过Store类创建的。

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

这段代码则是通过ES6的解构赋值,把构造store store = new Vuex.Store({...})时候的参数options,赋值成局部可引用的变量

  // store internal state
    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()

_committing:用来判断是否是通过commit来实现更改数据因为commit是修改state中数据的唯一合法途径
_actions:存放把我们store/actions下面编写的所有action函数通过注册后的函数集合,就是注册后的action
_actionSubscribers:订阅了action变化的订阅者集合
_mutations:存放把我们store/mutations下面编写的所有mutation函数通过注册后的函数集合,就是注册后的mutation
_wrappedGetters: 对我们编写的getters函数进行包装后的函数,用于组册getters
_modules:当我们的项目很庞大的时候,很多数据都要用状态管理,这时候把所有的数据都存在一个store进行管理会显得臃肿而且复杂,变得难以管理,所以Vuex提供了module,也就是每个module下包含各自的state,actions,getters,mutations。他是根据ModuleCollection这个类生产一个module树,接下来会对这个类进行解析
_modulesNaspaceMap:用于存放命名空间下对应的module
_subscribers:用来存储所有对 mutation 变化的订阅者
_watcherVM:一个vue实例,利用vue的watch来监测state数据的变化

至于Object.create(null)是创建一个空对象但是又有别于var obj = {}中这种通过字面量创建的空对象,因为Object.create(null)创建的空对象的__proto__是指向null的,Object.create创建对象的大概过程就是:

function (o) {
  var F = function () {}
  F.prototype = o
  var f = new F()
  return f
}
this._modules = new ModuleCollection(options)

通过把options也就是创建store时候的参数传给ModuleCollection构建module树

// module-collection.js
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

这个是ModuleCollection的构造器,rawRootModule就是我们前面传将来的options,构造器通过调用register()函数来注册module构建module树

// module-collection.js
register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }

    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      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)
      })
    }
  }

比如我们一开始传过来的参数是register([], rawRootModule, false)第一个参数是用来表示也可以说是层级关系,rawModule则是用于构造一个module所用的参数,runtime表示mudule是否是运行时。
我们先分析为根module的情况,也就是rawModule为构建store时候传进来的option,一开始也是先对传进来的rawModule先做断言,看是不是符合构造一个module,这边的代码也是比较好理解的这边就不解析了。
通过 const newModule = new Module(rawModule, runtime)创建一个module对象
来看看Module代码

// module.js
  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) || {}
  }

设置这个module的运行时状态为false,因为一开始这个根module的子module还没注册所以为空,所以先this.children = Object.create(null)赋值个空对象,还有设置这个module的state属性,也就是根module的state。
然后再回到module-collection.js中,这时候这个 newModule就是根module了,因为一开始path === []所以设置这个store的root为根module。接下来会判断这个rawModule是否配置了子module,如果没有则整个module就一个root module,如果有子module,forEachValue也是他们自己封装的枚举对象的,具体可以查看 utils.js他把rawModule.modules的对象当作构建子module的参数,还有把当前子module的key放到path中递归调用register从而实现module树的构建。假如刚才创建的root还存在子module的话,第二次调用register,假设rawModule.modules存在moa: {...}这样一个module配置的话,那么第二次调用register传进去的参数就是,register(['moa'], moa, runtime)然后再通过 new Module() 创建一个module对象,然而这时候path.length !== 0
通过const parent = this.get(path.slice(0, -1))来获取moa这个module的父module

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

这段代码就是用来获取当前module的父module,因为要找父module肯定不能包含自己,所以通过path.slice(0, -1)传到get函数中的时候就不包含自己了,由于这时候get中path为空数组所以直接返回this.root就是根module,这时候parent === this.root为true,然后在这个父module中的_childre添加当前的newModule,key就是moa,这时候root就有子module了。递归直到没有其他module配置为止,就完成了module树的构建。

现在再回到store.js这个文件中,
this._modules = new ModuleCollection(options)这个_modules就是刚才构建出来的module树了。

// store.js
// 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这个局部变量指向store对象,然后对store里面的dispatch和commit绑定当前的store对象。也就是这两个函数里面的this都是指向这个store对象

// store.js
 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(() => {
     entry.forEach(function commitIterator (handler) {
       handler(payload)
     })
   })
   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的时候有两种参数方式具体可以查看Vuex的官网
通过参数构建一个mutation对象,从组册的mutatons中通过type也就是我们定义mutation时候的函数名字,拿到同名的mutation集合entry,为什么说同名呢,下面在安装module的时候会讲到,会先对这entry进行判断如果没有这个mutation则报错,接下来会调用,_withCommit()这个函数进行state的改变,传进一个遍历执行对于mutation的函数其中payload就是我们定义mutation时候的函数的参数。

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

其实这个函数不复杂,我们在commit的时候去改变_committing的状态,为true则表示正在通过commit方式改变state,用于在watch监听数据改变的时候判断是否是通过commit来改变的。然后执行我们注册的mutation函数。
回到commit函数this._subscribers.forEach(sub => sub(mutation, this.state))遍历通知订阅了mutation的订阅者,传入最新的state。

  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
    }

    this._actionSubscribers.forEach(sub => sub(action, this.state))

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

dispatch一开始跟commit是类似的,不同的是最后return那边,会先判断同名的action有几个,如果是一个就是直接执行entry中的第一个action,如果大于一个,我们都知道我们可以在action中进行异步的操作,所以如果action中有reutrn数据的话,由于异步的关系,你return出来的数据不一定是你想要的结果,所以在组册action的时候他的包装函数会把不是Promise结果处理成Promise,Promise.all(),用来确保全部的注册的action函数执行,也就是状态都是resolve否则dispatch不会返回正确的值。所以你获取到var d = dispatch(xxx,xxx)值后可以通过d.then(res => {})来获取对于同名的action集合return的值就是res是个数组,所以这边就解释了为什么一开始要判断是否支持Promise了,Promise也是属于ES6的内容。

接着回到Store的构造器中

// strict mode
    this.strict = strict

    const state = this._modules.root.state

如果开启严格模式的话,Vuex就会监听数据的改变是否是通过commit。
然后把root moudle的状态赋值给一个局部变量

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

这边开始安装modules,我们一开始只是构建了module树,但是mutations,actions,getters都还没注册,而且如果含有多个module的话,状态state树也还没构建。通过installModule函数来完成这些事情。

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

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

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

  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 type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, 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)
  })
}

安装module也是从根module开始 也就是从root开始,参数中,store为当前store对象,rootState为root的state,path就是用来记录module层级关系,module当前要安装的module,hot为热更新。
当安装root module的时候,通过path判断是否是根module,获得当前module的命名空间,比如我之前root下面还有一个moa的module,如果这个module的namespaced为true的时候得到的命名空间就是moa/。如果moa下面还有一个moa1的module,他的namespaced也为true那么这个moa1的命名空间就是moa/moa1/,所以要把当前module的名称加到命名空间中必须当前module的namespaced = true才可以,比如说moa他的namespaced 为 false 那么moa的命名空间为空,而且moa1的namespaced为true,那么moa1的命名空间为moa1/这样moa就没有出现在命名空间中。
因为我们当前是根module所以 namespace 为空,他设置namespaced也没有意义

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

这三行代码是把如果定义了命名空间,就把当前的module放入这个_modulesNamespaceMap集合中,key为命名空间,value为当前module
接下来因为不是根module所以这个代码等到安装子module的时候解析
const local = module.context = makeLocalContext(store, namespace, path)这行代码的作用就是用来生成,当前module的上下文对象,生成当前module下的dispatch,commit,getters,state

/**
 * 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 (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
}

参数store为当前的store对象, namespace为当前module的命名空间,path则是用来在state中找到当前module的state。因为在Vuex中没有设置命名空间的话,actions,mutations,getters默认都是组册在全局里面的。具体会在讲解注册他们的时候分析。
local就是为对应module的actions,mucations,getters,state提供的上下文对象。
不要忘了我们一开始是安装root module所以是没有命名空间的,所以当前module就是store一开始提供的dispatch,而这个dispatch函数默认执行的是全局的action,如果是有命名空间的module下的dispatch,他对store一开始的dispatch又包装了一层,作用就是自动加上命名空间,这样我们在module里面dispatch的时候只要写action的type就可以对应到这个module下的action而不是全局的,再来看看这个有命名空间下的dispatch多了一个_options参数,这个作用就是实现dispatch('someOtherAction', null, { root: true })这种方式来分发全局里面的action。判断完后再调用store一开始提供的dispatch来分发actionreturn store.dispatch(type, payload)因为这时候的type已经结果判断是否有加上命名空间了。commit的原理也是类似的。然后再用Object.defineProperties(....)在local添加经过命名空间判断后的gettersstate,对于getters的话没有命名空间的话就返回store.getters有命名空间的话通过命名空间获取当前module下的getters,来看看makeLocalGetters这个函数

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // skip if the target getter is not match this namespace
    if (type.slice(0, splitPos) !== namespace) return

    // extract local getter type
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

这个函数就是,从store中注册的getters中,因为getters是一个对象,key是命名空间加我们定义的getters的名字,比如moa/moa1/getusername其中moa/moa1是命名空间,getusername是我们定义的getters的名字。所以我们在一个module中调用getters的时候,也不用带上命名空间。
对于state的话调用了getNestedState来获取当前module的state

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

这个函数就是用来从state树中来找到对于module的state。
回到makeLocalContext函数中,他最终返回这个module的上下文对象。用于在注册actions,mutations,getters中用到。
回到installModule函数中,接下来的三个函数就是用于注册

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

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

先来说说注册mutation他在store的_mutations中注册的名字就是命名空间+我们定义的mutation名字,看下registerMutation具体怎么注册

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

功能很简单,这边我们会知道,原来store中注册的_mutations的值是以数组存放的,这也是为什么如果你没有定义命名空间的module下面commit_mutations中注册的同名mutation的时候,其他的在同个命名空间下的mutation一起执行的原因了。回到代码中,我们编写的,mutation会经过一个包装函数包装,其中handler,就是我们编写的mutation,这里也会知道为什么我们编写mutation的时候,参数中的state就是当前module下定义的state,而payload就是我们commit的时候带的参数。

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)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

registerAction中其实也是跟registerCommit差不多的实现。这里面我们也会看到,为什么我们在编写action的时候,参数中{dispatch,commit,getters,state,rootGetters,rootState}是怎么来的了,其实cb很少用到,这里的包装函数也会把你返回的结果包装成Promise,在解析dispatch的时候也有提到。

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

这里跟注册mutations和actions不同的地方就是他不能重复。其他地方差不多。但是其实这里的getters还没有真正的注册完成,这时候我们this.$store.getters是没有东西的。这个会在 《不完全解析Vuex源码(二)》中会提到。
再次回到installModule函数中,最后回去遍历module树中的子module,如果有就继续安装

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

跟安装root module不同的是installModule中的这几行代码

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

作用就是构建store的state树,先获取到当前module的父state,然后在父的state下面加入当前module的state,key就module名称,value就是当前module的state。这里我们也会看到commit是改变state的唯一合法途径了。

到这边完成了actions,mutations,getters的注册(getters其实不能说是真正的注册)module树的安装。还有很重要的一部分就是利用Vue的功能实现数据的响应,和监测将在(二)中解析。

答应我一定要在会用Vuex的前提下再去看源码。

上一篇下一篇

猜你喜欢

热点阅读