• API
    • 数据获取
    • 数据存储
    • 语法糖
      • mapState
      • mapGetters
      • mapMutations
      • mapActions
    • 动态更新模块
    • 总结

    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