1. 程式人生 > 實用技巧 >Vuex中的核心方法

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即從storestate中派生出的狀態,例如我們需要對列表進行過濾並計數,如果有多個元件需要用到某個屬性,我們要麼複製這個函式,或者抽取到一個共享函式然後在多處匯入它,這兩種方式無論哪種方式都不是很理想。而Vuex允許我們在store中定義getter(可以認為是store的計算屬性),就像計算屬性一樣getter的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。

訪問getters

getters接收state作為其第一個引數,接受其他getters作為第二個引數,如不需要則第二個引數可以省略,與state一樣,我們也可以通過VueComputed獲得Vuexgetters

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,這個選項更像是事件註冊,當觸發一個型別為incrementmutation時,呼叫此函式,要喚醒一個mutation handler,你需要以相應的type呼叫store.commit方法。

//無提交荷載
this.$store.commit("increment");
//提交荷載
this.$store.commit("incrementN", { n: 100 });

Mutations需遵守Vue的響應規則

既然Vuexstore中的狀態是響應式的,那麼當我們變更狀態時,監視狀態的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來改變狀態,你無法知道什麼時候回撥和哪個先回調,這就是為什麼要區分MutationAction這兩個概念,在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.statecontext.getters來獲取stategetters

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分割成模組。

模組分割

當進行模組分割時,每個模組擁有自己的statemutationactiongetter,甚至是巢狀子模組,即從上至下進行同樣方式的分割。

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 的狀態

模組的區域性狀態

對於模組內部的mutationgetter,接收的第一個引數是模組的區域性狀態,對於模組內部的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");
            }
        }
    }
}

名稱空間

預設情況下,模組內部的actionmutationgetter是註冊在全域性名稱空間的——這樣使得多個模組能夠對同一mutationaction作出響應。如果希望你的模組具有更高的封裝度和複用性,你可以通過新增namespaced: true的方式使其成為帶名稱空間的模組,當模組被註冊後,它的所有getteractionmutation都會自動根據模組註冊的路徑調整命名。

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']
                    }
                }
            }
        }
    }
})

啟用了名稱空間的getteraction會收到區域性化的getterdispatchcommit。換言之,你在使用模組內容module assets時不需要在同一模組內額外新增空間名字首,更改namespaced屬性後不需要修改模組內的程式碼。
如果你希望使用全域性stategetterrootStaterootGetters會作為第三和第四引數傳入getter,也會通過context物件的屬性傳入action。若需要在全域性名稱空間內分發action或提交mutation,將{ root: true }作為第三引數傳給dispatchcommit即可。

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"
                }
            }
        }
    }
}

當使用mapStatemapGettersmapActionsmapMutations這些函式來繫結帶名稱空間的模組時,寫起來可能比較繁瑣,對於這種情況,你可以將模組的空間名稱字串作為第一個引數傳遞給上述函式,這樣所有繫結都會自動將該模組作為上下文。或者你可以通過使用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.myModulestore.state.nested.myModule訪問模組的狀態。模組動態註冊功能使得其他Vue外掛可以通過在store中附加新模組的方式來使用Vuex管理狀態。例如vuex-router-sync外掛就是通過動態註冊模組將vue-routervuex結合在一起,實現應用的路由狀態管理。你也可以使用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