一、Vuex是幹什麼用的?
Vuex是用於對複雜應用進行狀態管理用的(官方說法是它是一種狀態管理模式)。
“殺雞不用宰牛刀”。對於簡單的專案,根本用不著Vuex這把“宰牛刀”。那簡單的專案用什麼呢?用Vue。js官方提供的“事件匯流排”就可以了。
二、我們import進來的Vuex物件都包含些什麼呢?
我們使用Vuex的時候怎麼用呢?通常都是這樣:
import Vue from ‘vue’;
import Vuex from ‘vuex’;
Vue。use(Vuex);
new Vuex。Store({
state: { //放置state的值
count: 0,
str:“abcd234”
},
getters: { //放置getters方法
strLen: state => state。str。length
},
// mutations只能是同步操作
mutations: { //放置mutations方法
increment(state, payload) {
//在這裡改變state中的資料
state。count = payload。number;
}
},
// actions可以是非同步操作
actions: { //放置actions方法
actionName({ commit }) {
//dosomething
commit(‘mutationName’)
},
getSong ({commit}, id) {
api。getMusicUrlResource(id)。then(res => {
let url = res。data。data[0]。url;
})
。catch((error) => { // 錯誤處理
console。log(error);
});
}
}
});
new Vue({
el: ‘#app’,
store,
。。。
});
這裡import進來的Vuex是個什麼東西呢?我們用console。log把它輸出一下:
console。log(Vuex)
透過輸出,我們發現其結構如下:
可見,import進來的Vuex它實際上是一個物件,裡面包含了Store這一建構函式,還有幾個mapActions、mapGetters、mapMutations、mapState這幾個輔助方法(後面再講)。
除此之外,還有一個install方法。我們發現,import之後要對其進行Vue。use(Vuex)的操作。根據這兩個線索,我們就明白了,Vuex本質上就是一個Vue。js的外掛。
三、建立好的store例項怎麼在各個元件中都能引用到?
Vuex 透過 store 選項,提供了一種機制將狀態從根元件“注入”到每一個子元件中(需呼叫 Vue。use(Vuex)):
const app = new Vue({
el: ‘#app’,
// 把 store 物件提供給 “store” 選項,這可以把 store 的例項注入所有的子元件
store,
components: { Counter },
template: `
`
})
透過在根例項中註冊 store 選項,該 store例項會注入到根元件下的所有子元件中,且子元件能透過 this。$store 訪問到。
四、Vuex中的幾大核心概念
1。 State
這個很好理解,就是狀態資料。Vuex所管理的就是狀態,其它的如Actions、Mutations都是來輔助實現對狀態的管理的。Vue元件要有所變化,也是直接受到State的驅動來變化的。
可以透過this。$store。state來直接獲取狀態,也可以利用vuex提供的mapState輔助函式將state對映到計算屬性computed中去。
2。 Getters
Getters本質上是用來對狀態進行加工處理。Getters與State的關係,就像Vue。js的computed與data的關係。 getter的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。
可以透過this。$store。getters。valueName對派生出來的狀態進行訪問。或者直接使用輔助函式mapGetters將其對映到本地計算屬性中去。
3。 Mutations
更改 Vuex的 store中的狀態的唯一方法是提交 mutation。Vuex中的 mutation非常類似於事件:每個 mutation都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)。這個回撥函式就是我們實際進行狀態更改的地方。
你不能直接呼叫一個 mutation handler。這個選項更像是事件註冊:“當觸發一個型別為 increment 的 mutation時,呼叫此函式。”要喚醒一個mutation handler,你需要以相應的 type 呼叫store。commit方法,並且它會接受 state 作為第一個引數,也可以向 store。commit 傳入額外的引數,即 mutation 的 載荷(payload):
const store = new Vuex。Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 變更狀態
state。count++
}
}
})
store。commit(‘increment’)
// 。。。
mutations: {
increment (state, n) {
state。count += n
}
}
store。commit(‘increment’, 10)
在大多數情況下,載荷應該是一個物件,這樣可以包含多個欄位並且記錄的 mutation 會更易讀:
// 。。。
mutations: {
increment (state, payload) {
state。count += payload。amount
}
}
store。commit(‘increment’, {
amount: 10
})
// ***或者以物件風格提交***
mutations: {
increment (state, payload) {
state。count += payload。amount
}
}
store。commit({
type: ‘increment’,
amount: 10
})
Mutation需遵守 Vue的響應規則
既然 Vuex的 store中的狀態是響應式的,那麼當我們變更狀態時,監視狀態的 Vue元件也會自動更新。這也意味著 Vuex中的 mutation也需要與使用 Vue一樣遵守一些注意事項:
最好提前在你的 store中初始化好所有所需屬性。
當需要在物件上新增新屬性時,你應該:
使用 Vue。set(obj, ‘newProp’, 123), 或者
以新物件替換老物件。例如,利用物件展開運算子 我們可以這樣寫:
state。obj = { 。。。state。obj, newProp: 123 }
Mutation必須是同步函式
一條重要的原則就是要記住 mutation必須是同步函式。為什麼?請參考下面的例子:
mutations: {
someMutation (state) {
api。callAsyncMethod(() => {
state。count++
})
}
}
現在想象,我們正在 debug一個 app並且觀察 devtool中的 mutation日誌。每一條 mutation被記錄,devtools都需要捕捉到前一狀態和後一狀態的快照。然而,在上面的例子中 mutation中的非同步函式中的回撥讓這不可能完成:因為當 mutation觸發的時候,回撥函式還沒有被呼叫,devtools不知道什麼時候回撥函式實際上被呼叫——實質上任何在回撥函式中進行的狀態的改變都是不可追蹤的。
在元件中提交 Mutation
除了這種使用 this。$store。commit(‘xxx’) 提交 mutation的方式之外,還有一種方式,即使用 mapMutations 輔助函式將元件中的 methods對映為 this。$store。commit。例如:
import { mapMutations } from ‘vuex’
export default {
// 。。。
methods: {
。。。mapMutations([
‘increment’, // 將 `this。increment()` 對映為 `this。$store。commit(‘increment’)`
// `mapMutations` 也支援載荷:
‘incrementBy’ // 將 `this。incrementBy(amount)` 對映為 `this。$store。commit(‘incrementBy’, amount)`
]),
。。。mapMutations({
add: ‘increment’ // 將 `this。add()` 對映為 `this。$store。commit(‘increment’)`
})
}
}
經過這樣的對映之後,就可以透過呼叫方法的方式來觸發其對應的(所對映到的)mutation commit了,比如,上例中呼叫add()方法,就相當於執行了this。$store。commit(‘increment’)了。
4。 Actions
Action類似於 mutation,不同在於:
Action提交的是 mutation,而不是直接變更狀態。
Action可以包含任意非同步操作。
Action函式接受一個與 store例項具有相同方法和屬性的 context物件,因此你可以呼叫 context。commit 提交一個 mutation,或者透過 context。state 和 context。getters 來獲取 state和 getters。
分發 Action
Action 透過 store。dispatch 方法觸發:
store。dispatch(‘increment’)
Actions 支援同樣的載荷方式和物件方式進行分發:
// 以載荷形式分發
store。dispatch(‘incrementAsync’, {
amount: 10
})
// 以物件形式分發
store。dispatch({
type: ‘incrementAsync’,
amount: 10
})
另外,你需要知道, this。$store。dispatch 可以處理被觸發的 action 的處理函式返回的 Promise,並且 this。$store。dispatch 仍舊返回 Promise。
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit(‘someMutation’)
resolve()
}, 1000)
})
}
}
store。dispatch(‘actionA’)。then(() => {
// 。。。
})
5。 Module
Module是什麼概念呢?它實際上是對於store的一種切割。由於Vuex使用的是單一狀態樹,這樣整個應用的所有狀態都會集中到一個比較大的物件上面,那麼,當應用變得非常複雜時,store物件就很可能變得相當臃腫!Vuex允許我們將 store分割成一個個的模組(module)。每個模組擁有自己的 state、mutation、action、getter、甚至是巢狀子模組——從上至下進行同樣方式的分割。
(1)模組的區域性狀態
對於每個模組內部的 mutation 和 getter,接收的第一個引數就是模組的區域性狀態物件,對於模組內部的 getter,根節點狀態會作為第三個引數暴露出來。同樣,對於模組內部的 action,區域性狀態透過 context。state 暴露出來,根節點狀態則為 context。rootState:
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 這裡的 `state` 物件是模組的區域性狀態
state。count++
}
},
getters: {
doubleCount (state) {
return state。count * 2
},
sumWithRootCount (state, getters, rootState) {
return state。count + rootState。count
}
},
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state。count + rootState。count) % 2 === 1) {
commit(‘increment’)
}
}
}
}
(2)名稱空間
預設情況下,模組內部的 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 () { 。。。 } // -> getters[‘account/isAdmin’]
},
actions: {
login () { 。。。 } // -> dispatch(‘account/login’)
},
mutations: {
login () { 。。。 } // -> commit(‘account/login’)
},
// 巢狀模組
modules: {
// 繼承父模組的名稱空間
myPage: {
state: { 。。。 },
getters: {
profile () { 。。。 } // -> getters[‘account/profile’]
}
},
// 進一步巢狀名稱空間
posts: {
namespaced: true,
state: { 。。。 },
getters: {
popular () { 。。。 } // -> getters[‘account/posts/popular’]
}
}
}
}
}
})
啟用了名稱空間的 getter 和 action 會收到區域性化的 getter,dispatch 和 commit。換言之,
你在使用模組內容(module assets)時不需要在同一模組內額外新增空間名字首。更改namespaced 屬性後不需要修改模組內的程式碼。
(3)在帶名稱空間的模組內訪問全域性內容(Global Assets)
如果你希望使用全域性 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) { 。。。 }
}
}
}
(4)在帶名稱空間的模組註冊全域性 action
若需要在帶名稱空間的模組註冊全域性 action,你可新增 root: true,並將這個 action 的定義放在函式 handler 中。例如:
{
actions: {
someOtherAction ({dispatch}) {
dispatch(‘someAction’)
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { 。。。 } // -> ‘someAction’
}
}
}
}
}
(5)帶名稱空間繫結函式
當使用 mapState, mapGetters, mapActions 和 mapMutations 這些函式來繫結帶名稱空間的模組時,寫起來可能比較繁瑣:
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()
])
}
(6)模組動態註冊
在 store建立之後,你可以使用 store。registerModule 方法註冊模組:
import Vuex from ‘vuex’
const store = new Vuex。Store({ /* 選項 */ })
// 註冊模組 `myModule`
store。registerModule(‘myModule’, {
// 。。。
})
// 註冊巢狀模組 `nested/myModule`
store。registerModule([‘nested’, ‘myModule’], {
// 。。。
})
之後就可以透過 store。state。myModule 和 store。state。nested。myModule 訪問模組的狀態。
模組動態註冊功能使得其他 Vue 外掛可以透過在 store 中附加新模組的方式來使用 Vuex 管理狀態。例如,vuex-router-sync外掛就是透過動態註冊模組將vue-router 和 vuex結合在一起,實現應用的路由狀態管理。
你也可以使用 store。unregisterModule(moduleName) 來動態解除安裝模組。注意,你不能使用此方法解除安裝靜態模組(即建立 store時宣告的模組)。
注意,你可以透過 store。hasModule(moduleName) 方法檢查該模組是否已經被註冊到 store。
保留 state
在註冊一個新 module時,你很有可能想保留過去的 state,例如從一個服務端渲染的應用保留 state。你可以透過 preserveState 選項將其歸檔:store。registerModule(‘a’, module, { preserveState: true })。
當你設定 preserveState: true 時,該模組會被註冊,action、mutation和 getter會被新增到 store中,但是 state不會。這裡假設 store的 state已經包含了這個 module的 state並且你不希望將其覆寫。
(7)模組重用
有時我們可能需要建立一個模組的多個例項,例如:
建立多個 store,他們共用同一個模組 (例如當 runInNewContext 選項是 false 或 ‘once’ 時,為了在服務端渲染中避免有狀態的單例 (opens new window))
在一個 store中多次註冊同一個模組
如果我們使用一個純物件來宣告模組的狀態,那麼這個狀態物件會透過引用被共享,導致狀態物件被修改時 store 或模組間資料互相汙染的問題。
實際上這和 Vue元件內的 data 是同樣的問題。因此解決辦法也是相同的——使用一個函式來宣告模組狀態(僅 2。3。0+ 支援):
const MyReusableModule = {
state: () => ({
foo: ‘bar’
}),
// mutation, action 和 getter 等等。。。
}
五、Vuex中的表單處理
當在嚴格模式中使用 Vuex時,在屬於 Vuex的 state上使用 v-model 會比較棘手:
假設這裡的 obj 是在計算屬性中返回的一個屬於 Vuex store 的物件,在使用者輸入時,v-model 會試圖直接修改 obj。message。在嚴格模式中,由於這個修改不是在 mutation 函式中執行的, 這裡會丟擲一個錯誤。
用“Vuex 的思維”去解決這個問題的方法是:給 中繫結 value,然後偵聽 input 或者 change 事件,在事件回撥中呼叫一個方法:
computed: {
。。。mapState({
message: state => state。obj。message
})
},
methods: {
updateMessage (e) {
this。$store。commit(‘updateMessage’, e。target。value)
}
}
下面是 mutation函式:
// 。。。
mutations: {
updateMessage (state, message) {
state。obj。message = message
}
}
必須承認,這樣做比簡單地使用“v-model + 區域性狀態”要囉嗦得多,並且也損失了一些 v-model 中很有用的特性。另一個方法是使用帶有 setter 的雙向繫結計算屬性:
computed: {
message: {
get () {
return this。$store。state。obj。message
},
set (value) {
this。$store。commit(‘updateMessage’, value)
}
}
}
雲管理服務專家新鈦雲服 林泓輝原創