API

上一节我们对 Vuex 的初始化过程有了深入的分析,在我们构造好这个 store 后,需要提供一些 API 对这个 store 做存取的操作,那么这一节我们就从源码的角度对这些 API 做分析。

数据获取

Vuex 最终存储的数据是在 state 上的,我们之前分析过在 store.state 存储的是 root state,那么对于模块上的 state,假设我们有 2 个嵌套的 modules,它们的 key 分别为 ab,我们可以通过 store.state.a.b.xxx 的方式去获取。它的实现是在发生在 installModule 的时候:

  1. function installModule (store, rootState, path, module, hot) {
  2. const isRoot = !path.length
  3. // ...
  4. // set state
  5. if (!isRoot && !hot) {
  6. const parentState = getNestedState(rootState, path.slice(0, -1))
  7. const moduleName = path[path.length - 1]
  8. store._withCommit(() => {
  9. Vue.set(parentState, moduleName, module.state)
  10. })
  11. }
  12. // ...
  13. }

在递归执行 installModule 的过程中,就完成了整个 state 的建设,这样我们就可以通过 module 名的 path 去访问到一个深层 modulestate

有些时候,我们获取的数据不仅仅是一个 state,而是由多个 state 计算而来,Vuex 提供了 getters,允许我们定义一个 getter 函数,如下:

  1. getters: {
  2. total (state, getters, localState, localGetters) {
  3. // 可访问全局 state 和 getters,以及如果是在 modules 下面,可以访问到局部 state 和 局部 getters
  4. return state.a + state.b
  5. }
  6. }

我们在 installModule 的过程中,递归执行了所有 getters 定义的注册,在之后的 resetStoreVM 过程中,执行了 store.getters 的初始化工作:

  1. function installModule (store, rootState, path, module, hot) {
  2. // ...
  3. const namespace = store._modules.getNamespace(path)
  4. // ...
  5. const local = module.context = makeLocalContext(store, namespace, path)
  6. // ...
  7. module.forEachGetter((getter, key) => {
  8. const namespacedType = namespace + key
  9. registerGetter(store, namespacedType, getter, local)
  10. })
  11. // ...
  12. }
  13. function registerGetter (store, type, rawGetter, local) {
  14. if (store._wrappedGetters[type]) {
  15. if (process.env.NODE_ENV !== 'production') {
  16. console.error(`[vuex] duplicate getter key: ${type}`)
  17. }
  18. return
  19. }
  20. store._wrappedGetters[type] = function wrappedGetter (store) {
  21. return rawGetter(
  22. local.state, // local state
  23. local.getters, // local getters
  24. store.state, // root state
  25. store.getters // root getters
  26. )
  27. }
  28. }
  29. function resetStoreVM (store, state, hot) {
  30. // ...
  31. // bind store public getters
  32. store.getters = {}
  33. const wrappedGetters = store._wrappedGetters
  34. const computed = {}
  35. forEachValue(wrappedGetters, (fn, key) => {
  36. // use computed to leverage its lazy-caching mechanism
  37. computed[key] = () => fn(store)
  38. Object.defineProperty(store.getters, key, {
  39. get: () => store._vm[key],
  40. enumerable: true // for local getters
  41. })
  42. })
  43. // use a Vue instance to store the state tree
  44. // suppress warnings just in case the user has added
  45. // some funky global mixins
  46. // ...
  47. store._vm = new Vue({
  48. data: {
  49. $$state: state
  50. },
  51. computed
  52. })
  53. // ...
  54. }

installModule 的过程中,为建立了每个模块的上下文环境,因此当我们访问 store.getters.xxx 的时候,实际上就是执行了 rawGetter(local.state,…)rawGetter 就是我们定义的 getter 方法,这也就是为什么我们的 getter 函数支持这四个参数,并且除了全局的 stategetter 外,我们还可以访问到当前 module 下的 stategetter

数据存储

Vuex 对数据存储的存储本质上就是对 state 做修改,并且只允许我们通过提交 mutaion 的形式去修改 statemutation 是一个函数,如下:

  1. mutations: {
  2. increment (state) {
  3. state.count++
  4. }
  5. }

