1. 程式人生 > >Vue的狀態管理器:Vuex

Vue的狀態管理器:Vuex

歡迎大家訪問我的個人網站 - Sunday俱樂部


這一章我們來學習Vuex,如果要學習好Vuex那麼最最重要的就是要知道Vuex是幹嘛的,我們為什麼需要Vuex。因為對於Vuex來說,它的使用方式非常簡單,知識點也不多。Vuex唯一的難點就是很多人無法理解它。所以在本章我們會著重的講解Vuex的作用,我們為什麼需要它,在理解了這些之後,我們再去學習Vuex的使用就會水到渠成了。

狀態管理

我們直接來看這一段程式碼:

<div id="app">
    <com-1></com-1>
    <com-2></com-2
>
</div> <script type="text/x-template" id="com-1"> <div> <input type="button" @click="addCount" value="count++"> </div> </script> <script type="text/x-template" id="com-2"> <div> count:
{{count}} </div> </script>
<script> Vue.component('com-1', { template: '#com-1', data: function () { return { count: 0 } }, methods: { addCount: function () { this.count += 1; } } }); Vue.component('com-2'
, { template: '#com-2', data: function () { return { count: 0 } }, }); var vm = new Vue({ el: '#app', });
</script>

在這段程式碼中,我們期望能夠通過點選com-1中的count++按鈕來改變com-2中的count,使其每點選一下自加一。基於這個需求我們應該如何去實現呢?基於我們現在所學到的知識,一共有兩種解決辦法,我們來看一下。

第一種解決方案就是:通過元件傳參的方式。我們知道在Vue中,兄弟元件是沒有辦法進行傳參操作的,那麼按照我們現在所學到的知識,如果希望通過元件間傳參的方式解決這個問題,那麼就需要對我們的html結構進行一下修改,我們看下面的程式碼:

<div id="app">
    <com-1></com-1>  
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
        <com-2 v-bind:count="count"></com-2>
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{count}}
    </div>
</script>

