本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这篇文章是从源码角度分析vuex的核心概念,记录下自己花一周时间看vuex源码后的笔记,这里我看的vuex源码版本是
3.6.0,后面有时间后会看vuex4的源码,既然是看核心概念,就跟着vuex的官方文档一起来看vuex3官方文档地址:v3.vuex.vuejs.org/zh/
前期准备
在看源码之前应该要使用过vuex或者对vuex有过了解,这样在看源码的过程中才能更加明白,如果还没有了解的话或者忘记了的话可以跟着文档再学习一下,这里我是通过vuex源码中example文件夹中的例子以及文档再去了解了vuex,而通过源码中的例子学习还有一个好处就是方便调试理解代码,veux主要源码再src文件夹中,核心文件便是store.js
State
我们可以在vuex中以下面两种方式定义一个state
const state = {
count: 0,
}
const state = () => ({
items: [],
})
获取到这个state,我们可以this.$store.state,而且我们知道veux中状态的存储是响应式的,并且state状态的改变只能通过提交mutation,我们拆解源码来分析下
export class Store {
constructor(options = {}) {
this.committing = false
this._modules = new ModuleCollection(options)
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
}
}
在Store类中,this.committing的作用是表示提交的状态,也就是说如果是通过提交mutations来改变状态的话,它的值为true,state改变完后它的值为false,this._modules是根据传入的options,注册各个模块,这里我们在Module那细讲,state变量也就是将根模块的state赋值给了state变量,installModule就是注册完善各个模块内的信息,resetStoreVM也就是生成一个Vue实例去管理state
installModule对state的处理
function installModule(store, rootState, path, module, hot) {
const isRoot = !path.length
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)
})
}
}
function _withCommit(fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
function getNestedState(state, path) {
return path.reduce((state, key) => state[key], state)
}
函数中先判断是否是根模块,如果不是根模块,通过getNestedState获取到当前模块的父模块中的state,并用Vue.set响应式地设置到父模块的state上
resetStoreVM
function resetStoreVM(store, state, hot) {
const oldVm = store._vm
store._vm = new Vue({
data: {
$$state: state
},
computed
})
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
resetStoreVM函数响应式地初始化store._vm,并且如果存在旧的实例就销毁,并且state是只读的,我们可以看Store中的get和set
get state() {
return this._vm.data.$$state
}
set state(v) {
if (__DEV__) {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
Mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,并且mutations必须是同步函数, 提交mutations的方式可以是以下几种
store.commit('increment')
store.commit('increment', 10)
store.commit('increment', {
amount: 10
})
store.commit({
type: 'increment',
amount: 10
})
this._mutations = Object.create(null)
const store = this
const { dispatch, commit } = this
this.commit = function boundCommit(type, payload, options) {
return commit.call(store, type, payload, options)
}
在Store类里,this._mutations的作用是存放注册的mutation,commit方法也用call绑定到store实例上
commit(_type, _payload, _options) {
const { 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)
})
})
}
}
function unifyObjectStyle(type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
if (__DEV__) {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
return { type, payload, options }
}
unifyObjectStyle函数用于处理两种不同风格的提交方式,去this._mutations找对应的方法,有对应的方法便forEach遍历执行
mutations注册函数
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)
})
}
在registerMutation参数中出现了local,这个local是什么呢?这个local就是设置当前模块的上下文context,后面再往下看,这个方法的调用是在installModule里
const local = module.context = makeLocalContext(store, namespace, path)
Actions
Actions类似于 mutation,不同的是Action 提交的是 mutation,而不是直接变更状态,可以包含任意异步操作并且内部是返回一个Promise用以处理异步流程
Actions是通过dispatch分发
store.dispatch('increment')
store.dispatch('incrementAsync', {
amount: 10
})
store.dispatch({
type: 'incrementAsync',
amount: 10
})
this._actions = Object.create(null)
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
this._actions存放注册的actions函数,跟commit方法一样,dispatch方法也进行了绑定
dispatch(_type, _payload) {
const { 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
}
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
resolve(res)
}, error => {
reject(error)
})
})
}
dispatch方法内部与commit方法不一样的就是使用了Promise.all去处理异步,一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。并且还返回一个Promise对象,这也就是我们可以在外部使用then进行链式操作和使用async await的原因了
actions注册函数
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)
}
return res
})
}
registerAction函数里,注册的actions方法接收两个参数,一个是context,包括(dispatch,commit,state,getters),一个是payload,对函数返回的结果判断是否是Promise,如果不是用Promise.resolve包装
Modules
Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
Module
export default class Module {
constructor(rawModule, runtime) {
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
addChild(key, module) {
this._children[key] = module
}
getChild(key) {
return this._children[key]
}
forEachChild(fn) {
forEachValue(this._children, fn)
}
forEachGetter(fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
forEachAction(fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
forEachMutation(fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
Module类里都比较好理解,这里没什么可说的,主要是添加子模块,获取子模块,以及遍历当前模块下的mutation,action,getter执行一些回调操作
ModuleCollection
export default class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule, false)
}
get(path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
getNamespace(path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
register(path, rawModule, runtime = true) {
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
ModuleCollection类是递归注册模块,如果rawModule还有子模块,则递归调用register注册子模块,get方法是获取父模块,父模块在调用addChild添加子模块,getNamespace为获取当前模块命名空间,Module和ModuleCollection看完后我们可以继续去看installModule函数了,在之前我们知道了installModule对state的处理,下面接着看installModule还做了什么
再回到installModule
function installModule(store, rootState, path, module, hot) {
const namespace = store._modules.getNamespace(path)
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
分析代码,先是取得当前模块的namespace,如果当前模块设置了namespaced,则在modulesNamespaceMap中存储一下当前模块,接着就是注册模块下所有的mutation,action,getter,子模块,这里需要注意的是action,可以在带命名空间的模块注册全局action
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
于是才有了源码中对type和handler的处理
makeLocalContext
function makeLocalContext(store, namespace, path) {
const noNamespace = namespace === ''
const local = {
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
type = namespace + type
if (__DEV__ && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
type = namespace + type
if (__DEV__ && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
makeLocalContext根据是否设置命名空间返回一个本地的dispatch,commit和getters,没有设置命名空间则返回全局的。if (!options || !options.root)判断是否传入第三个参数{root:true},如果为true,则也是调用全局上的方法,最后在用Object.defineProperties对local进行代理
Getters
getter类似于计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
this._wrappedGetters = Object.create(null)
this._makeLocalGettersCache = Object.create(null)
this._wrappedGetters用来存放注册的getters,this._makeLocalGettersCache是本地的getters缓存
getters注册函数
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.getters,
store.state,
store.getters
)
}
}
再回到resetStoreVM
看看resetStoreVM是怎么实现getters类似于计算属性的效果的
function resetStoreVM(store, state, hot) {
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true
})
});
}
在store上定义一个getters对象,遍历getters,将每一个getter注册到store.getters,访问对应getter时会去vm上访问对应的computed
最后
附上一张简单的图对store做个总结,这次对vuex源码的阅读在花了五天时间,完整地阅读下来后会发现其实并不难,耐心下来看代码,看官方的例子再结合调试基本上都会弄明白的,写文章也是一件费时间的事,还有一些像辅助函数,vuex的注册,插件等,等周末有空了在写上一篇,Ending......