mutations 的初始化也是在 installModule 的时候:

  1. function installModule (store, rootState, path, module, hot) {
  2. // ...
  3. const namespace = store._modules.getNamespace(path)
  4. // ...
  5. const local = module.context = makeLocalContext(store, namespace, path)
  6. module.forEachMutation((mutation, key) => {
  7. const namespacedType = namespace + key
  8. registerMutation(store, namespacedType, mutation, local)
  9. })
  10. // ...
  11. }
  12. function registerMutation (store, type, handler, local) {
  13. const entry = store._mutations[type] || (store._mutations[type] = [])
  14. entry.push(function wrappedMutationHandler (payload) {
  15. handler.call(store, local.state, payload)
  16. })
  17. }

store 提供了commit 方法让我们提交一个 mutation

  1. commit (_type, _payload, _options) {
  2. // check object-style commit
  3. const {
  4. type,
  5. payload,
  6. options
  7. } = unifyObjectStyle(_type, _payload, _options)
  8. const mutation = { type, payload }
  9. const entry = this._mutations[type]
  10. if (!entry) {
  11. if (process.env.NODE_ENV !== 'production') {
  12. console.error(`[vuex] unknown mutation type: ${type}`)
  13. }
  14. return
  15. }
  16. this._withCommit(() => {
  17. entry.forEach(function commitIterator (handler) {
  18. handler(payload)
  19. })
  20. })
  21. this._subscribers.forEach(sub => sub(mutation, this.state))
  22. if (
  23. process.env.NODE_ENV !== 'production' &&
  24. options && options.silent
  25. ) {
  26. console.warn(
  27. `[vuex] mutation type: ${type}. Silent option has been removed. ` +
  28. 'Use the filter functionality in the vue-devtools'
  29. )
  30. }
  31. }

这里传入的 _type 就是 mutationtype,我们可以从 store._mutations 找到对应的函数数组,遍历它们执行获取到每个 handler 然后执行,实际上就是执行了 wrappedMutationHandler(playload),接着会执行我们定义的 mutation 函数,并传入当前模块的 state,所以我们的 mutation 函数也就是对当前模块的 state 做修改。

需要注意的是, mutation 必须是同步函数,但是我们在开发实际项目中,经常会遇到要先去发送一个请求,然后根据请求的结果去修改 state,那么单纯只通过 mutation 是无法完成需求,因此 Vuex 又给我们设计了一个 action 的概念。

action 类似于 mutation,不同在于 action 提交的是 mutation,而不是直接操作 state,并且它可以包含任意异步操作。例如:

  1. mutations: {
  2. increment (state) {
  3. state.count++
  4. }
  5. },
  6. actions: {
  7. increment (context) {
  8. setTimeout(() => {
  9. context.commit('increment')
  10. }, 0)
  11. }
  12. }

actions 的初始化也是在 installModule 的时候:

  1. function installModule (store, rootState, path, module, hot) {
  2. // ...
  3. const namespace = store._modules.getNamespace(path)
  4. // ...
  5. const local = module.context = makeLocalContext(store, namespace, path)
  6. module.forEachAction((action, key) => {
  7. const type = action.root ? key : namespace + key
  8. const handler = action.handler || action
  9. registerAction(store, type, handler, local)
  10. } )
  11. // ...
  12. }
  13. function registerAction (store, type, handler, local) {
  14. const entry = store._actions[type] || (store._actions[type] = [])
  15. entry.push(function wrappedActionHandler (payload, cb) {
  16. let res = handler.call(store, {
  17. dispatch: local.dispatch,
  18. commit: local.commit,
  19. getters: local.getters,
  20. state: local.state,
  21. rootGetters: store.getters,
  22. rootState: store.state
  23. }, payload, cb)
  24. if (!isPromise(res)) {
  25. res = Promise.resolve(res)
  26. }
  27. if (store._devtoolHook) {
  28. return res.catch(err => {
  29. store._devtoolHook.emit('vuex:error', err)
  30. throw err
  31. })
  32. } else {
  33. return res
  34. }
  35. })
  36. }

store 提供了dispatch 方法让我们提交一个 action

  1. dispatch (_type, _payload) {
  2. // check object-style dispatch
  3. const {
  4. type,
  5. payload
  6. } = unifyObjectStyle(_type, _payload)
  7. const action = { type, payload }
  8. const entry = this._actions[type]
  9. if (!entry) {
  10. if (process.env.NODE_ENV !== 'production') {
  11. console.error(`[vuex] unknown action type: ${type}`)
  12. }
  13. return
  14. }
  15. this._actionSubscribers.forEach(sub => sub(action, this.state))
  16. return entry.length > 1
  17. ? Promise.all(entry.map(handler => handler(payload)))
  18. : entry[0](payload)
  19. }

