前言
Vuex作为一款优秀的开源状态管理工具,官方的解释是“通过集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化”,所以我们只能通过提交mutation来修改状态的变化,同时为了分离同步与异步的操作,新增了dispatch来支持异步修改状态,也提供了状态的模块划分来支持状态的灵活配置,当然,还有其他的优秀概念,所以勾起我看其相关实现的好奇心。以及解决我看之前的疑问,本文使用的例子是Vuex库里提供的,examples/shpping-cart,建议大家先看看这个例子,并且把Vuex下载下来,方便大家理解跟一起复现😊。先抛出几个问题
vuex是怎么安装到vue的?- 为什么
mutation是同步的,而dispatch可以执行异步? - 使用
modules划分多个module后,子modules设置的mutations如何跟父module的区分使用? Vuex是怎么变成响应式的
源码解析
安装
先看看我们是怎么使用vuex的:
// index.js
import Vue from 'vue'
import App from './components/App.vue'
import store from './store'
import { currency } from './currency'
Vue.filter('currency', currency)
new Vue({
el: '#app',
store, // 将store添加到根组件上
render: h => h(App)
})
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart' // 子module
import products from './modules/products' // 子module
Vue.use(Vuex) // Vue.use(Vuex)安装Vuex插件
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({ // 将我们定义的状态信息通过Vuex.Store创建状态管理仓库
modules: {
cart,
products
}
})
可以看到,我们使用Vuex的步骤如下
- 使用
Vue.use(Vuex)安装Vuex插件 - 将我们定义的状态信息通过
Vuex.Store创建状态管理仓库 - 将store添加到根组件上
ok,既然使用
Vue.use,那么我们得提供install()方法供Vue执行安装逻辑,Vuex的入口文件是index.js,如下
// index.js
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'
export default {
Store,
install,
...
}
export {
Store,
install,
...
}
可以看到,我们抛出了install,这个方法是在store.js里定义
// store.js
export function install (_Vue) {
...
Vue = _Vue
applyMixin(Vue)
}
上面首先保存了我们传进来的Vue,这是一个在很多Vue插件里常用的操作,比方说Vue-router也是这样,这样可以直接在插件里使用Vue的功能,如状态响应更新,也不用额外在插件里安装Vue。接着就是执行applyMixin,看命名就是使用mixin做一些操作
// mixin.js
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
...
}
function vuexInit () {
const options = this.$options
// store injection
if (options.store) { // 这里是根组件,设置this.$store的指向,这样我们就可以通过this.$stor获取我们定义的state、getter等
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) { // 非根组件获取父组件的$store,最终都指向同一个$store
this.$store = options.parent.$store
}
}
}
可以看到,是通过Vue.mixin设置beforeCreate这个生命周期,这样每个组件执行这个生命周期时就会调用vuexInit进行vuex的初始化,vuexInit作用就是设置每一个组件的this.$store指向我们在Vue根组件传入的store。所以到这里,每一个组件都可以通过this.$store获取我事先配置的在store.js里的state、getters等。
Store
我们在store.js调用Vuex.Store创建状态管理仓库,这个就是Vuex的核心,我们来看看他的构造函数
// store.js
export class Store {
constructor (options = {}) {
...
this._committing = false // 是否处于是通过_withComming提交修改state的
this._actions = Object.create(null) // 保存action
this._actionSubscribers = [] // 保存subscribeAction,会在action执行的前后调用
this._mutations = Object.create(null) // 保存mutations
this._wrappedGetters = Object.create(null) // 保存getters
this._modules = new ModuleCollection(options) // 构造modules的集合
this._modulesNamespaceMap = Object.create(null) // 保存命名空间
this._subscribers = [] // 保存subscribeAction,会在mutations执行
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
this.strict = strict
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
...
}
...
ModuleCollection
上面代码通过this._modules = new ModuleCollection(options)来构造modules的集合
// module/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
...
register (path, rawModule, runtime = true) {
...
const newModule = new Module(rawModule, runtime) // 生成module
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule) // 添加当前modules到父moduels的_children数组里
}
// 如果当前modules下面还有modules则递归继续
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
可以看到,上面就是做了主要就是通过new Module来生成各个module,并将子module通过addChild添加到父module的_children中。再看看Module
// module/module.js
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null) // 保存子modules
this._rawModule = rawModule // 当前modules
const rawState = rawModule.state // 当前modules的.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
...
本质上就是做格式化,将配置的modules转化成统一格式,然后通过_chidren关联起来,这样,后续我们就可以通过这个获取我们的所有状态信息,比如example/shopping-cart例子里,经过上面转化后变成如下:
可以看到每个节点都有相同属性,嵌套的modules变成对应的_children.
回到Store的构造函数,接下来就是执行
// store.js
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
代码就是绑定dispatch跟commit,先看下commit
commit
// store.js
commit (_type, _payload, _options) {
const {
type, // 触发的mutations名
payload, // 传入的数据
options
} = unifyObjectStyle(_type, _payload, _options)
console.log(`type=${type}, payload=${payload}, options=${options}`)
const mutation = { type, payload }
const entry = this._mutations[type]
...
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload) // 执行mutations
})
})
this._subscribers
.slice()
.forEach(sub => {
console.log(`_subscribers after mutations`)
sub(mutation, this.state)
})
...
}
流程很简单,
- 通过
unifyObjectStyle获取触发的的mutations的名字type,跟传给mutation的数据payload,options是用于标识触发的mutations是全局modules里的mutations,还是当前modules里的mutations,是用于命名空间的,详细见这里 - 然后就是找到对应的
mutations然后执行,这里使用了this._withCommit包裹entry来执行mutations,这里先不管他的使用,后面会说 - 接着就是调用
this._subscribers来执行我们传给Vuex时使用store.subscribe定义的方法,store.subscribe是在mutations执行后才执行的,上面跑下example/shopping-cart例子看看那提交一个mutations各个位置输出什么,例子里这样设置subscribe
// logger.js
store.subscribe((mutation, state) => {
const nextState = deepCopy(state)
if (filter(mutation, prevState, nextState)) {
...
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))
endMessage(logger)
}
...
})
可以看到,先执行了
mutations,再执行subscribe,上面图里最后一行就log就是subscribe执行时打印的log
接下来看dispatch的代码
dispatch
// store.js
dispatch (_type, _payload) {
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
console.warn(`dispatch - type=${type}, payload=${payload}`)
console.log(this._actionSubscribers, 'asdf=asd=fa=sdf=sd=f')
const action = { type, payload }
const entry = this._actions[type]
...
try { // dispatch执行前
this._actionSubscribers
.slice()
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
...
}
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload))) // 执行dispatch
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
try { // dispatch执行后
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
...
}
resolve(res)
}, error => {
try {
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
...
}
reject(error)
})
})
}
可以看到,其实跟commit很类似,区别在于
_subscribers变成了_actionSubscribers,Vuex时使用store.subscribeAction定义的方法,且可以指定before跟after,如果没指定before或者aftre,则默认是before- 使用
promise来包裹执行函数,这也就是dispatch可以执行异步的原因。 同样的,我们打印一下触发dispatch的log,这里为了看到after,我稍微修改了一下例子,添加了一个after,执行结果如下
可以看到,先
before,再执行dispatch,由于dispatch本质就是用异步的方式提交mutaions,所以这里会触发dispacth里的mutations操作,最后再执行after。
接着再次回到Store,执行installModule进行模块的安装
installModule
// store.js
installModule(this, state, [], this._modules.root)
installModule主要分为三个部分:
- 第一部分:设置
module.state到父state上,这样就可以通过state.module.xxx获取子module的state,比如本文例子可以这样取state.cart.items,cart其实就是一个module - 第二部分:设置当前
module的局部commit & dispatch,如果没有命名空间,则指向父commit & dispatch - 第三部分:注册模块的
mustations、action、getters
// store.js
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length // 判断是否是根节点,因为只有根节点的为空
const namespace = store._modules.getNamespace(path) // 获取命名空间, 会拼接父+子
if (module.namespaced) { // 保存命名空间与当前module的映射
...
store._modulesNamespaceMap[namespace] = module
}
// 第一部分
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取父store的state
const moduleName = path[path.length - 1] // 获取mudule的名字
store._withCommit(() => { // 设置module到父state上
...
console.log(`parentState = ${JSON.stringify(parentState)}, moduleName = ${moduleName}, module.state = ${JSON.stringify(module.state)}`)
Vue.set(parentState, moduleName, module.state) // 设置module.state到父state上,这样就可以通过state.module.xxx获取子module的state, 疑问,这是使用Vue.set的目的是啥?直接这样不行?parentState[moduleName] = module.state
})
}
// 第二部分,设置当前module的局部commit & dispatch,如果没有命名空间,则指向父commit & dispatch
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
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
第一部分
这一块首先获取获取父store的state,接着获取要设置到父state上的moudle,这样就可以就可以设置module.state到父state上,也就可以通过state.module.xxx获取子module的state,,比如本文例子可以这样取state.cart.items,我也截取了log日志
第一个log就是未设置的时候,第二个log就是设置第一个后的结果,可以看到,父state上已有cart这个moudle的state,这里我目前有个疑问🤔️,就是这行代码:
Vue.set(parentState, moduleName, module.state)
这里应该只是使用这个就行,有大佬知道的解答一下
parentState[moduleName] = module.state
第二部分
第二部分就是调用makeLocalContext设置moudle.context,有什么用呢?为的是在局部的模块内调默认可以用模块自己定义的action和mutation,是否局部取决于我们是否有设置命名空间。比如我们看example/shopping-cart里面cart这个module的定义(这个有设置命名空间namespaced: true,没截进来):
addProductToCart是一个action,可以看到,如果未加{root: true},那么pushProductToCart触发的mutations是cart这个module自己的,如果加了,就会像红框一样,会去找父module的mutations,接下来看看makeLocalContext的定义
// store.js
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 // 拼接
...
}
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
...
}
store.commit(type, payload, options)
}
}
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
首先看下是否有设置命名空间,没有则无需特殊处理,直接返回对应的dispatch/commit,否则需要重新拼接type值,因为我们如果在子module调用commit的话,如果没指定{root: true},那么其实是要调用子module自己的mutations,而不是父module的,所以得拼接拼接moduleName + type来找到需要触发的mutations。我们可以看下这里的namespace等于啥:
// module/module-collection.js
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
可以看到,就是如果设置了module.namespaced = true那么就拼接当前模块的名字,即mouduleName,也就是里面的key,之所以使用path可以获取mouduleName,是因为path本质上就是父子module的mouduleName拼接而来的。
第三部分
第三部分用于注册模块的mustations、action、getters,这些都大同小异本质上只是做了挂载跟入参设置,比如把我们以其中的mutations为例,看看registerMutation(store, namespacedType, mutation, local)做了啥
// store.js
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) // 设置入参
})
}
可以看到本质上就是把mutations挂在store._mutations,这样我们触发commit的时候,就可以通过store._mutations找到对应的mutations,然后设置入参数,这也就是我们可以在定义mutations函数时可以获取state,跟payload的原因,因为我们是这样定义mutations的(来源Vuex官网)
最后就是递归子module了,没啥可说🤷♂️。
// 递归子module
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
ok,到这里installModules就结束了,再看看他做了啥?
- 第一部分:设置
module.state到父state上,这样就可以通过state.module.xxx获取子module的state,同时变成响应式,比如本文例子可以这样取state.cart.items - 第二部分:设置当前module的局部
commit & dispatch,如果没有命名空间,则指向父commit & dispatch - 第三部分:注册模块的
mustations、action、getters通过上面的设置,我们得到了一颗完整的store树。如下:
这样我们就可以通过这个store树找到任何我们想要的state、mutations、action...
接下来再次回到Store的定义,只剩下最后一步了resetStoreVM(this, state)💥
resetStoreVM
这里做的就是初始化
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters // 获取installModules里挂在store._wrappedGetters里的getters
const computed = {}
// 循环getters,使用computed来指向我们的getters,并支持通过this.$store.getters.xxx能够访问到该getters
forEachValue(wrappedGetters, (fn, key) => {
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({ // 这里类似bus总线
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
if (store.strict) {
enableStrictMode(store)
}
...
}
先想一下,我们是怎么使用getters的?,是通过this.$store.getters.xxx获取的,所以需要设置this.$store.getters.xxx指向对应的store._vm[xxx],这里的_vm就是下面的new Vue(),这里类似另一种通信方式,$bus总线,使用一个总线来保存所有的state、computed,这样订阅这些的属性想也就具有响应式了,但在编码中,我们是通过this.$sotre.state获取state的,所以还得做一层代理,如下:
// store.js
get state () {
return this._vm._data.$$state
}
上图可以看到,_data下面就是保存了我们的$$state,也就是state,而computed通过
Object.defineProperty指向了._vm[key],也就是我们一个红框的部分。
_withCommit
ok,源码基本ok了,现在看下这个,源码里面多次出现_withCommit,他的作用是mutaitions的代理,所有涉及修改state的都得经过他才可以设置,看下他的代码
// store.js
_withCommit (fn) { // fn是mutations
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
可以看到,他在传入的fn前后设置committing,而这个committing是用来表示用户是通过mutations修改state,还是直接修改的,Vuex会watch state的变动,如果state被修改时this._committing为false,那么就会抛出警告,如下:
// store.js
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (__DEV__) {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
我们通过_vm.$watch监听了 this._data.$$state的改动,如果改动时_committing = false,则代表我们是通过类似this.$store.state.xxx = xxx修改state的,而通过mutations的修改,由于使用了_withCommit包装了一层,那么就会在修改前把_committing改为true,就通过检测了,等mutations修改完state再重置为false。
结尾
至此,Vuex的核心流程就走完了,剩余如辅助函数、插件我觉得自行看源码即可。
现在回答开篇的三个问题:
-
vuex是怎么安装到vue的?通过
install -
为什么
mutation是同步的,而dispatch可以执行异步?dispatch使用promise包装了一层
-
使用
modules划分多个module后,子modules设置的mutations如何跟父module的区分使用?通过命名空间,设置了命名空间后,源码会根据是否设置了
{root: true}来动态拼接实际的key,比如虽然example/shppping-cart的cart子module写了commit('pushProductToCart', { id: product.id }),但实际上触发的是cart这个module的pushProductToCart,而不是父module的pushProductToCart -
Vuex是怎么变成响应式的?将所有的
state,getters通过new Vue设置在的data跟computed里,然后将state,getters代理到new Vue的data跟computed里,这样读取后就自动响应式了.