# Getters 详解

# getter 使用方法

其实 getters 很像我们在 Vue 中使用的计算属性,它的用法也很简单:

const store = new Vuex.Store({
  state: {
    firstName: 'Jerry',
    lastName: 'Yuan'
  },
  getters: {
    fullname(state) {
      return state.firstName + ' ' + state.lastName
    }
  }
})

我们在 Vue 里面,可以这么来使用 getters

export default {
  data () {
    return {
      name: this.$store.getters.fullname
    }
  }
}

是不是很简单。本节我们就来看看它的内部实现原理。

# getter 实现原理

还记得上一节我们分析 state 的时候,在 Store 构造函数里面会初始化一些内部的变量,其中有两个是跟 getters 相关的:

this._wrappedGetters = Object.create(null)
this._makeLocalGettersCache = Object.create(null)

这两个一看就是跟 getters 相关的有木有!根据名字,我们看出来 _wrappedGetters 是一个包装了 getters 的对象,而 _makeLocalGettersCache 则是跟缓存相关的对象,本节我们分析不到这个变量。

我们继续往下看,接着就到了我们之前介绍过的:

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

这里面的逻辑我们就不啰嗦了,上一节都有介绍过,我们只看跟 getters 相关的逻辑即可。所以我们来看这一段代码:

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

生成局部上下文时,会初始化我们的 getters 对象,我们来看一下:

function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''
  const local = {} // 省略 ...
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })
  return local

我们给 local 扩充属性的时候,定义了一个 getters,它是一个函数,只有在调用的时候才会求值,是懒加载的一种实现。它返回的值是 store.getters。注意的是,这个 makeLocalContext 函数返回的值,是赋值给我们的module.context 的,在我们的例子中,也就是根模块 store._modules.root。我们继续看后面会对这个 local 做什么操作:

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

这个 module.forEachGetter 方法是在 module/module.jsModule 类中定义的,很简单:

forEachGetter (fn) {
  if (this._rawModule.getters) {
    forEachValue(this._rawModule.getters, fn)
  }
}

在上一节我们分析过了,此时的 this._rawModule 对应的正是我们的 options,所以这里的 getters 是:

{
  fullname(state) {
    return state.firstName + ' ' + state.lastName
  }
}

所以会遍历这个 getters 对象,用 fn 函数操作它。这个 forEachValue 函数也很简单:

export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

就是遍历这个对象的属性,然后用传入的函数来操作这些属性,属性值作为第一个参数,属性的 key 作为第二个参数,比如:

forEachValue({name:'jerry', age: 12}, (value, key) => {
  console.info(key, value)
})

>> name jerry
>> 12 age