<script>
    Vue.component('com-1', {
        template: '#com-1',
        data: function () {
            return {
                count: 0
            }
        },
        methods: {
            addCount: function () {
                this.count += 1;
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
        props: {
            count: 0
        }
    });

    var vm = new Vue({
        el: '#app'
    });
</script>

在上面的程式碼中我們把com-2變成了com-1的一個子元件,當我們點選count++按鈕的時候,我們通過propscom-2中傳遞了count的值,我們在學習元件的時候學習過 當我們使用prop進行引數傳遞的時候,父元件資料的改變會影響子元件 。這樣,我們在com-1count的變化就會在com-2中被展示出來。

在我們的Demo事例中,通過這種方法來維護count的狀態未嘗不可。不過大家想一下,在我們的實際專案中各個元件的層級會變得非常複雜,同時對於這種資料的狀態管理如果都通過這種元件之間傳參的方式來進行解決的話,則會變得非常難以維護。那麼我們就會想有沒有其他的方式可以解決這個問題?那就是採用全域性狀態管理的方法。

我們看一下下面的程式碼:

<div id="app">
    <com-1></com-1>
    <com-2></com-2>
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{count}}
    </div>
</script>

<script>
    var store = {
        count: 0
    };

    Vue.component('com-1', {
        template: '#com-1',
        data: function () {
            return store
        },
        methods: {
            addCount: function () {
                this.count += 1;
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
        data: function () {
            return store
        },
    });

    var vm = new Vue({
        el: '#app',
    });
</script>

我們看到在上面的程式碼中我們維護了一個store物件,用作兩個元件中原始資料物件的實際來源 當訪問資料物件時,一個 Vue 例項(元件例項)只是簡單的代理訪問。 所以,如果你有一處需要被多個例項間共享的狀態,可以簡單地通過維護一份資料來實現共享。現在當 store 發生變化,com-1com-2 都將自動的更新引用它們的檢視。從而達到了我們需求,當我們在com-1中點選count++的時候在com-2中展示count的值。

但是對我們現在的程式碼來說,依然存在一個非常致命的問題,那就是 在任何時間,我們應用中的任何部分,在任何資料改變後,都不會留下變更過的記錄。這在我們想要進行程式碼除錯的時候,將會變成一個噩夢。所以我們可以採用一個簡單的store模式來解決這個問題:

<div id="app">
    <com-1></com-1>
    <com-2></com-2>
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{sharedState.count}}
    </div>
</script>

<script>
    var store = {
        debug: true,
        // 資料 / 狀態
        state: {
            count: 0
        },
        // state的變化要通過mutation來進行
        mutation: {
            addCount: function (state) {
                if (store.debug) console.log('呼叫addCount方法,count自加1');
                state.count += 1;
            }
        }
    };

    Vue.component('com-1', {
        template: '#com-1',
        data: function () {
            return {
                // 私有資料
                privateState: {

                },
                // 共同資料
                sharedState: store.state
            }
        },
        methods: {
            addCount: function () {
                // 呼叫
                store.mutation.addCount(store.state);
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
        data: function () {
            return {
                // 私有資料
                privateState: {

                },
                // 共同資料
                sharedState: store.state
            }
        },
    });

    var vm = new Vue({
        el: '#app',
    });
</script>

在上面的程式碼中我們為store增加了debug、state、mutation三個屬性,statestore物件中所有狀態資料的描述,當state中的資料想要發生改變的時候都要通過mutation來進行。比如我們的count++的點選事件,則是通過呼叫store.mutation.addCount方法進行,以此來記錄資料的改變。

這樣的一種方式在Vue中被稱為store模式,我們使用store模式作為我們整個專案的狀態管理器,但是當我們的專案變得越來越大,越來越複雜的時候,我們更需要一個更加專業,更加全面的狀態管理器,那麼這個狀態管理器就是Vuex

Vuex

Vuex繼承了store模式的思想,並做了更多的擴充套件,提供了更多的功能。我們先來看一下Vuex的定義:

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。致力於管理專案中所有公用資料的狀態。

我們參照上面的幾個例子,是不是對Vuex的定義更加容易理解。Vuex是在store模式上的擴充套件,它們擁有相同的思想。我們看一下Vuex中的核心功能。

1、State :Vuex中的 “ 唯一資料來源 ”
2、Getter :Vuex 中的計算屬性,getter 的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。
3、Mutation :更改Vuex中資料的唯一方式,就是通過mutation進行修改,只支援同步操作。就像我們上面的例子一樣。
4、Action :當我們需要非同步更改資料時,通過 Action 提交的是 mutation,而不是直接變更狀態。
5、Module : 由於Vuex使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件。Vuex 允許我們將 store 分割成模組(module)。每個模組擁有自己的 state、mutation、action、getter甚至是巢狀子模組——從上至下進行同樣方式的分割。

由前面的例子我們也可以看出,我們不希望state中的狀態被直接改變(雖然我們可以這麼做),而是期望把Mutation作為改變state的唯一方式,而當我們必須要通過非同步來改變狀態的時候,我們期望使用action來提交mutaion用於改變狀態。大家思考一下Vuex為什麼要這麼去設計呢?

Vuex中說白了,任何的操作都是圍繞state來進行的,Vuex狀態管理器,作用就是管理state中的狀態,其他提供的所有功能Getter、Mutation、Action都是為了能夠更好的管理state,而之所以設計成期望通過Mutation改變狀態,是因為我們期望所有狀態的變化都是有跡可循的!我們通過一個圖示來看一下Vuex的設計流程(圖示來自Vuex官網):

這裡寫圖片描述

圖示完美的解釋了Vuex的執行流程:我們在Vue的元件中提交Dispatch操作,用以操作對應的Actions方法,然後在Actions的方法中,通過提交Commit操作呼叫對應的Mutation方法來修改對應的狀態State,最後State的改變被渲染到我們的元件中。這就是Vuex的整個核心流程,我們通過一段程式碼來描述一下:

<div id="app">
    <com-1></com-1>
    <com-2></com-2>
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{this.$store.state.count}}
    </div>
</script>

<script>
    /**
     * 宣告一個Vuex例項,這個例項全域性只有一個
     * */
    const store = new Vuex.Store({
        state: {
            count: 1
        },
        mutations: {
            /**
             * Vuex 中的 mutation 非常類似於事件:每個 mutation 都有一個字串的 事件型別 (type) 
             * 和 一個 回撥函式 (handler)。
             * 這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受 state 作為第一個引數:
             * */
            increment (state, num) {
                // 變更狀態
                state.count += num;
            }
        },
        actions: {
            /**
             * Action 函式接受一個與 store 例項具有相同方法和屬性的 context 物件,
             * 因此你可以呼叫 context.commit 提交一個 mutation,
             * 或者通過 context.state 和 context.getters 來獲取 state 和 getters。
             * 但是 context 物件並!不!等!於!store 例項本身
             * */
            increment (context, num) {
                // 延遲1秒提交commit操作
                setTimeout(function () {
                    /**
                     * 你不能直接呼叫一個 mutation handler。
                     * 這個選項更像是事件註冊:“當觸發一個型別為 increment 的 mutation 時,呼叫此函式。
                     * ”要喚醒一個 mutation handler,你需要以相應的 type 呼叫 store.commit 方法.
                     * 你可以向 store.commit 傳入額外的引數,即 mutation 的 載荷(payload)。
                     * 或者我們也可以通過 物件風格的提交方式:
                     * store.commit({
                            type: 'increment',
                            num: 1
                        })
                       這種情況下我們將在 mutation 中接收到一個物件
                     * */
                    context.commit('increment', num)
                }, 1000);
            }
        }
    })

    Vue.component('com-1', {
        template: '#com-1',
        methods: {
            addCount: function () {
                /**
                 * 我們可以直接向 store.dispatch 傳入額外的引數,即 action 的 載荷(payload)。
                 * 或者我們也可以通過 物件風格的提交方式:
                 * store.dispatch({
                        type: 'increment',
                        num: 1
                    })
                    這種情況下我們將在 action 中接收到一個物件
                 * */
                this.$store.dispatch('increment', 1);
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
    });

    var vm = new Vue({
        el: '#app',
        store , // 等同於 store:store
    });
</script>

在上面的程式碼中,我們首先通過new Vuex.Store去聲明瞭一個Vuex的例項store ,當我們點選count++按鈕的時候,呼叫this.$store.dispatch('increment', 1);其實是呼叫了Vuexactions中定義的increment方法,在這個方法中我們聲明瞭一個setTimeout,指定在1秒鐘之後執行context.commit('increment', num)方法,呼叫mutationincrement方法,然後我們在mutationincrement方法中修改了state中的count的值,使其state.count += num;。這樣的一個執行流程就是我們Vuex中所提倡的執行方法,也是我們上方圖示中所描述的執行流程。

我們知道state、mutaion、action是整個Vuex最核心的內容,在它們之外,Getter、Module也是我們必須要了解的內容。我們看下面的程式碼:

...
<script type="text/x-template" id="com-2">
   <div>
       <!-- 我們將要通過模組的路徑來調整 state的命名 -->
       <p>count: {{this.$store.state.a.count}}</p>
       <!-- Getter 會暴露為 store.getters 物件,你可以以屬性的形式訪問這些值: -->
       <p>doubleCount: {{this.$store.getters.doubleCount}}</p>
   </div>
</script>

<script>
   /**
    * 宣告一個Vuex模組
    * 預設情況下,模組內部的 action、mutation 和 getter 是註冊在全域性名稱空間的
    * ——這樣使得多個模組能夠對同一 mutation 或 action 作出響應。
    * 而 state 則是模組的區域性狀態物件!
    * 
    *    如果希望你的模組具有更高的封裝度和複用性,
    *    你可以通過新增 namespaced: true 的方式使其成為帶名稱空間的模組。
    *    當模組被註冊後,它的所有 getter、action 及 mutation 都會自動根據模組註冊的路徑調整命名。
    * */
   const moduleA = {
       // namespaced: true,
       state: {
           count: 1
       },
       getters: {
          /**
            * Getter 接受兩個引數,
            * 第一為:state 狀態物件
            * 第二位:其他的 getter 
            * */
           doubleCount: function (state, getters) {
               console.log('getters.count: ' + getters.count);
               return state.count * 2;
           },
           count: function (state) {
               return state.count;
           },
       },
       mutations: {
           increment (state, num) {
               // 這裡的 `state` 物件是模組的區域性狀態
               state.count += num;
           }
       },
       ...
   };

   /**
    * 宣告一個Vuex例項,這個例項全域性只有一個
    * */
   const store = new Vuex.Store({
       modules: {
           a: moduleA
       }
   });
   ...
</script>

在上面的程式碼中,我們只貼出了關鍵性程式碼,我們把Vuex進行了模組化的區分,提供了moduleA用以描述state、getters、mutations等,具體的作用在上面的程式碼中有詳細的註釋,我們這裡就不在過多敘述了。

總結

在本章的內容中,我們並沒有一頭扎進Vuex的細節中,因為對Vuex來說,它的具體使用方式比較容易,只要我們能夠理解它的本質作用,它的執行流程那麼在我們使用Vuex的時候就會水到渠成。

除去我們講到的Vuex的核心功能之外,Vuex還提供了一些其他的功能,比如plugins外掛,這些內容在Vuex官網中介紹的非常清楚,我們這裡就不想在進行重複介紹了。


前端技術日新月異,每一種新的思想出現,都代表了一種技術的躍進、架構的變化,那麼對於目前的前端技術而言,MVVM 的思想已經可以代表當今前端領域的前沿思想理念,Angular、React、Vue 等基於 MVVM 思想的具體實現框架,也成為了人們爭相學習的一個熱點。而 Vue 作為其中唯一沒有大公司支援但卻能與它們並駕齊驅並且隱隱有超越同類的趨勢,不得不說這種增長讓人感到驚奇。

本系列課程內容將會帶領大家由淺入深的學習 Vue 的基礎知識,瞭解 Vue 的原始碼設計和實現原理,和大家一起看一下尤雨溪先生的程式設計思想、架構設計以及如何進行程式碼實現。本系列課程內容主要分為三大部分:

Vue 的基礎知識:在這一部分將學習 Vue 的基礎語法及其原始碼的實現。例如,Vue 的生命週期鉤子如何設計?當聲明瞭一個 directive 時,Vue 究竟執行了什麼?為什麼只有通過 vue.set 函式才能為響應式物件新增響應式屬性?如果我們自己要實現一個響應式的框架的話,應該如何下手、如何思考等。
Vue的周邊生態:在這一部分將學習 Vue 的周邊生態圈,包括有哪些 UI 庫可以和 Vue 配合快速構建介面、如何使用 vue-router構建前端路由、如何使用 Vuex 進行狀態管理、如何使用 Axios 進行網路請求、如何使用 Webpack、使用 vue-cli 構建出的專案裡的各種配置有什麼意義?
專案實戰:在這一部分將會通過一個有意思的自動對話系統來進行專案實戰,爭取通過這個小專案把學到的知識點進行一個整合。

這裡寫圖片描述