想了解Vuex?一定先把這篇筆記碼住

一、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)

透過輸出,我們發現其結構如下:

想了解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)

}

}

}

雲管理服務專家新鈦雲服 林泓輝原創