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
的使用和原理就介绍到这里。最后截取一张官网的图为大家进行一下总结。