Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。下面我们针对以下几点,对源码进行分析:
- Vuex这个包对外暴露哪些东西,其用处是什么?
- Vuex怎么初始化及挂载,以及和vue组件关联起来?
- Vuex怎么实现不同module下,state、getters、mutations、actions等属性的获取?
- Vuex怎么做到限制state的更新要经过commit相应的mutations,要不然报错?
- Vuex和redux、mobx等状态管理库一样,本身都是同步更新的吗?
一. Vuex暴露的方法和属性及其用处
// src/index.js
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
可以看到对外暴露Store对象、install挂载方法、version版本、以及方便在组件中获取state、mutation、getter和action的方法和初始化时用于创建命名空间别名的createNamespacedHelpers;
二. Vuex的挂载及如何与组件关联
Vuex作为Vue的插件,通过提供install方法来挂载到Vue实例上。
// src/mixin.js 导出作为install方法
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
// 根组件挂载$store
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 从父组件获取$store
this.$store = options.parent.$store
}
}
}
可以看出来,是通过调用了Vue.mixin在全局里混入了beforeCreate生命周期,那么Vue实例在初始化触发beforeCreate生命周期时,就会执行vuexInit方法。然后将我们在初始化配置Vue实例时,传入的store实例挂载到每个组件的$store上,也就是说,我们可以在任何组件里,都能通过this.$store取到相应的state状态值。
const store = new Vuex.Store({
state: {},
mutations: {}
});
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store
})
三. Vuex初始化
export default Store {
constructor (options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
// ....
const {
plugins = [],
strict = false
} = options
// store internal state
this._committing = false
this._actions = Object.create(null) // 用于存储配置的actions
this._actionSubscribers = []
this._mutations = Object.create(null) // 用于存储配置的mutations
this._wrappedGetters = Object.create(null) // 用于存储配置的getters
this._modules = new ModuleCollection(options) // 实例化后会将我们在store初始化配置的mudule构建成属性,eg:{root: {_children: {a: module, b: module}, module}}
this._modulesNamespaceMap = Object.create(null) // 用去存储每个命令空间的模块。eg: {'a/': module, 'b/': module}
this._subscribers = []
this._watcherVM = new Vue()
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
// 重新绑定dipatch和commit方法,强行将其执行上下文this指向store,避免用户使用时,改写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)
}
// strict mode
this.strict = strict
const state = this._modules.root.state
// 根据命令空间与否注册所有模块的state,mutation,getter和action(重点)
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// 将state和getters变成响应式(重点)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
if (Vue.config.devtools) {
devtoolPlugin(this)
}
}
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// 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)
})
}
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)
})
}
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
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 (process.env.NODE_ENV !== 'production' && !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 (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// 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
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
}
初始化时通过调用installModule方法,注册state、mutation、getter、action。在注册的过程中,会拼接上相应的命名空间。而makeLocalContext 方法将会构造不同的访问方式,当没有命名空间时,用默认的commit和action;有命名空间时,使用改造过的commit和action,其里面会拼接上命名空间的值去查找相应的mutation和action(getter和state也类似),因为我们在开发中,可以直接在组件中store.commit('increment')或模块中commit('increment'),内部会拼接上命令空间找到相应的方法,无需我们处理。
最后调用resetStoreVM将我们的state和getter设置到vue实例上,变成响应式。因此当我们在组件中访问state时,其实访问的是$$state数据,这样就会触发Vue的依赖收集,改变后也就会触发组件更新。
四. vuex更新数据的方式
Vuex异步请求放在Action处理,Action 提交的是 mutation,而不是直接变更状态,更新数据直接通过commit来操作(必须是同步函数),目的是为了devtools 能捕捉到前一状态和后一状态的快照,然后进行回溯。enableStrictMode会对state进行深度监听,如果_committing值为false,则报错提示,其中_committing可以通过_withCommit函数设置(只能提示,不能拦截掉,也就是还可以强制更新)
function enableStrictMode (store) {
// 严格模式且开发环境下,深度监听state值的变化,如果_committing为false,则报错
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
五. vuex数据更新是异步还是同步?
从之前的源码中可以看到,Vuex本身其实是同步更新的,但是在Vue中表现出异步是因为将state的数据挂载做为Vue的响应式data数据处理,满足Vue的数据更新机制(异步更新)。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。下面我们针对以下几点,对源码进行分析:
一. Vuex暴露的方法和属性及其用处
可以看到对外暴露Store对象、install挂载方法、version版本、以及方便在组件中获取state、mutation、getter和action的方法和初始化时用于创建命名空间别名的createNamespacedHelpers;
二. Vuex的挂载及如何与组件关联
Vuex作为Vue的插件,通过提供install方法来挂载到Vue实例上。
可以看出来,是通过调用了Vue.mixin在全局里混入了beforeCreate生命周期,那么Vue实例在初始化触发beforeCreate生命周期时,就会执行vuexInit方法。然后将我们在初始化配置Vue实例时,传入的store实例挂载到每个组件的$store上,也就是说,我们可以在任何组件里,都能通过this.$store取到相应的state状态值。
三. Vuex初始化
初始化时通过调用installModule方法,注册state、mutation、getter、action。在注册的过程中,会拼接上相应的命名空间。而makeLocalContext 方法将会构造不同的访问方式,当没有命名空间时,用默认的commit和action;有命名空间时,使用改造过的commit和action,其里面会拼接上命名空间的值去查找相应的mutation和action(getter和state也类似),因为我们在开发中,可以直接在组件中store.commit('increment')或模块中commit('increment'),内部会拼接上命令空间找到相应的方法,无需我们处理。
最后调用resetStoreVM将我们的state和getter设置到vue实例上,变成响应式。因此当我们在组件中访问state时,其实访问的是$$state数据,这样就会触发Vue的依赖收集,改变后也就会触发组件更新。
四. vuex更新数据的方式
Vuex异步请求放在Action处理,Action 提交的是 mutation,而不是直接变更状态,更新数据直接通过commit来操作(必须是同步函数),目的是为了devtools 能捕捉到前一状态和后一状态的快照,然后进行回溯。enableStrictMode会对state进行深度监听,如果_committing值为false,则报错提示,其中_committing可以通过_withCommit函数设置(只能提示,不能拦截掉,也就是还可以强制更新)
五. vuex数据更新是异步还是同步?
从之前的源码中可以看到,Vuex本身其实是同步更新的,但是在Vue中表现出异步是因为将state的数据挂载做为Vue的响应式data数据处理,满足Vue的数据更新机制(异步更新)。