这里传入的 _type 就是 actiontype,我们可以从 store._actions 找到对应的函数数组,遍历它们执行获取到每个 handler 然后执行,实际上就是执行了 wrappedActionHandler(payload),接着会执行我们定义的 action 函数,并传入一个对象,包含了当前模块下的 dispatchcommitgettersstate,以及全局的 rootStaterootGetters,所以我们定义的 action 函数能拿到当前模块下的 commit 方法。

因此 action 比我们自己写一个函数执行异步操作然后提交 muataion 的好处是在于它可以在参数中获取到当前模块的一些方法和状态,Vuex 帮我们做好了这些。

语法糖

我们知道 storeStore 对象的一个实例,它是一个原生的 Javascript 对象,我们可以在任意地方使用它们。但大部分的使用场景还是在组件中使用,那么我们之前介绍过,在 Vuex 安装阶段,它会往每一个组件实例上混入 beforeCreate 钩子函数,然后往组件实例上添加一个 $store 的实例,它指向的就是我们实例化的 store,因此我们可以在组件中访问到 store 的任何属性和方法。

比如我们在组件中访问 state

  1. const Counter = {
  2. template: `<div>{{ count }}</div>`,
  3. computed: {
  4. count () {
  5. return this.$store.state.count
  6. }
  7. }
  8. }

但是当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。同样这些问题也在存于 gettermutationaction

为了解决这个问题,Vuex 提供了一系列 mapXXX 辅助函数帮助我们实现在组件中可以很方便的注入 store 的属性和方法。

mapState

我们先来看一下 mapState 的用法:

  1. // 在单独构建的版本中辅助函数为 Vuex.mapState
  2. import { mapState } from 'vuex'
  3. export default {
  4. // ...
  5. computed: mapState({
  6. // 箭头函数可使代码更简练
  7. count: state => state.count,
  8. // 传字符串参数 'count' 等同于 `state => state.count`
  9. countAlias: 'count',
  10. // 为了能够使用 `this` 获取局部状态,必须使用常规函数
  11. countPlusLocalState (state) {
  12. return state.count + this.localCount
  13. }
  14. })
  15. }

再来看一下 mapState 方法的定义,在 src/helpers.js 中:

  1. export const mapState = normalizeNamespace((namespace, states) => {
  2. const res = {}
  3. normalizeMap(states).forEach(({ key, val }) => {
  4. res[key] = function mappedState () {
  5. let state = this.$store.state
  6. let getters = this.$store.getters
  7. if (namespace) {
  8. const module = getModuleByNamespace(this.$store, 'mapState', namespace)
  9. if (!module) {
  10. return
  11. }
  12. state = module.context.state
  13. getters = module.context.getters
  14. }
  15. return typeof val === 'function'
  16. ? val.call(this, state, getters)
  17. : state[val]
  18. }
  19. // mark vuex getter for devtools
  20. res[key].vuex = true
  21. })
  22. return res
  23. })
  24. function normalizeNamespace (fn) {
  25. return (namespace, map) => {
  26. if (typeof namespace !== 'string') {
  27. map = namespace
  28. namespace = ''
  29. } else if (namespace.charAt(namespace.length - 1) !== '/') {
  30. namespace += '/'
  31. }
  32. return fn(namespace, map)
  33. }
  34. }
  35. function normalizeMap (map) {
  36. return Array.isArray(map)
  37. ? map.map(key => ({ key, val: key }))
  38. : Object.keys(map).map(key => ({ key, val: map[key] }))
  39. }

首先 mapState 是通过执行 normalizeNamespace 返回的函数,它接收 2 个参数,其中 namespace 表示命名空间,map 表示具体的对象,namespace 可不传,稍后我们来介绍 namespace 的作用。

当执行 mapState(map) 函数的时候,实际上就是执行 normalizeNamespace 包裹的函数,然后把 map 作为参数 states 传入。

mapState 最终是要构造一个对象,每个对象的元素都是一个方法,因为这个对象是要扩展到组件的 computed 计算属性中的。函数首先执行 normalizeMap 方法,把这个 states 变成一个数组,数组的每个元素都是 {key, val} 的形式。接着再遍历这个数组,以 key 作为对象的 key,值为一个 mappedState 的函数,在这个函数的内部,获取到 $store.getters$store.state,然后再判断数组的 val 如果是一个函数,执行该函数,传入 stategetters,否则直接访问 state[val]

