- API
- 数据获取
- 数据存储
- 语法糖
- mapState
- mapGetters
- mapMutations
- mapActions
- 动态更新模块
- 总结
API
上一节我们对 Vuex 的初始化过程有了深入的分析,在我们构造好这个 store 后,需要提供一些 API 对这个 store 做存取的操作,那么这一节我们就从源码的角度对这些 API 做分析。
数据获取
Vuex 最终存储的数据是在 state 上的,我们之前分析过在 store.state 存储的是 root state,那么对于模块上的 state,假设我们有 2 个嵌套的 modules,它们的 key 分别为 a 和 b,我们可以通过 store.state.a.b.xxx 的方式去获取。它的实现是在发生在 installModule 的时候:
function installModule (store, rootState, path, module, hot) {const isRoot = !path.length// ...// set stateif (!isRoot && !hot) {const parentState = getNestedState(rootState, path.slice(0, -1))const moduleName = path[path.length - 1]store._withCommit(() => {Vue.set(parentState, moduleName, module.state)})}// ...}
在递归执行 installModule 的过程中,就完成了整个 state 的建设,这样我们就可以通过 module 名的 path 去访问到一个深层 module 的 state。
有些时候,我们获取的数据不仅仅是一个 state,而是由多个 state 计算而来,Vuex 提供了 getters,允许我们定义一个 getter 函数,如下:
getters: {total (state, getters, localState, localGetters) {// 可访问全局 state 和 getters,以及如果是在 modules 下面,可以访问到局部 state 和 局部 gettersreturn state.a + state.b}}
我们在 installModule 的过程中,递归执行了所有 getters 定义的注册,在之后的 resetStoreVM 过程中,执行了 store.getters 的初始化工作:
function installModule (store, rootState, path, module, hot) {// ...const namespace = store._modules.getNamespace(path)// ...const local = module.context = makeLocalContext(store, namespace, path)// ...module.forEachGetter((getter, key) => {const namespacedType = namespace + keyregisterGetter(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 statelocal.getters, // local gettersstore.state, // root statestore.getters // root getters)}}function resetStoreVM (store, state, hot) {// ...// bind store public gettersstore.getters = {}const wrappedGetters = store._wrappedGettersconst computed = {}forEachValue(wrappedGetters, (fn, key) => {// use computed to leverage its lazy-caching mechanismcomputed[key] = () => 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// ...store._vm = new Vue({data: {$$state: state},computed})// ...}
在 installModule 的过程中,为建立了每个模块的上下文环境,因此当我们访问 store.getters.xxx 的时候,实际上就是执行了 rawGetter(local.state,…),rawGetter 就是我们定义的 getter 方法,这也就是为什么我们的 getter 函数支持这四个参数,并且除了全局的 state 和 getter 外,我们还可以访问到当前 module 下的 state 和 getter。
数据存储
Vuex 对数据存储的存储本质上就是对 state 做修改,并且只允许我们通过提交 mutaion 的形式去修改 state,mutation 是一个函数,如下:
mutations: {increment (state) {state.count++}}
mutations 的初始化也是在 installModule 的时候:
function installModule (store, rootState, path, module, hot) {// ...const namespace = store._modules.getNamespace(path)// ...const local = module.context = makeLocalContext(store, namespace, path)module.forEachMutation((mutation, key) => {const namespacedType = namespace + keyregisterMutation(store, namespacedType, mutation, local)})// ...}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 提供了commit 方法让我们提交一个 mutation:
commit (_type, _payload, _options) {// check object-style commitconst {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')}}
这里传入的 _type 就是 mutation 的 type,我们可以从 store._mutations 找到对应的函数数组,遍历它们执行获取到每个 handler 然后执行,实际上就是执行了 wrappedMutationHandler(playload),接着会执行我们定义的 mutation 函数,并传入当前模块的 state,所以我们的 mutation 函数也就是对当前模块的 state 做修改。
需要注意的是, mutation 必须是同步函数,但是我们在开发实际项目中,经常会遇到要先去发送一个请求,然后根据请求的结果去修改 state,那么单纯只通过 mutation 是无法完成需求,因此 Vuex 又给我们设计了一个 action 的概念。
action 类似于 mutation,不同在于 action 提交的是 mutation,而不是直接操作 state,并且它可以包含任意异步操作。例如:
mutations: {increment (state) {state.count++}},actions: {increment (context) {setTimeout(() => {context.commit('increment')}, 0)}}
actions 的初始化也是在 installModule 的时候:
function installModule (store, rootState, path, module, hot) {// ...const namespace = store._modules.getNamespace(path)// ...const local = module.context = makeLocalContext(store, namespace, path)module.forEachAction((action, key) => {const type = action.root ? key : namespace + keyconst handler = action.handler || actionregisterAction(store, type, handler, local)} )// ...}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}})}
store 提供了dispatch 方法让我们提交一个 action:
dispatch (_type, _payload) {// check object-style dispatchconst {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)}
这里传入的 _type 就是 action 的 type,我们可以从 store._actions 找到对应的函数数组,遍历它们执行获取到每个 handler 然后执行,实际上就是执行了 wrappedActionHandler(payload),接着会执行我们定义的 action 函数,并传入一个对象,包含了当前模块下的 dispatch、commit、getters、state,以及全局的 rootState 和 rootGetters,所以我们定义的 action 函数能拿到当前模块下的 commit 方法。
因此 action 比我们自己写一个函数执行异步操作然后提交 muataion 的好处是在于它可以在参数中获取到当前模块的一些方法和状态,Vuex 帮我们做好了这些。
语法糖
我们知道 store 是 Store 对象的一个实例,它是一个原生的 Javascript 对象,我们可以在任意地方使用它们。但大部分的使用场景还是在组件中使用,那么我们之前介绍过,在 Vuex 安装阶段,它会往每一个组件实例上混入 beforeCreate 钩子函数,然后往组件实例上添加一个 $store 的实例,它指向的就是我们实例化的 store,因此我们可以在组件中访问到 store 的任何属性和方法。
比如我们在组件中访问 state:
const Counter = {template: `<div>{{ count }}</div>`,computed: {count () {return this.$store.state.count}}}
但是当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。同样这些问题也在存于 getter、mutation 和 action。
为了解决这个问题,Vuex 提供了一系列 mapXXX 辅助函数帮助我们实现在组件中可以很方便的注入 store 的属性和方法。
mapState
我们先来看一下 mapState 的用法:
// 在单独构建的版本中辅助函数为 Vuex.mapStateimport { mapState } from 'vuex'export default {// ...computed: mapState({// 箭头函数可使代码更简练count: state => state.count,// 传字符串参数 'count' 等同于 `state => state.count`countAlias: 'count',// 为了能够使用 `this` 获取局部状态,必须使用常规函数countPlusLocalState (state) {return state.count + this.localCount}})}
再来看一下 mapState 方法的定义,在 src/helpers.js 中:
export const mapState = normalizeNamespace((namespace, states) => {const res = {}normalizeMap(states).forEach(({ key, val }) => {res[key] = function mappedState () {let state = this.$store.statelet getters = this.$store.gettersif (namespace) {const module = getModuleByNamespace(this.$store, 'mapState', namespace)if (!module) {return}state = module.context.stategetters = module.context.getters}return typeof val === 'function'? val.call(this, state, getters): state[val]}// mark vuex getter for devtoolsres[key].vuex = true})return res})function normalizeNamespace (fn) {return (namespace, map) => {if (typeof namespace !== 'string') {map = namespacenamespace = ''} else if (namespace.charAt(namespace.length - 1) !== '/') {namespace += '/'}return fn(namespace, map)}}function normalizeMap (map) {return Array.isArray(map)? map.map(key => ({ key, val: key })): Object.keys(map).map(key => ({ key, val: map[key] }))}
首先 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 如果是一个函数,执行该函数,传入 state 和 getters,否则直接访问 state[val]。
比起一个个手动声明计算属性,mapState 确实要方便许多,下面我们来看一下 namespace 的作用。
当我们想访问一个子模块的 state 的时候,我们可能需要这样访问:
computed: {mapState({a: state => state.some.nested.module.a,b: state => state.some.nested.module.b})},
这样从写法上就很不友好,mapState 支持传入 namespace, 因此我们可以这么写:
computed: {mapState('some/nested/module', {a: state => state.a,b: state => state.b})},
这样看起来就清爽许多。在 mapState 的实现中,如果有 namespace,则尝试去通过 getModuleByNamespace(this.$store, 'mapState', namespace) 对应的 module,然后把 state 和 getters 修改为 module 对应的 state 和 getters。
function getModuleByNamespace (store, helper, namespace) {const module = store._modulesNamespaceMap[namespace]if (process.env.NODE_ENV !== 'production' && !module) {console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)}return module}
我们在 Vuex 初始化执行 installModule 的过程中,初始化了这个映射表:
function installModule (store, rootState, path, module, hot) {// ...const namespace = store._modules.getNamespace(path)// register in namespace mapif (module.namespaced) {store._modulesNamespaceMap[namespace] = module}// ...}
mapGetters
我们先来看一下 mapGetters 的用法:
import { mapGetters } from 'vuex'export default {// ...computed: {// 使用对象展开运算符将 getter 混入 computed 对象中mapGetters(['doneTodosCount','anotherGetter',// ...])}}
和 mapState 类似,mapGetters 是将 store 中的 getter 映射到局部计算属性,来看一下它的定义:
export const mapGetters = normalizeNamespace((namespace, getters) => {const res = {}normalizeMap(getters).forEach(({ key, val }) => {// thie namespace has been mutate by normalizeNamespaceval = namespace + valres[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 devtoolsres[key].vuex = true})return res})
mapGetters 也同样支持 namespace,如果不写 namespace ,访问一个子 module 的属性需要写很长的 key,一旦我们使用了 namespace,就可以方便我们的书写,每个 mappedGetter 的实现实际上就是取 this.$store.getters[val]。
mapMutations
我们可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 的调用。
我们先来看一下 mapMutations 的用法:
import { mapMutations } from 'vuex'export default {// ...methods: {...mapMutations(['increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`// `mapMutations` 也支持载荷:'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`]),...mapMutations({add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`})}}
mapMutations 支持传入一个数组或者一个对象,目标都是组件中对应的 methods 映射为 store.commit 的调用。来看一下它的定义:
export const mapMutations = normalizeNamespace((namespace, mutations) => {const res = {}normalizeMap(mutations).forEach(({ key, val }) => {res[key] = function mappedMutation (...args) {// Get the commit method from storelet commit = this.$store.commitif (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})
可以看到 mappedMutation 同样支持了 namespace,并且支持了传入额外的参数 args,作为提交 mutation 的 payload,最终就是执行了 store.commit 方法,并且这个 commit 会根据传入的 namespace 映射到对应 module 的 commit 上。
mapActions
我们可以在组件中使用 this.$store.dispatch('xxx') 提交 action,或者使用 mapActions 辅助函数将组件中的 methods 映射为 store.dispatch 的调用。
mapActions 在用法上和 mapMutations 几乎一样,实现也很类似:
export const mapActions = normalizeNamespace((namespace, actions) => {const res = {}normalizeMap(actions).forEach(({ key, val }) => {res[key] = function mappedAction (...args) {// get dispatch function from storelet dispatch = this.$store.dispatchif (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})
和 mapMutations 的实现几乎一样,不同的是把 commit 方法换成了 dispatch。
动态更新模块
在 Vuex 初始化阶段我们构造了模块树,初始化了模块上各个部分。在有一些场景下,我们需要动态去注入一些新的模块,Vuex 提供了模块动态注册功能,在 store 上提供了一个 registerModule 的 API。
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)}
registerModule 支持传入一个 path 模块路径 和 rawModule 模块定义,首先执行 register 方法扩展我们的模块树,接着执行 installModule 去安装模块,最后执行 resetStoreVM 重新实例化 store._vm,并销毁旧的 store._vm。
相对的,有动态注册模块的需求就有动态卸载模块的需求,Vuex 提供了模块动态卸载功能,在 store 上提供了一个 unregisterModule 的 API。
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)}
unregisterModule 支持传入一个 path 模块路径,首先执行 unregister 方法去修剪我们的模块树:
unregister (path) {const parent = this.get(path.slice(0, -1))const key = path[path.length - 1]if (!parent.getChild(key).runtime) returnparent.removeChild(key)}
注意,这里只会移除我们运行时动态创建的模块。
接着会删除 state 在该路径下的引用,最后执行 resetStore 方法:
function resetStore (store, hot) {store._actions = Object.create(null)store._mutations = Object.create(null)store._wrappedGetters = Object.create(null)store._modulesNamespaceMap = Object.create(null)const state = store.state// init all modulesinstallModule(store, state, [], store._modules.root, true)// reset vmresetStoreVM(store, state, hot)}
该方法就是把 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
