1.前言
状态管理在开发中已经算是老生常谈了,本篇文章我们转向前端方向的Vue框架,看看Vuex是怎么通过store处理数据状态的管理的。由于Vue框架本身就是响应式处理数据的,所以store更多的为了跨路由去管理数据的状态。在使用store之前,我们先来说明store的两个特性:
-
Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。 - 你不能直接改变
store中的状态。改变store中的状态的唯一途径就是显式地提交(commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
store是由五大成员组成的,开发者可根据业务复杂程度,合理的使用它们。我们先来了解一下store的五大成员:
2.五大成员
-
State:Vuex的store中的state是响应式的,当state中的数据发生变化时,所有依赖于该数据的组件都会自动更新。 -
Getters:Vuex的store中的getters可以理解为store的计算属性,它们会基于store中的state派生出一些新的状态,供组件使用。 -
Mutations:Vuex的store中的mutations是同步的事件操作,它们用于更改store中的state。在Vuex中,mutations是唯一可以修改store中的state的方式。 -
Actions:Vuex的store中的actions是异步的事件操作,它们用于处理异步逻辑,如API请求等。Actions可以通过提交mutations来修改store中的state。 -
Modules:Vuex的store可以通过模块化的方式组织代码,将一个大的store拆分成多个小的模块,每个模块都有自己的state、getters、mutations和actions。
其中State和Mutations是构成store必不可少的成员。那么store是怎么使用的呢?我们来看下一章节。
3.Store的使用
3.1.安装:
npm install vuex
3.2.最简单的用法:
我们创建一个新的js文件,引入vuex并通过Vue.use()使用它,然创建一个Store对象并export:
import Vuex from 'vuex'
import Vue from "vue";
Vue.use(Vuex)
const store = new Vuex.Store({
state: { //在state对象建立需要数据
count: 0
},
mutations: {
add: function (state) {
state.count++;
}
},
});
export default store
其中count是我们在state里声明的需要被监听的变量。add是个方法,其实现是count++。接下来我们在main.js里注入store实例:
import store from "./store"
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
好了我们的store已经可以用了。我们假设有两个路由:home page和new page,我们看看store是怎么监听数据变化并跨路由共享的。
home page代码如下:
<template>
<div>
<div>I am home page</div>
<div>{{ getCount() }}</div>
<div @click="addCount" class="button">add count</div>
<div @click="gotoNewPage" class="button">Go to newpage</div>
</div>
</template>
<script>
export default {
methods: {
gotoNewPage() {
this.$router.push({ path: "/newpage" });
},
getCount() {
return this.$store.state.count;
},
addCount() {
this.$store.commit('add');
},
},
};
</script>
<style lang="scss" scoped>
.button {
width: 100px;
height: 50px;
background: #527dab;
margin-top: 15px;
}
</style>
其中getCount()方法从store的state里读取count的值。addCount()方法用来调用mutations的add()方法,实现count++。们点击“add count”按钮看下效果:
我们可以看到
count的状态变化会被监听到。那么我们跳转至new page,代码如下:
<template>
<div>
<div>new page</div>
<div>{{ getCount() }}</div>
<div @click="back" class="button">back</div>
</div>
</template>
<script>
export default {
methods: {
back() {
this.$router.push({
path: "/home",
});
},
getCount() {
return this.$store.state.count;
},
},
};
</script>
<style lang="scss" scoped>
.button {
width: 100px;
height: 50px;
background: #527dab;
margin-top: 15px;
}
</style>
跨路由读取到了count的值。
以上就是
Store最简单的用法,接下来我们进阶一下。
3.3.getters和actions
为store增加getters和actions:
const store = new Vuex.Store({
state: { //在state对象建立需要数据
count: 0
},
getters: {
getCount: function (state) {
return "count是: " + state.count
}
},
mutations: {
add: function (state) {
state.count++;
}
},
actions: {
addAction: function (context) {
setTimeout(() => {
context.commit('add')
}, 500)
}
}
});
如果上一节所讲,getters类似于计算属性,有时候我们需要从store中的 state中派生出一些状态,那么就需要getters了。getters的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。actions的核心在于可以包含任意异步操作。它提交的是mutation,不能直接修改state的值。addAction模拟了一个异步操作,500ms之后执行add方法。我们接下来看一下使用,改造一下home page的getCount()和addCount()方法:
getCount() {
return this.$store.getters.getCount;
},
addCount() {
this.$store.dispatch("addAction");
},
actions需要使用store.dispatch()进行分发,我们看一下执行结果:
点击
add count按钮后500ms后,count加1,通过getters得到新的显示文本。
3.4.Modules
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。
为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块。接着举个例子:
const userStore = {
state: { //在state对象建立需要数据
name: "tom"
},
getters: {
getName: function (state) {
return "name是: " + state.name
}
},
mutations: {
setName: function (state, name) {
state.name = name;
}
},
actions: {
setNameAction: function (context,name) {
console.log("setNameAction")
setTimeout(() => {
console.log("setName")
context.commit('setName', name)
}, 500)
}
}
};
export default userStore
刚才的实例代码中,我们增加一个userStore,为其配置了state,getters,mutations和actions。然后我们为之前的store增加modules,代码如下:
import Vuex from 'vuex'
import Vue from "vue";
import userModule from "@/store/modules/user"
Vue.use(Vuex)
const store = new Vuex.Store({
state: { //在state对象建立需要数据
count: 0
},
getters: {
getCount: function (state) {
return "count是: " + state.count
}
},
mutations: {
add: function (state) {
state.count++;
}
},
actions: {
addAction: function (context) {
setTimeout(() => {
context.commit('add')
}, 500)
}
},
modules:{
user: userModule
}
});
export default store
导入了userModule,我们看看这个user的module是怎么用的。
<template>
<div>
<div>I am home page</div>
<div>{{ getName() }}</div>
<div @click="setName" class="button">set Name</div>
</div>
</template>
<script>
export default {
methods: {
getName(){
return this.$store.getters.getName;
},
setName() {
this.$store.dispatch("setNameAction","bigcatduan");
},
},
};
</script>
<style lang="scss" scoped>
.button {
width: 100px;
height: 50px;
background: #527dab;
margin-top: 15px;
}
</style>
如果是从getters里取数据用法同之前的实例一样。如果是从state里取数据则需要指定module:
getName(){
return this.$store.state.user.name;
},
需要注意的是,如果module里的getter的方法名与父节点store的冲突了,则运行的时候会取父节点store的值,但会报duplicate getter key的错误。如果module里的getter的方法名与同节点的其它modules冲突了,则运行的时候会根据在节点注册的顺序来取值,同时报duplicate getter key的错误。比如:
modules:{
user: userModule,
product: productModule
}
由于userModule先被注册到modules里,所以取值时会取userModule的值。
如果父子之间或同节点的action和mutation的方法名相同的话,则各个节点同方法名的方法都会被执行。
好了到此为止我们发现一个问题,如果项目庞大,免不了会在不同的module里定义相同的变量或方法名称,让开发者自己去维护庞大的方法/变量列表显然不现实,于是vuex引入了命名空间namespace。
3.5.命名空间
默认情况下,模块内部的action和mutation仍然是注册在全局命名空间的——这样使得多个模块能够对同一个action或mutation作出响应。Getter同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的getter从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true的方式使其成为带命名空间的模块。当模块被注册后,它的所有getter、action及mutation都会自动根据模块注册的路径调整命名。
比如我们为刚才的userModule设置namespaced: true。
const userStore = {
namespaced: true,
state: { //在state对象建立需要数据
name: "tom"
},
getters: {
getName: function (state) {
return "name是: " + state.name
}
},
mutations: {
setName: function (state, name) {
state.name = name;
}
},
actions: {
setNameAction: function (context,name) {
console.log("setNameAction")
setTimeout(() => {
console.log("setName")
context.commit('setName', name)
}, 500)
}
}
};
export default userStore
set和get的相应方式变化如下:
getName() {
return this.$store.getters["user/getName"];
},
setName() {
this.$store.dispatch("user/setNameAction", "bigcatduan");
},
在getter,dispatch或commit时需要显示的指明局部的命名空间。
接下来我们思考一下,是否可以在不同的module下读取到其它module的内容呢?答案是可以的。比如我们的store结构如下:
根节点:
import Vuex from 'vuex'
import Vue from "vue";
import userModule from "@/store/modules/user"
import productModule from "@/store/modules/product"
Vue.use(Vuex)
const store = new Vuex.Store({
state: { //在state对象建立需要数据
count: 20
},
getters: {
getCount: function (state) {
return "count是: " + state.count
}
},
mutations: {
add: function (state) {
state.count++;
}
},
actions: {
addAction: function (context) {
setTimeout(() => {
context.commit('add')
}, 500)
}
},
modules:{
user: userModule,
product: productModule
}
});
export default store
里面包含了user和product这两个module。
代码如下:
//user
const userStore = {
namespaced: true,
state: { //在state对象建立需要数据
name: "tom"
},
getters: {
getName: function (state, getters, rootState, rootGetters) {
return "name是: " + state.name
}
},
mutations: {
setName: function (state, name) {
state.name = name;
}
},
actions: {
setNameAction: function (context,name) {
setTimeout(() => {
context.commit('setName', name)
}, 500)
}
}
};
export default userStore
//product
const productStore = {
namespaced: true,
state: { //在state对象建立需要数据
price: 15
},
getters: {
getPrice: function (state) {
return "price是: " + state.price
}
},
mutations: {
setPrice: function (state, price) {
state.price = price;
}
},
actions: {
setPriceAction: function (context,price) {
setTimeout(() => {
context.commit('setPrice', price)
}, 500)
}
}
};
export default productStore
我们改造一下user的getters:
//user
getters: {
getName: function (state, getters, rootState, rootGetters) {
console.log("rootState count: ",rootState.count)
console.log("product state price: ",rootState.product.price)
console.log("root getter getCount: ",rootGetters.getCount)
console.log("product getter getPrice: ",rootGetters["product/getPrice"])
return "name是: " + state.name
}
},
除了state,vuex还为我们提供了getters,rootState和rootGetters这几个参数。于是我们可以读取根节点state相应的变量,其他节点state相应的变量,根getters里的方法,还可以找到其他getters里的方法。上面代码的打印结果如下:

我们再来改造一下
actions:
actions: {
setNameActionRoot: {
root:true,
handler (namespacedContext, name){
console.log("setNameAction")
setTimeout(() => {
console.log("setName")
namespacedContext.commit('setName', name)
}, 500)
}
},
}
root:true意味着我们为这个带命名空间的模块注册全局action,所以我们依然可以不指定命名空间执行dispatch方法:
this.$store.dispatch("setNameActionRoot", "bigcatduan");
继续改造一下actions:
actions: {
setNameAction: function (context,name) {
setTimeout(() => {
context.commit('setName', name)
}, 500)
},
setSomeActions ( { dispatch, commit, getters, rootGetters },someAction){
console.log("root getter getCount: ",rootGetters.getCount)
console.log("product getter getPrice: ",rootGetters["product/getPrice"])
dispatch('setNameAction',someAction.name)
commit('setName',someAction.name)
commit('product/setPrice', someAction.price, {root:true})
}
}
可以拿到全局rootGetters。若需要在全局命名空间内分发action 或提交 mutation,将{ root: true } 作为第三参数传给dispatch或commit即可。
好了以上就是store的用法,更多用法大家可以继续探索。接下来我们来讲一下store的实现原理。
4.实现原理
我们先来看看构造方法:
//Store
constructor (options = {}) {
if (__DEV__) {
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `store must be called with the new operator.`)
}
const {
plugins = [],
strict = false,
devtools
} = options
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._makeLocalGettersCache = Object.create(null)
// EffectScope instance. when registering new getters, we wrap them inside
// EffectScope so that getters (computed) would not be destroyed on
// component unmount.
this._scope = null
this._devtools = devtools
// 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)
}
// strict mode
this.strict = strict
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store state, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreState(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
}
创建了各个成员变量,其中最重要的是_modules的创建。之后依次执行installModule(),resetStoreState()和plugins.forEach(plugin => plugin(this))。我们先来看看_modules的创建。
4.1. ModuleCollection
_modules对所有的module进行了初始化并构造了其依赖关系。其构造方法实现如下:
//ModuleCollection
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
调用了register方法:
register (path, rawModule, runtime = true) {
if (__DEV__) {
assertRawModule(path, rawModule)
}
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)
}
// register nested modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
在初始化的时候,由于传入的是根module,path.length === 0,所以会创建一个Module对象,并为root赋值,把它设置为根module。Module的构造方法如下:
//Module
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
回到ModuleCollection,如果存在子modules,就会遍历modules,递归执行register()方法。而再次执行register()方法时,path.length === 0为false,会找到它的parent,并执行addChild()方法:
//Module
addChild (key, module) {
this._children[key] = module
}
由此实现了modules依赖关系的创建,生成了一个modules树。接下来我们看installModule()方法的实现:
4.2.installModule()
它的作用是对根module进行初始化,根据上一章节生成的modules树,递归注册每一个module。代码如下:
export function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
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(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
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)
})
}
如果设置了nameSpace,则向_modulesNamespaceMap注册。之后如果不是根module,将模块的state添加到state链中,就可以按照state.moduleName进行访问。
接下来创建makeLocalContext上下文,为该module设置局部的dispatch、commit方法以及getters和state,为的是在局部的模块内调用模块定义的action和mutation,这个过程具体就不展开了。
接下来分别遍历_mutations,_actions和_wrappedGetters进行注册。注册过程大同小异,我们取_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)
})
}
把对应的值函数封装后存储在数组里面,然后作为store._mutations的属性。store._mutations收集了我们传入的所有mutation函数。
installModule()的分析就完成了,我们继续看resetStoreState()的实现。
4.4.resetStoreState()
这个方法的作用是初始化state,并且注册getters作为一个computed属性。
export function resetStoreState (store, state, hot) {
const oldState = store._state
const oldScope = store._scope
// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computedObj = {}
const computedCache = {}
// create a new effect scope and create computed object inside it to avoid
// getters (computed) getting destroyed on component unmount.
const scope = effectScope(true)
scope.run(() => {
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldState.
// using partial to return function with only arguments preserved in closure environment.
computedObj[key] = partial(fn, store)
computedCache[key] = computed(() => computedObj[key]())
Object.defineProperty(store.getters, key, {
get: () => computedCache[key].value,
enumerable: true // for local getters
})
})
})
store._state = reactive({
data: state
})
//...
}
遍历store._wrappedGetters,并新建computed对象进行存储,通过Object.defineProperty方法为getters对象建立属性并实现响应式,使得我们通过this.$store.getters.xxxgetter能够访问到该getters。最后利用reactive为state实现响应式。
4.5.install
在vue里,通过Vue.use()挂载全局变量,所以业务组件中通过this就能访问到store:
install (app, injectKey) {
app.provide(injectKey || storeKey, this)
app.config.globalProperties.$store = this
const useDevtools = this._devtools !== undefined
? this._devtools
: __DEV__ || __VUE_PROD_DEVTOOLS__
if (useDevtools) {
addDevtools(app, this)
}
}
4.6.commit和dispatch
最后我们再来看看commit和dispatch都做了什么
//commit
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]
//...
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))
//...
}
主要做了两件事:1.遍历_mutations,执行handler。2.遍历_subscribers,通知订阅者。我们在看dispatch
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
//...
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)
})
})
}
dispatch使用Promise封装了异步操作,遍历_actions执行handler操作,并遍历_actionSubscribers通知订阅者。
5.总结
Vuex的状态管理方式store的使用和原理就介绍到这里。最后截取一张官网的图为大家进行一下总结。