比起一个个手动声明计算属性,mapState 确实要方便许多,下面我们来看一下 namespace 的作用。

当我们想访问一个子模块的 state 的时候,我们可能需要这样访问:

  1. computed: {
  2. mapState({
  3. a: state => state.some.nested.module.a,
  4. b: state => state.some.nested.module.b
  5. })
  6. },

这样从写法上就很不友好,mapState 支持传入 namespace, 因此我们可以这么写:

  1. computed: {
  2. mapState('some/nested/module', {
  3. a: state => state.a,
  4. b: state => state.b
  5. })
  6. },

这样看起来就清爽许多。在 mapState 的实现中,如果有 namespace,则尝试去通过 getModuleByNamespace(this.$store, 'mapState', namespace) 对应的 module,然后把 stategetters 修改为 module 对应的 stategetters

  1. function getModuleByNamespace (store, helper, namespace) {
  2. const module = store._modulesNamespaceMap[namespace]
  3. if (process.env.NODE_ENV !== 'production' && !module) {
  4. console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  5. }
  6. return module
  7. }

我们在 Vuex 初始化执行 installModule 的过程中,初始化了这个映射表:

  1. function installModule (store, rootState, path, module, hot) {
  2. // ...
  3. const namespace = store._modules.getNamespace(path)
  4. // register in namespace map
  5. if (module.namespaced) {
  6. store._modulesNamespaceMap[namespace] = module
  7. }
  8. // ...
  9. }

mapGetters

我们先来看一下 mapGetters 的用法:

  1. import { mapGetters } from 'vuex'
  2. export default {
  3. // ...
  4. computed: {
  5. // 使用对象展开运算符将 getter 混入 computed 对象中
  6. mapGetters([
  7. 'doneTodosCount',
  8. 'anotherGetter',
  9. // ...
  10. ])
  11. }
  12. }

mapState 类似,mapGetters 是将 store 中的 getter 映射到局部计算属性,来看一下它的定义:

  1. export const mapGetters = normalizeNamespace((namespace, getters) => {
  2. const res = {}
  3. normalizeMap(getters).forEach(({ key, val }) => {
  4. // thie namespace has been mutate by normalizeNamespace
  5. val = namespace + val
  6. res[key] = function mappedGetter () {
  7. if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
  8. return
  9. }
  10. if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
  11. console.error(`[vuex] unknown getter: ${val}`)
  12. return
  13. }
  14. return this.$store.getters[val]
  15. }
  16. // mark vuex getter for devtools
  17. res[key].vuex = true
  18. })
  19. return res
  20. })

mapGetters 也同样支持 namespace,如果不写 namespace ,访问一个子 module 的属性需要写很长的 key,一旦我们使用了 namespace,就可以方便我们的书写,每个 mappedGetter 的实现实际上就是取 this.$store.getters[val]

mapMutations

我们可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 的调用。

我们先来看一下 mapMutations 的用法:

  1. import { mapMutations } from 'vuex'
  2. export default {
  3. // ...
  4. methods: {
  5. ...mapMutations([
  6. 'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
  7. // `mapMutations` 也支持载荷:
  8. 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
  9. ]),
  10. ...mapMutations({
  11. add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
  12. })
  13. }
  14. }

mapMutations 支持传入一个数组或者一个对象,目标都是组件中对应的 methods 映射为 store.commit 的调用。来看一下它的定义:

  1. export const mapMutations = normalizeNamespace((namespace, mutations) => {
  2. const res = {}
  3. normalizeMap(mutations).forEach(({ key, val }) => {
  4. res[key] = function mappedMutation (...args) {
  5. // Get the commit method from store
  6. let commit = this.$store.commit
  7. if (namespace) {
  8. const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
  9. if (!module) {
  10. return
  11. }
  12. commit = module.context.commit
  13. }
  14. return typeof val === 'function'
  15. ? val.apply(this, [commit].concat(args))
  16. : commit.apply(this.$store, [val].concat(args))
  17. }
  18. })
  19. return res
  20. })

可以看到 mappedMutation 同样支持了 namespace,并且支持了传入额外的参数 args,作为提交 mutationpayload,最终就是执行了 store.commit 方法,并且这个 commit 会根据传入的 namespace 映射到对应 modulecommit 上。

