在构造好这个 store 后,需要提供一些 API 对这个 store 做存取的操作
数据获取
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(() => {if (__DEV__) {if (moduleName in parentState) {console.warn(`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`)}}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 (__DEV__) {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 = {}// reset local getters cachestore._makeLocalGettersCache = Object.create(null)const wrappedGetters = store._wrappedGettersconst 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})})// use a Vue instance to store the state tree// suppress warnings just in case the user has added// some funky global mixinsconst silent = Vue.config.silentVue.config.silent = truestore._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
// _type就是mutation的typecommit (_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 (__DEV__) {console.error(`[vuex] unknown mutation type: ${type}`)}return}this._withCommit(() => {entry.forEach(function commitIterator (handler) {handler(payload)})})this._subscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.forEach(sub => sub(mutation, this.state))if (__DEV__ &&options && options.silent) {console.warn(`[vuex] mutation type: ${type}. Silent option has been removed. ` +'Use the filter functionality in the vue-devtools')}}
可以从 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) {let res = handler.call(store, {dispatch: local.dispatch,commit: local.commit,getters: local.getters,state: local.state,rootGetters: store.getters,rootState: store.state}, payload)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
// _type就是action的typedispatch (_type, _payload) {// check object-style dispatchconst {type,payload} = unifyObjectStyle(_type, _payload)const action = { type, payload }const entry = this._actions[type]if (!entry) {if (__DEV__) {console.error(`[vuex] unknown action type: ${type}`)}return}try {this._actionSubscribers.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe.filter(sub => sub.before).forEach(sub => sub.before(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in before action subscribers: `)console.error(e)}}const result = entry.length > 1? Promise.all(entry.map(handler => handler(payload))): entry[0](payload)return new Promise((resolve, reject) => {result.then(res => {try {this._actionSubscribers.filter(sub => sub.after).forEach(sub => sub.after(action, this.state))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in after action subscribers: `)console.error(e)}}resolve(res)}, error => {try {this._actionSubscribers.filter(sub => sub.error).forEach(sub => sub.error(action, this.state, error))} catch (e) {if (__DEV__) {console.warn(`[vuex] error in error action subscribers: `)console.error(e)}}reject(error)})})}
可以从 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 中
/*** Reduce the code which written in Vue.js for getting the state.* @param {String} [namespace] - Module's namespace* @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.* @param {Object}*/// mapState 通过执行normalizeNamespace返回的函数// namespace表示命名空间 map表示具体的对象 namespace可不传export const mapState = normalizeNamespace((namespace, states) => {const res = {}if (__DEV__ && !isValidMap(states)) {console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')}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})/*** Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.* @param {Function} fn* @return {Function}*/function normalizeNamespace (fn) {return (namespace, map) => {if (typeof namespace !== 'string') {map = namespacenamespace = ''} else if (namespace.charAt(namespace.length - 1) !== '/') {namespace += '/'}return fn(namespace, map)}}/*** Normalize the map* normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]* normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]* @param {Array|Object} map* @return {Array}*/function normalizeMap (map) {if (!isValidMap(map)) {return []}return Array.isArray(map)? map.map(key => ({ key, val: key })): Object.keys(map).map(key => ({ key, val: map[key] }))}
当执行 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
/*** Search a special module from store by namespace. if module not exist, print error message.* @param {Object} store* @param {String} helper* @param {String} namespace* @return {Object}*/function getModuleByNamespace (store, helper, namespace) {const module = store._modulesNamespaceMap[namespace]if (__DEV__ && !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) {if (store._modulesNamespaceMap[namespace] && __DEV__) {console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)}store._modulesNamespaceMap[namespace] = module}// ...}
mapGetters
mapGetters的用法
import { mapGetters } from 'vuex'export default {// ...computed: {// 使用对象展开运算符将 getter 混入 computed 对象中mapGetters(['doneTodosCount','anotherGetter',// ...])}}
和 mapState 类似,mapGetters 是将 store 中的 getter 映射到局部计算属性,来看一下它的定义
/*** 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 (__DEV__ && !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 normalizeNamespaceval = namespace + valres[key] = function mappedGetter () {if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {return}if (__DEV__ && !(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 的调用
来看一下它的定义
/*** Reduce the code which written in Vue.js for committing the mutation* @param {String} [namespace] - Module's namespace* @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept another params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.* @return {Object}*/export const mapMutations = normalizeNamespace((namespace, mutations) => {const res = {}if (__DEV__ && !isValidMap(mutations)) {console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')}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 几乎一样,实现也很类似
/*** Reduce the code which written in Vue.js for dispatch the action* @param {String} [namespace] - Module's namespace* @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function.* @return {Object}*/export const mapActions = normalizeNamespace((namespace, actions) => {const res = {}if (__DEV__ && !isValidMap(actions)) {console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')}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
// path模块路径 rawModule模块定义registerModule (path, rawModule, options = {}) {if (typeof path === 'string') path = [path]if (__DEV__) {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.')}// 执行register方法扩展模块树this._modules.register(path, rawModule)// 安装模块installModule(this, this.state, path, this._modules.get(path), options.preserveState)// reset store to update getters...// 重新实例化store.vm,并销毁旧的store.vmresetStoreVM(this, this.state)}
相对的,有动态注册模块的需求就有动态卸载模块的需求,Vuex 提供了模块动态卸载功能,在 store 上提供了一个 unregisterModule 的 API
// path模块路径unregisterModule (path) {if (typeof path === 'string') path = [path]if (__DEV__) {assert(Array.isArray(path), `module path must be a string or an Array.`)}// 修剪模块树this._modules.unregister(path)this._withCommit(() => {// 删除state在该路径下的引用const parentState = getNestedState(this.state, path.slice(0, -1))Vue.delete(parentState, path[path.length - 1])})// 执行resetStoreresetStore(this)}
unregister
只会移除运行时动态创建的模块
unregister (path) {const parent = this.get(path.slice(0, -1))const key = path[path.length - 1]const child = parent.getChild(key)if (!child) {if (__DEV__) {console.warn(`[vuex] trying to unregister module '${key}', which is ` +`not registered`)}return}if (!child.runtime) {return}parent.removeChild(key)}
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 设计角度中应该学习的方向
