Vuex中的核心方法
Vuex中的核心方法
Vuex
是一個專為Vue.js
應用程式開發的狀態管理模式,其採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。每一個Vuex
應用的核心就是store
倉庫,store
基本上就是一個容器,它包含著你的應用中大部分的狀態state
。
描述
在大量的業務場景下,不同的模組元件之間確實需要共享資料,也需要對其進行修改操作。也就引發軟體設計中的矛盾:模組元件之間需要共享資料和資料可能被任意修改導致不可預料的結果。為了解決其矛盾,軟體設計上就提出了一種設計和架構思想,將全域性狀態進行統一的管理,並且需要獲取、修改等操作必須按我設計的套路來,就好比馬路上必須遵守的交通規則,右行斑馬線就是隻能右轉一個道理,統一了對全域性狀態管理的唯一入口,使程式碼結構清晰、更利於維護。狀態管理模式從軟體設計的角度,就是以一種統一的約定和準則,對全域性共享狀態資料進行管理和操作的設計理念。你必須按照這種設計理念和架構來對你專案裡共享狀態資料進行CRUD
關於
Vuex
的五個核心概念,在這裡可以簡單地進行總結:
state
: 基本資料。getters
: 從基本資料派生的資料。mutations
: 提交更改資料的方法,同步操作。actions
: 像一個裝飾器,包裹mutations
,使之可以非同步。modules
: 模組化Vuex
。
State
Vuex
使用單一狀態樹,即用一個物件就包含了全部的狀態資料,state
作為構造器選項,定義了所有我們需要的基本狀態引數,也就是說state
便是唯一資料來源SSOT
,同樣每個應用將僅僅包含一個store
例項。單一狀態樹讓我們能夠直接地定位任一特定的狀態片段,在除錯的過程中也能輕易地取得整個當前應用狀態的快照。此外單狀態樹和模組化並不衝突,我們仍然可以將狀態和狀態變更事件分佈到各個子模組中。使用Vuex
Vuex
,雖然將所有的狀態放到Vuex
會使狀態變化更顯式和易除錯,但也會使程式碼變得冗長和不直觀,如果有些狀態嚴格屬於單個元件,最好還是作為元件的區域性狀態。
在Vue元件中獲得Vuex狀態
從store
例項中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態,由於Vuex
的狀態儲存是響應式的,所以在這裡每當store.state.count
變化的時候,都會重新求取計算屬性,進行響應式更新。
const store = new Vuex.Store({ state: { count: 0 } }) const vm = new Vue({ //.. store, computed: { count: function(){ return this.$store.state.count; } }, //.. })
mapState輔助函式
mapState
函式返回的是一個物件,當一個元件需要獲取多個狀態時候,將這些狀態都宣告為計算屬性會有些重複和冗餘,為了解決這個問題,我們可以使用mapState
輔助函式幫助我們生成計算屬性。
// 在單獨構建的版本中輔助函式為 Vuex.mapState
import { mapState } from "vuex";
export default {
// ...
computed: mapState({
// 箭頭函式
count: state => state.count,
// 傳字串引數 count 等同於 state => state.count
countAlias: "count",
// 使用 this
countPlusLocalState: function(state) {
return state.count + this.localCount;
}
})
// ...
}
如果當前元件中還有區域性計算屬性需要定義,通常可以使用物件展開運算子...
將此物件混入到外部物件中。
import { mapState } from "vuex";
export default {
// ...
computed: {
localComputed: function() { /* ... */ },
// 使用物件展開運算子將此物件混入到外部物件中
...mapState({
// ...
})
// ...
}
// ...
}
Getter
getters
即從store
的state
中派生出的狀態,例如我們需要對列表進行過濾並計數,如果有多個元件需要用到某個屬性,我們要麼複製這個函式,或者抽取到一個共享函式然後在多處匯入它,這兩種方式無論哪種方式都不是很理想。而Vuex
允許我們在store
中定義getter
(可以認為是store
的計算屬性),就像計算屬性一樣getter
的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。
訪問getters
getters
接收state
作為其第一個引數,接受其他getters
作為第二個引數,如不需要則第二個引數可以省略,與state
一樣,我們也可以通過Vue
的Computed
獲得Vuex
的getters
。
const store = new Vuex.Store({
state: {
count:0
},
getters: {
// 單個引數
countDouble: function(state){
return state.count * 2
},
// 兩個引數
countDoubleAndDouble: function(state, getters) {
return getters.countDouble * 2
}
}
})
const vm = new Vue({
//..
store,
computed: {
count: function(){
return this.$store.state.count;
},
countDouble: function(){
return this.$store.getters.countDouble;
},
countDoubleAndDouble: function(){
return this.$store.getters.countDoubleAndDouble;
}
},
//..
})
mapGetters輔助函式
mapGetters
輔助函式是將store
中的getters
對映到區域性計算屬性,與state
類似。
import { mapGetters } from "vuex";
export default {
// ...
computed: {
// 使用物件展開運算子將 getters 混入 computed 物件中
...mapGetters([
"countDouble",
"CountDoubleAndDouble",
//..
]),
...mapGetters({
// 對映 this.double 為 store.getters.countDouble
double: "countDouble"
})
}
// ...
}
Mutation
提交mutation
是更改Vuex
中的store
中的狀態的唯一方法,mutation
必須是同步的,如果要非同步需要使用action
。
定義mutation
每個mutation
都有一個字串的事件型別type
和一個回撥函式handler
,這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受state
作為第一個引數,提交載荷作為第二個引數(提交荷載在大多數情況下應該是一個物件),提交荷載也可以省略的。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
// 無提交荷載
increment: function(state) {
state.count++;
},
// 提交荷載
incrementN: function(state, payload) {
state.count += payload.n;
}
}
})
你不能直接呼叫一個mutation handler
,這個選項更像是事件註冊,當觸發一個型別為increment
的mutation
時,呼叫此函式,要喚醒一個mutation handler
,你需要以相應的type
呼叫store.commit
方法。
//無提交荷載
this.$store.commit("increment");
//提交荷載
this.$store.commit("incrementN", { n: 100 });
Mutations需遵守Vue的響應規則
既然Vuex
的store
中的狀態是響應式的,那麼當我們變更狀態時,監視狀態的Vue
元件也會自動更新,這也意味著Vuex
中的mutation
也需要與使用Vue
一樣遵守一些注意事項:
- 最好提前在你的
store
中初始化好所有所需屬性。 - 當需要在物件上新增新屬性時,應該使用
Vue.set(obj, "newProp", 1)
, 或者以新物件替換老物件,例如state.obj = { ...state.obj, newProp: 1 }
。
Mutation必須是同步函式
一條重要的原則就是mutation
必須是同步函式,假如我們正在debug
一個app
並且觀察devtool
中的mutation
日誌,每一條mutation
被記錄,devtools
都需要捕捉到前一狀態和後一狀態的快照,然而如果在mutation
中使用非同步函式中的回撥讓這不可能完成,因為當mutation
觸發的時候,回撥函式還沒有被呼叫,devtools
不知道什麼時候回撥函式實際上被呼叫,實質上任何在回撥函式中進行的狀態的改變都是不可追蹤的。
在mutation
中混合非同步呼叫會導致你的程式很難除錯,當你呼叫了兩個包含非同步回撥的mutation
來改變狀態,你無法知道什麼時候回撥和哪個先回調,這就是為什麼要區分Mutation
和Action
這兩個概念,在Vuex
中,mutation
都是同步事務,任何由提交的key
導致的狀態變更都應該在此刻完成。
mapMutations輔助函式
與其他輔助函式類似,你可以在元件中使用this.$store.commit("xxx")
提交mutation
,或者使用mapMutations
輔助函式將元件中的methods
對映為store.commit
呼叫。
import { mapMutations } from "vuex";
export default {
//..
methods: {
...mapMutations([
"increment" // 對映 this.increment() 為 this.$store.commit("increment")
]),
...mapMutations({
add: "increment" // 對映 this.add() 為 this.$store.commit("increment")
})
}
// ...
}
Action
Action
類似於mutation
,不同在於Action
提交的是mutation
,而不是直接變更狀態,而且Action
可以包含任意非同步操作。
註冊actions
Action
函式接受一個與store
例項具有相同方法和屬性的context
物件,因此你可以呼叫context.commit
提交一個mutation
,或者通過context.state
和context.getters
來獲取state
和getters
。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment: function(state) {
state.count++;
}
},
actions: {
increment: function(context) {
setInterval(() => context.commit("increment"), 1000);
}
}
})
分發actions
Action
通過store.dispatch
方法觸發,同樣也支援以載荷方式和物件方式進行分發。
// 分發
this.$store.dispatch("increment");
// 以載荷形式分發
store.dispatch("incrementN", { n: 10 });
// 以物件形式分發
store.dispatch({ type: "incrementN", n: 10 });
mapActions輔助函式
使用mapActions
輔助函式可以將元件的methods
對映為store.dispatch
呼叫。
import { mapActions } from "vuex";
export default {
//..
methods: {
...mapActions([
"incrementN" //對映 this.incrementN() 為 this.$store.dispatch("incrementN")
]),
...mapActions({
add: "incrementN" //對映 this.add() 為 this.$store.dispatch("incrementN")
})
}
// ...
}
組合Action
Action
通常是非同步的,在一些場景下我們需要組合Action
用以處理更加複雜的非同步流程,store.dispatch
可以處理被觸發的action
的處理函式返回的Promise
,並且store.dispatch
仍舊返回Promise
。一個store.dispatch
在不同模組中可以觸發多個action
函式,在這種情況下,只有當所有觸發函式完成後,返回的Promise
才會執行。
// ...
actions: {
actionA: function({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit("someMutation");
resolve();
}, 1000)
})
}
}
// ...
// 在觸發Actions時
// ...
store.dispatch("actionA").then(() => {
// ...
})
// ...
// 在另外一個 action 中
// ...
actions: {
// ...
actionB: function({ dispatch, commit }) {
return dispatch("actionA").then(() => {
commit("someOtherMutation");
})
}
}
// ...
// 使用 async/await
// 當然此時getData()和getOtherData()需要返回Promise
actions: {
actionA: async function({ commit }) {
commit("gotData", await getData());
},
actionB: async function({ dispatch, commit }) {
await dispatch("actionA");
commit("gotOtherData", await getOtherData());
}
}
// ...
Module
由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件,當應用變得非常複雜時,store
物件就有可能變得相當臃腫,為了解決以上問題,Vuex
允許我們將store
分割成模組。
模組分割
當進行模組分割時,每個模組擁有自己的state
、mutation
、action
、getter
,甚至是巢狀子模組,即從上至下進行同樣方式的分割。
const moduleA = {
state: () => ({ /* ... */ }),
mutations: { /* ... */ },
actions: { /* ... */ },
getters: { /* ... */ }
}
const moduleB = {
state: () => ({ /* ... */ }),
mutations: { /* ... */ },
actions: { /* ... */ }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
模組的區域性狀態
對於模組內部的mutation
和getter
,接收的第一個引數是模組的區域性狀態,對於模組內部的getter
,根節點狀態會作為第三個引數。
const moduleA = {
state: { count: 0 },
mutations: {
increment: function(state) {
// state 模組的區域性狀態
state.count++;
}
},
getters: {
doubleCount: function(state) {
return state.count * 2
},
sumWithRootCount: function(state, getters, rootState) {
return state.count + rootState.count;
}
}
}
同樣對於模組內部的action
,區域性狀態通過context.state
暴露出來,根節點狀態則為context.rootState
。
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum: function({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit("increment");
}
}
}
}
名稱空間
預設情況下,模組內部的action
、mutation
和getter
是註冊在全域性名稱空間的——這樣使得多個模組能夠對同一mutation
或action
作出響應。如果希望你的模組具有更高的封裝度和複用性,你可以通過新增namespaced: true
的方式使其成為帶名稱空間的模組,當模組被註冊後,它的所有getter
、action
及mutation
都會自動根據模組註冊的路徑調整命名。
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模組內容(module assets)
state: () => ({ ... }), // 模組內的狀態已經是巢狀的了,使用 `namespaced` 屬性不會對其產生影響
getters: {
isAdmin: function() { ... } // -> getters['account/isAdmin']
},
actions: {
login: function() { ... } // -> dispatch('account/login')
},
mutations: {
login: function() { ... } // -> commit('account/login')
},
// 巢狀模組
modules: {
// 繼承父模組的名稱空間
myPage: {
state: () => ({ ... }),
getters: {
profile: function() { ... } // -> getters['account/profile']
}
},
// 進一步巢狀名稱空間
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular: function() { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
啟用了名稱空間的getter
和action
會收到區域性化的getter
,dispatch
和commit
。換言之,你在使用模組內容module assets
時不需要在同一模組內額外新增空間名字首,更改namespaced
屬性後不需要修改模組內的程式碼。
如果你希望使用全域性state
和getter
,rootState
和rootGetters
會作為第三和第四引數傳入getter
,也會通過context
物件的屬性傳入action
。若需要在全域性名稱空間內分發action
或提交mutation
,將{ root: true }
作為第三引數傳給dispatch
或commit
即可。
modules: {
foo: {
namespaced: true,
getters: {
// 在這個模組的 getter 中,`getters` 被區域性化了
// 你可以使用 getter 的第四個引數來呼叫 `rootGetters`
someGetter(state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> "foo/someOtherGetter"
rootGetters.someOtherGetter // -> "someOtherGetter"
},
someOtherGetter: state => { /* ... */ }
},
actions: {
// 在這個模組中, dispatch 和 commit 也被區域性化了
// 他們可以接受 `root` 屬性以訪問根 dispatch 或 commit
someAction({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> "foo/someGetter"
rootGetters.someGetter // -> "someGetter"
dispatch("someOtherAction") // -> "foo/someOtherAction"
dispatch("someOtherAction", null, { root: true }) // -> "someOtherAction"
commit("someMutation") // -> "foo/someMutation"
commit("someMutation", null, { root: true }) // -> "someMutation"
},
someOtherAction(ctx, payload) { /* ... */ }
}
}
}
若需要在帶名稱空間的模組註冊全域性action
,你可新增root: true
,並將這個action
的定義放在函式handler
中。
{
actions: {
someOtherAction({ dispatch }) {
dispatch("someAction")
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler(namespacedContext, payload) { /* ... */ } // -> "someAction"
}
}
}
}
}
當使用mapState
、mapGetters
、mapActions
和mapMutations
這些函式來繫結帶名稱空間的模組時,寫起來可能比較繁瑣,對於這種情況,你可以將模組的空間名稱字串作為第一個引數傳遞給上述函式,這樣所有繫結都會自動將該模組作為上下文。或者你可以通過使用createNamespacedHelpers
建立基於某個名稱空間輔助函式。它返回一個物件,物件裡有新的繫結在給定名稱空間值上的元件繫結輔助函式
// ...
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
"some/nested/module/foo", // -> this["some/nested/module/foo"]()
"some/nested/module/bar" // -> this["some/nested/module/bar"]()
])
}
// ...
// ...
computed: {
...mapState("some/nested/module", {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions("some/nested/module", [
"foo", // -> this.foo()
"bar" // -> this.bar()
])
}
// ...
// ...
import { createNamespacedHelpers } from "vuex"
const { mapState, mapActions } = createNamespacedHelpers("some/nested/module")
export default {
computed: {
// 在 `some/nested/module` 中查詢
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查詢
...mapActions([
"foo",
"bar"
])
}
}
// ...
模組動態註冊
在store
建立之後,你可以使用store.registerModule
方法註冊模組,之後就可以通過store.state.myModule
和store.state.nested.myModule
訪問模組的狀態。模組動態註冊功能使得其他Vue
外掛可以通過在store
中附加新模組的方式來使用Vuex
管理狀態。例如vuex-router-sync
外掛就是通過動態註冊模組將vue-router
和vuex
結合在一起,實現應用的路由狀態管理。你也可以使用store.unregisterModule(moduleName)
來動態解除安裝模組,注意你不能使用此方法解除安裝靜態模組,即建立store
時宣告的模組。此外你可以通過store.hasModule(moduleName)
方法檢查該模組是否已經被註冊到store
。
import Vuex from "vuex";
const store = new Vuex.Store({ /* 選項 */ })
// 註冊模組 `myModule`
store.registerModule("myModule", {
// ...
})
// 註冊巢狀模組 `nested/myModule`
store.registerModule(["nested", "myModule"], {
// ...
})
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://vuex.vuejs.org/zh/
https://www.jianshu.com/p/1fdf9518cbdf
https://www.jianshu.com/p/29467543f77a
https://juejin.cn/post/6844903624137523213
https://segmentfault.com/a/1190000024371223
https://github.com/Hibop/Hibop.github.io/issues/45