mapActions

我们可以在组件中使用 this.$store.dispatch('xxx') 提交 action,或者使用 mapActions 辅助函数将组件中的 methods 映射为 store.dispatch 的调用。

mapActions 在用法上和 mapMutations 几乎一样,实现也很类似:

  1. export const mapActions = normalizeNamespace((namespace, actions) => {
  2. const res = {}
  3. normalizeMap(actions).forEach(({ key, val }) => {
  4. res[key] = function mappedAction (...args) {
  5. // get dispatch function from store
  6. let dispatch = this.$store.dispatch
  7. if (namespace) {
  8. const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
  9. if (!module) {
  10. return
  11. }
  12. dispatch = module.context.dispatch
  13. }
  14. return typeof val === 'function'
  15. ? val.apply(this, [dispatch].concat(args))
  16. : dispatch.apply(this.$store, [val].concat(args))
  17. }
  18. })
  19. return res
  20. })

mapMutations 的实现几乎一样,不同的是把 commit 方法换成了 dispatch

动态更新模块

在 Vuex 初始化阶段我们构造了模块树,初始化了模块上各个部分。在有一些场景下,我们需要动态去注入一些新的模块,Vuex 提供了模块动态注册功能,在 store 上提供了一个 registerModule 的 API。

  1. registerModule (path, rawModule, options = {}) {
  2. if (typeof path === 'string') path = [path]
  3. if (process.env.NODE_ENV !== 'production') {
  4. assert(Array.isArray(path), `module path must be a string or an Array.`)
  5. assert(path.length > 0, 'cannot register the root module by using registerModule.')
  6. }
  7. this._modules.register(path, rawModule)
  8. installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  9. // reset store to update getters...
  10. resetStoreVM(this, this.state)
  11. }

registerModule 支持传入一个 path 模块路径 和 rawModule 模块定义,首先执行 register 方法扩展我们的模块树,接着执行 installModule 去安装模块,最后执行 resetStoreVM 重新实例化 store._vm,并销毁旧的 store._vm

相对的,有动态注册模块的需求就有动态卸载模块的需求,Vuex 提供了模块动态卸载功能,在 store 上提供了一个 unregisterModule 的 API。

  1. unregisterModule (path) {
  2. if (typeof path === 'string') path = [path]
  3. if (process.env.NODE_ENV !== 'production') {
  4. assert(Array.isArray(path), `module path must be a string or an Array.`)
  5. }
  6. this._modules.unregister(path)
  7. this._withCommit(() => {
  8. const parentState = getNestedState(this.state, path.slice(0, -1))
  9. Vue.delete(parentState, path[path.length - 1])
  10. })
  11. resetStore(this)
  12. }

unregisterModule 支持传入一个 path 模块路径,首先执行 unregister 方法去修剪我们的模块树:

  1. unregister (path) {
  2. const parent = this.get(path.slice(0, -1))
  3. const key = path[path.length - 1]
  4. if (!parent.getChild(key).runtime) return
  5. parent.removeChild(key)
  6. }

注意,这里只会移除我们运行时动态创建的模块。

接着会删除 state 在该路径下的引用,最后执行 resetStore 方法:

  1. function resetStore (store, hot) {
  2. store._actions = Object.create(null)
  3. store._mutations = Object.create(null)
  4. store._wrappedGetters = Object.create(null)
  5. store._modulesNamespaceMap = Object.create(null)
  6. const state = store.state
  7. // init all modules
  8. installModule(store, state, [], store._modules.root, true)
  9. // reset vm
  10. resetStoreVM(store, state, hot)
  11. }

该方法就是把 store 下的对应存储的 _actions_mutations_wrappedGetters_modulesNamespaceMap 都清空,然后重新执行 installModule 安装所有模块以及 resetStoreVM 重置 store._vm

总结

那么至此,Vuex 提供的一些常用 API 我们就分析完了,包括数据的存取、语法糖、模块的动态更新等。要理解 Vuex 提供这些 API 都是方便我们在对 store 做各种操作来完成各种能力,尤其是 mapXXX 的设计,让我们在使用 API 的时候更加方便,这也是我们今后在设计一些 JavaScript 库的时候,从 API 设计角度中应该学习的方向。

原文: https://ustbhuangyi.github.io/vue-analysis/vuex/api.html