一、API 的理解
-
之前我们对
Vuex
的初始化过程有了深入的分析,在我们构造好这个store
后,需要提供一些API
对这个store
做存取的操作,那么就从源码的角度对这些API
做分析。 -
数据获取,
Vuex
最终存储的数据是在state
上的,我们之前分析过在store.state
存储的是root state
,那么对于模块上的state
,假设我们有两个嵌套的modules
,它们的key
分别为a
和b
,我们可以通过store.state.a.b.xxx
的方式去获取。它的实现是在发生在installModule
的时候:
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
// ...
// set state
if (!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 和 局部 getters
return 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 + key
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
)
}
}
function resetStoreVM (store, state, hot) {
// ...
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[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 + key
registerMutation(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 commit
const {
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 + key
const handler = action.handler || action
registerAction(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 dispatch
const {
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.mapState
import { 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.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} 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
返回的函数,它接收两个参数,其中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 map
if (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 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
})
-
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 store
let commit = this.$store.commit
if (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 store
let dispatch = this.$store.dispatch
if (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) return
parent.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 modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}
该方法就是把
store
下的对应存储的_actions
、_mutations
、_wrappedGetters
和_modulesNamespaceMap
都清空,然后重新执行installModule
安装所有模块以及resetStoreVM
重置store._vm
。
- 总结:
Vuex
提供的一些常用API
我们就分析完了,包括数据的存取、语法糖、模块的动态更新等。要理解Vuex
提供这些API
都是方便我们在对store
做各种操作来完成各种能力,尤其是mapXXX
的设计,让我们在使用API
的时候更加方便,这也是我们今后在设计一些JavaScript
库的时候,从API
设计角度中应该学习的方向。
二、 插件
-
Vuex
除了提供的存取能力,还提供了一种插件能力,让我们可以监控store
的变化过程来做一些事情。 -
Vuex
的store
接受plugins
选项,我们在实例化Store
的时候可以传入插件,它是一个数组,然后在执行Store
构造函数的时候,会执行这些插件:
const {
plugins = [],
strict = false
} = options
// apply plugins
plugins.forEach(plugin => plugin(this))
在我们实际项目中,我们用到的最多的就是 Vuex 内置的
Logger
插件,它能够帮我们追踪state
变化,然后输出一些格式化日志。下面我们就来分析这个插件的实现。
Logger
插件,Logger
插件的定义在src/plugins/logger.js
中:
export default function createLogger ({
collapsed = true,
filter = (mutation, stateBefore, stateAfter) => true,
transformer = state => state,
mutationTransformer = mut => mut,
logger = console
} = {}) {
return store => {
let prevState = deepCopy(store.state)
store.subscribe((mutation, state) => {
if (typeof logger === 'undefined') {
return
}
const nextState = deepCopy(state)
if (filter(mutation, prevState, nextState)) {
const time = new Date()
const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
const formattedMutation = mutationTransformer(mutation)
const message = `mutation ${mutation.type}${formattedTime}`
const startMessage = collapsed
? logger.groupCollapsed
: logger.group
// render
try {
startMessage.call(logger, message)
} catch (e) {
console.log(message)
}
logger.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
logger.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
logger.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))
try {
logger.groupEnd()
} catch (e) {
logger.log('—— log end ——')
}
}
prevState = nextState
})
}
}
function repeat (str, times) {
return (new Array(times + 1)).join(str)
}
function pad (num, maxLength) {
return repeat('0', maxLength - num.toString().length) + num
}
- 插件函数接收的参数是
store
实例,它执行了store.subscribe
方法,先来看一下subscribe
的定义:
subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
}
function genericSubscribe (fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
subscribe
的逻辑很简单,就是往this._subscribers
去添加一个函数,并返回一个unsubscribe
的方法。而我们在执行store.commit
的方法的时候,会遍历this._subscribers
执行它们对应的回调函数:
commit (_type, _payload, _options) {
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// ...
this._subscribers.forEach(sub => sub(mutation, this.state))
}
-
回到我们的
Logger
函数,它相当于订阅了mutation
的提交,它的prevState
表示之前的state
,nextState
表示提交mutation
后的state
,这两个state
都需要执行deepCopy
方法拷贝一份对象的副本,这样对他们的修改就不会影响原始store.state
。 -
接下来就构造一些格式化的消息,打印出一些时间消息
message
, 之前的状态prevState
,对应的mutation
操作formattedMutation
以及下一个状态nextState
。最后更新prevState = nextState
,为下一次提交mutation
输出日志做准备。 -
总结:
Vuex
的插件分析就结束了,Vuex
从设计上支持了插件,让我们很好地从外部追踪store
内部的变化,Logger
插件在我们的开发阶段也提供了很好地指引作用。当然我们也可以自己去实现Vuex
的插件,来帮助我们实现一些特定的需求。