其实对于这些工具函数,如果不知道它的意思,但是又想最快的知道它的用途,可以去对应的单元测试里面去看看,在 test/unit/* 下。这也是源码学习的一个技巧。

好了,所以此时这个函数中: module.forEachGetter((getter, key) => { ... }) 的参数 getter 是我们的函数:

function fullname(state) {
  return state.firstName + ' ' + state.lastName
}

key 就是 fullname 了。其实看上去有点绕,但是实际上做的就是一件事,遍历 getters,用回调来处理里面的每一个属性和值。所以我们来看看回调中的这句代码:

registerGetter(store, namespacedType, getter, local)

它的实现:

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

很清晰。首先先检查有没有重复的定义 getter,如果有的话在开发环境下报错。如果没有定义过,那么就给 store._wrappedGetters 对象添加这个属性,它的 key 是我们的 getter 的名字,值是一个函数:

function wrappedGetter (store) {
  return rawGetter(
    local.state, // local state
    local.getters, // local getters
    store.state, // root state
    store.getters // root getters
  )
}

这个函数的参数是 store,函数只做了一件事,就是调用我们的 rawGetter 函数,并且给它填充几个参数,注释写的也很清晰,分别是局部 state,局部 getters,根 state,根 getters。所以我们在定义 getter 的时候,第一个参数是当前模块下的 state, 第二个参数是当前模块下的 getter,后面两个在模块下使用的,后续再说。

到这里,我们就给 store._wrappedGetters 赋上了我们定义的 getters 了,继续回到 Store 的构造函数往下看:

resetStoreVM(store, state, hot)

这个函数我们上一节分析过,不过是针对 state 的,这一节我们可以针对性的分析 getter 了,具体的逻辑在这里:

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  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 environment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent
  // 省略 ....
}

可以看到,我们遍历了包含 gettersstore._wrappedGetters 对象,并且赋值给了 computed 对象,这个对象会作为 Vue 实例中的计算属性传入。我们来分析一下给计算属性赋值的过程:

computed[key] = partial(fn, store)

其中 partial 的定义:

export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}

所以,此时 computed['fullname'] 的值是:

function () {
  return fn(arg)
}

思考一下,这里为什么用 partial 包一层呢?其实这也是一种懒加载的模式。因为我们返回的是一个函数,这个函数没被调用之前,它的内部是不确定的。即这个时候,fn 的值是未知的,得运行时才能确定。另外这里还考虑了行内函数引起的闭包的问题。

此时,对 Vue 源码了解的同学应该知道,这个计算属性对应的 computedWatchergetter属性其实就是上面这个函数。 这个我们后面对计算属性求值的时候会用到。

接着,往 store.getters 中添加这个属性,它是只读的,它的值是 store._vm[key]

好了,现在我们的 getters 的初始化就完成了。相信这时候,你已经知道我们在页面使用 this.$store.getters.fullname 是如何获取到这个正确的值了吧。我们根据这个例子再走一遍流程吧。使用 this.$store.getters.fullname 获取 fullname,会触发 get 访问器,也就是这里定义的:

Object.defineProperty(store.getters, key, {
  get: () => store._vm[key],
  enumerable: true // for local getters
})

出发了访问器后,会执行函数来获取值,这里的值就是 store._vm['fullname'],然而,store._vm 是一个 Vue 实例,访问它的属性会触发 Vue 的响应式系统获取值。这里会触发 fullname 对应的 computedwatcher 的求值,也就是会调用:

function () {
    return fn(arg)
  }

这里的 arg 是我们的 storefn 是一个包装函数,我们上面分析过了:

function wrappedGetter (store) {
  return rawGetter(
    local.state, // local state
    local.getters, // local getters
    store.state, // root state
    store.getters // root getters
  )
}

所以最终会触发 rawGetter 这个函数的执行, rawGetter 其实就是我们的自己定义的获取函数:

function fullname(state) {
  return state.firstName + ' ' + state.lastName
}

顺便提一下,在这个 getter 函数中使用 state.firstNamestate.lastName 同样的会触发 Vue 的响应式系统,这个也就是之前分析过的 state 的取值过程。

# mapGetters 使用方法

state 类似,我们的 getters 也有类似的 map 方法:




















 



const store = new Vuex.Store({
  state: {
    firstName: 'Jerry',
    lastName: 'Yuan'
  },
  getters: {
    fullname(state) {
      return state.firstName + ' ' + state.lastName
    },
    reversedFullname(state) {
      return state.lastName + ' ' + state.firstName
    }
  }
})

var vm = new Vue({
  el: '#app',
  store,
  computed: {
    ...Vuex.mapGetters(['fullname', 'reversedFullname'])
  }
})

它的实现也很简单,有了上一节对 mapState 的理解,这里就很轻松了。

# mapGetters 实现原理

它的实现也在 src/helper.js 中:

/**
 * Reduce the code which written in Vue.js for getting the getters
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} getters
 * @return {Object}
 */
export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  if (process.env.NODE_ENV !== 'production' && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      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
})

首先一开始,还是检查了 getters 的类型,如果不是数组或者对象的话,会在开发环境报一个错误。接着我们对 getters 做一次 normalize,然后在循环中对它的 keyval 做处理。它会我们的 res 对象添加属性,keygetterkey,值是一个 mappedGetter 的函数,我们来看一下这个内部的函数的实现:

res[key] = function mappedGetter () {
  if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
    return
  }
  if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
    console.error(`[vuex] unknown getter: ${val}`)
    return
  }
  return this.$store.getters[val]
}

第一个判断是针对 namespace 的,这里我们不用管。第二个判断是如果没有找到对应的 getter 也会在开发环境给用户报错。找到的话,就直接返回这个 this.$store.getters[val],也就是说,是直接通过 key 来访问 this.$store.getters 对象的。

注意了,这里的 this 也是运行时绑定的,由于我们是在 Vue 实例中运行的,所以这里的 this 就是我们当前的 Vue 实例。

最后返回 res 对象,包含我们需要的这些 getters

# 总结

本节我们介绍了 gettersmapGetters 的使用和实现原理,通过上面的分析可以看出,它其实是借用 Vue 的计算属性实现的。当我们访问一个 getter 的时候,实际上是访问了当前 store 中的 _vm 实例上的对应的计算属性,触发计算属性的求值,最后得出我们的 getter 的值。 然后关于 mapGetters 也和我们上一节介绍的 mapState 大同小异,主要是让开发者少写一点重复代码,内部的实现其实就是遍历传入的 getters 对象,然后依次给你转成 getters 的实现。下一节我们将介绍如何利用 mutation 来修改我们的 state

上次更新: 6/2/2020, 4:11:13 PM