1.前言
狀態(tài)管理在開發(fā)中已經(jīng)算是老生常談了,本篇文章我們轉(zhuǎn)向前端方向的Vue框架,看看Vuex是怎么通過store處理數(shù)據(jù)狀態(tài)的管理的。由于Vue框架本身就是響應(yīng)式處理數(shù)據(jù)的,所以store更多的為了跨路由去管理數(shù)據(jù)的狀態(tài)。在使用store之前,我們先來說明store的兩個特性:
-
Vuex的狀態(tài)存儲是響應(yīng)式的。當(dāng)Vue組件從store中讀取狀態(tài)的時候,若store中的狀態(tài)發(fā)生變化,那么相應(yīng)的組件也會相應(yīng)地得到高效更新。 - 你不能直接改變
store中的狀態(tài)。改變store中的狀態(tài)的唯一途徑就是顯式地提交(commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態(tài)的變化,從而讓我們能夠?qū)崿F(xiàn)一些工具幫助我們更好地了解我們的應(yīng)用。
store是由五大成員組成的,開發(fā)者可根據(jù)業(yè)務(wù)復(fù)雜程度,合理的使用它們。我們先來了解一下store的五大成員:
2.五大成員
-
State:Vuex的store中的state是響應(yīng)式的,當(dāng)state中的數(shù)據(jù)發(fā)生變化時,所有依賴于該數(shù)據(jù)的組件都會自動更新。 -
Getters:Vuex的store中的getters可以理解為store的計(jì)算屬性,它們會基于store中的state派生出一些新的狀態(tài),供組件使用。 -
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是構(gòu)成store必不可少的成員。那么store是怎么使用的呢?我們來看下一章節(jié)。
3.Store的使用
3.1.安裝:
npm install vuex
3.2.最簡單的用法:
我們創(chuàng)建一個新的js文件,引入vuex并通過Vue.use()使用它,然創(chuàng)建一個Store對象并export:
import Vuex from 'vuex'
import Vue from "vue";
Vue.use(Vuex)
const store = new Vuex.Store({
state: { //在state對象建立需要數(shù)據(jù)
count: 0
},
mutations: {
add: function (state) {
state.count++;
}
},
});
export default store
其中count是我們在state里聲明的需要被監(jiān)聽的變量。add是個方法,其實(shí)現(xiàn)是count++。接下來我們在main.js里注入store實(shí)例:
import store from "./store"
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
好了我們的store已經(jīng)可以用了。我們假設(shè)有兩個路由:home page和new page,我們看看store是怎么監(jiān)聽數(shù)據(jù)變化并跨路由共享的。
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()方法用來調(diào)用mutations的add()方法,實(shí)現(xiàn)count++。們點(diǎn)擊“add count”按鈕看下效果:
我們可以看到
count的狀態(tài)變化會被監(jiān)聽到。那么我們跳轉(zhuǎn)至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最簡單的用法,接下來我們進(jìn)階一下。
3.3.getters和actions
為store增加getters和actions:
const store = new Vuex.Store({
state: { //在state對象建立需要數(shù)據(jù)
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)
}
}
});
如果上一節(jié)所講,getters類似于計(jì)算屬性,有時候我們需要從store中的 state中派生出一些狀態(tài),那么就需要getters了。getters的返回值會根據(jù)它的依賴被緩存起來,且只有當(dāng)它的依賴值發(fā)生了改變才會被重新計(jì)算。actions的核心在于可以包含任意異步操作。它提交的是mutation,不能直接修改state的值。addAction模擬了一個異步操作,500ms之后執(zhí)行add方法。我們接下來看一下使用,改造一下home page的getCount()和addCount()方法:
getCount() {
return this.$store.getters.getCount;
},
addCount() {
this.$store.dispatch("addAction");
},
actions需要使用store.dispatch()進(jìn)行分發(fā),我們看一下執(zhí)行結(jié)果:
點(diǎn)擊
add count按鈕后500ms后,count加1,通過getters得到新的顯示文本。
3.4.Modules
由于使用單一狀態(tài)樹,應(yīng)用的所有狀態(tài)會集中到一個比較大的對象。當(dāng)應(yīng)用變得非常復(fù)雜時,store對象就有可能變得相當(dāng)臃腫。
為了解決以上問題,Vuex允許我們將store分割成模塊(module)。每個模塊擁有自己的state、mutation、action、getter、甚至是嵌套子模塊。接著舉個例子:
const userStore = {
state: { //在state對象建立需要數(shù)據(jù)
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
剛才的實(shí)例代碼中,我們增加一個userStore,為其配置了state,getters,mutations和actions。然后我們?yōu)橹暗?code>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對象建立需要數(shù)據(jù)
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
導(dǎo)入了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里取數(shù)據(jù)用法同之前的實(shí)例一樣。如果是從state里取數(shù)據(jù)則需要指定module:
getName(){
return this.$store.state.user.name;
},
需要注意的是,如果module里的getter的方法名與父節(jié)點(diǎn)store的沖突了,則運(yùn)行的時候會取父節(jié)點(diǎn)store的值,但會報duplicate getter key的錯誤。如果module里的getter的方法名與同節(jié)點(diǎn)的其它modules沖突了,則運(yùn)行的時候會根據(jù)在節(jié)點(diǎn)注冊的順序來取值,同時報duplicate getter key的錯誤。比如:
modules:{
user: userModule,
product: productModule
}
由于userModule先被注冊到modules里,所以取值時會取userModule的值。
如果父子之間或同節(jié)點(diǎn)的action和mutation的方法名相同的話,則各個節(jié)點(diǎn)同方法名的方法都會被執(zhí)行。
好了到此為止我們發(fā)現(xiàn)一個問題,如果項(xiàng)目龐大,免不了會在不同的module里定義相同的變量或方法名稱,讓開發(fā)者自己去維護(hù)龐大的方法/變量列表顯然不現(xiàn)實(shí),于是vuex引入了命名空間namespace。
3.5.命名空間
默認(rèn)情況下,模塊內(nèi)部的action和mutation仍然是注冊在全局命名空間的——這樣使得多個模塊能夠?qū)ν粋€action或mutation作出響應(yīng)。Getter同樣也默認(rèn)注冊在全局命名空間,但是目前這并非出于功能上的目的(僅僅是維持現(xiàn)狀來避免非兼容性變更)。必須注意,不要在不同的、無命名空間的模塊中定義兩個相同的getter從而導(dǎo)致錯誤。
如果希望你的模塊具有更高的封裝度和復(fù)用性,你可以通過添加namespaced: true的方式使其成為帶命名空間的模塊。當(dāng)模塊被注冊后,它的所有getter、action及mutation都會自動根據(jù)模塊注冊的路徑調(diào)整命名。
比如我們?yōu)閯偛诺?code>userModule設(shè)置namespaced: true。
const userStore = {
namespaced: true,
state: { //在state對象建立需要數(shù)據(jù)
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的相應(yīng)方式變化如下:
getName() {
return this.$store.getters["user/getName"];
},
setName() {
this.$store.dispatch("user/setNameAction", "bigcatduan");
},
在getter,dispatch或commit時需要顯示的指明局部的命名空間。
接下來我們思考一下,是否可以在不同的module下讀取到其它module的內(nèi)容呢?答案是可以的。比如我們的store結(jié)構(gòu)如下:
根節(jié)點(diǎn):
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對象建立需要數(shù)據(jù)
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對象建立需要數(shù)據(jù)
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對象建立需要數(shù)據(jù)
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這幾個參數(shù)。于是我們可以讀取根節(jié)點(diǎn)state相應(yīng)的變量,其他節(jié)點(diǎn)state相應(yīng)的變量,根getters里的方法,還可以找到其他getters里的方法。上面代碼的打印結(jié)果如下:

我們再來改造一下
actions:
actions: {
setNameActionRoot: {
root:true,
handler (namespacedContext, name){
console.log("setNameAction")
setTimeout(() => {
console.log("setName")
namespacedContext.commit('setName', name)
}, 500)
}
},
}
root:true意味著我們?yōu)檫@個帶命名空間的模塊注冊全局action,所以我們依然可以不指定命名空間執(zhí)行dispatch方法:
this.$store.dispatch("setNameActionRoot", "bigcatduan");
繼續(xù)改造一下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。若需要在全局命名空間內(nèi)分發(fā)action 或提交 mutation,將{ root: true } 作為第三參數(shù)傳給dispatch或commit即可。
好了以上就是store的用法,更多用法大家可以繼續(xù)探索。接下來我們來講一下store的實(shí)現(xiàn)原理。
4.實(shí)現(xiàn)原理
我們先來看看構(gòu)造方法:
//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))
}
創(chuàng)建了各個成員變量,其中最重要的是_modules的創(chuàng)建。之后依次執(zhí)行installModule(),resetStoreState()和plugins.forEach(plugin => plugin(this))。我們先來看看_modules的創(chuàng)建。
4.1. ModuleCollection
_modules對所有的module進(jìn)行了初始化并構(gòu)造了其依賴關(guān)系。其構(gòu)造方法實(shí)現(xiàn)如下:
//ModuleCollection
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
調(diào)用了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,所以會創(chuàng)建一個Module對象,并為root賦值,把它設(shè)置為根module。Module的構(gòu)造方法如下:
//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,遞歸執(zhí)行register()方法。而再次執(zhí)行register()方法時,path.length === 0為false,會找到它的parent,并執(zhí)行addChild()方法:
//Module
addChild (key, module) {
this._children[key] = module
}
由此實(shí)現(xiàn)了modules依賴關(guān)系的創(chuàng)建,生成了一個modules樹。接下來我們看installModule()方法的實(shí)現(xiàn):
4.2.installModule()
它的作用是對根module進(jìn)行初始化,根據(jù)上一章節(jié)生成的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)
})
}
如果設(shè)置了nameSpace,則向_modulesNamespaceMap注冊。之后如果不是根module,將模塊的state添加到state鏈中,就可以按照state.moduleName進(jìn)行訪問。
接下來創(chuàng)建makeLocalContext上下文,為該module設(shè)置局部的dispatch、commit方法以及getters和state,為的是在局部的模塊內(nèi)調(diào)用模塊定義的action和mutation,這個過程具體就不展開了。
接下來分別遍歷_mutations,_actions和_wrappedGetters進(jìn)行注冊。注冊過程大同小異,我們?nèi)?code>_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)
})
}
把對應(yīng)的值函數(shù)封裝后存儲在數(shù)組里面,然后作為store._mutations的屬性。store._mutations收集了我們傳入的所有mutation函數(shù)。
installModule()的分析就完成了,我們繼續(xù)看resetStoreState()的實(shí)現(xiàn)。
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對象進(jìn)行存儲,通過Object.defineProperty方法為getters對象建立屬性并實(shí)現(xiàn)響應(yīng)式,使得我們通過this.$store.getters.xxxgetter能夠訪問到該getters。最后利用reactive為state實(shí)現(xiàn)響應(yīng)式。
4.5.install
在vue里,通過Vue.use()掛載全局變量,所以業(yè)務(wù)組件中通過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,執(zhí)行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執(zhí)行handler操作,并遍歷_actionSubscribers通知訂閱者。
5.總結(jié)
Vuex的狀態(tài)管理方式store的使用和原理就介紹到這里。最后截取一張官網(wǎng)的圖為大家進(jìn)行一下總結(jié)。
