理解vuex的狀態管理模式架構

龍恩0707發表於2017-12-24

理解vuex的狀態管理模式架構

一: 什麼是vuex?
官方解釋如下:
vuex是一個專為vue.js應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證以一種可預測的方式發生變化。
使用方式有如下2種:
1. 如果直接在瀏覽器下引用包的話;如下:

<script src="https://unpkg.com/vue@2.5.13/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>

接下來就可以使用了。

2. 使用npm安裝
npm install vuex --save
然後在入口檔案引入方式如下:

import Vue from 'vue';
import Vuex from 'vuex';
// vuex
Vue.use(Vuex);

首先我們先來看看一個簡單的demo,再來對比下vuex到底做了什麼事情。具體為我們解決了什麼事情?
我們先來實現一個簡單的demo,有一個標籤顯示數字,兩個按鈕分別做數字的加一和減一的操作;如下使用純vue的demo如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue-demo</title>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p>{{count}}
        <button @click="inc">+</button>
        <button @click="dec">-</button>
      </p>
    </div>
    <script>
      new Vue({
        el:'#app',
        data () {
          return {
            count: 0
          }
        },
        methods: {
          inc () {
            this.count++
          },
          dec () {
            this.count--
          }
        }
      })
    </script>
  </body>
</html>

如上的程式碼的含義是:button的標籤內繫結兩個函式,當點選的時候 分別呼叫 inc 和 dec的對應的函式,接著會呼叫 vue中的methods的對應的方法
。然後會對data中的count屬性值發生改變,改變後會把最新值渲染到檢視中。

注意:上面的程式碼直接複製執行下就可以看到效果了。

現在我們來看看使用vuex的方式來實現如上demo。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue-demo</title>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
  </head>
  <body>
    <div id="app">
      <p>{{count}}
        <button @click="inc">+</button>
        <button @click="dec">-</button>
      </p>
    </div>
    <script>
      const store = new Vuex.Store({
        state: {
          count: 0
        },
        mutations: {
          inc: state => state.count++,
          dec: state => state.count--
        }
      });
      const app = new Vue({
        el: '#app',
        computed: {
          count() {
            return store.state.count;
          }
        },
        methods: {
          inc() {
            store.commit('inc');
          },
          dec() {
            store.commit('dec');
          }
        }
      });
    </script>
  </body>
</html>

注意:上面的程式碼直接複製執行下就可以看到效果了。

對比下上面的程式碼:
1. 引用vuex原始碼;
2. methods的方法不變,但是方法內的邏輯不在函式內進行,而是讓store物件去處理。
3. count資料不再是一個data函式返回的物件的屬性了。而是通過store方法內的計算欄位返回的。
具體的呼叫如下:
先view上的元素操作點選事件 -> 呼叫methods中的對應方法 -> 通過store.commit(type) 觸發store中的mutations對應的方法來改變state的屬性,值發生改變後,檢視就得到更新。
回到store物件上來,store物件是 Vuex.Store的實列。在store內分為state物件和mutations物件,其中state存放的是狀態,
比如count屬性就是它的狀態值,而mutations則是一個會引發狀態改變的所有方法。

理解什麼是狀態管理模式?
狀態管理:簡單的理解就是統一管理和維護各個vue元件的可變化狀態。

我們明白vue是單向資料流的,那麼它的狀態管理一般包含如下幾部分:
1. state; 驅動應用的資料(一般指data中返回的資料)。
2. view; 一般指模板,以宣告的方式將state的資料對映到檢視。
3. actions: 響應在view上的使用者輸入導致的狀態變化
但是當我們的應用遇到多個元件共享狀態時候,那麼單向資料流可能不太滿足我們的需求:
比如如下幾個方面:
1. 多個檢視依賴於同一狀態。
傳參的方法對於多層巢狀的元件將會非常繁瑣,並且對於兄弟元件間的狀態傳遞無能為力。

2. 我們經常會採用父子元件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的程式碼。
因此我們可以把元件的共享狀態提取出來,作為全域性來管理,因此vuex產生了。

vuex的優點:
最主要解決了元件之間共享同一狀態的問題。可以把元件的共享狀態提取出來,作為全域性來管理。

什麼情況下我應該使用 Vuex?

如果不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗餘的。確實是如此——如果您的應用夠簡單,最好不要使用 Vuex。一個簡單的 global event bus 就足夠您所需了。但是,如果您需要構建是一箇中大型單頁應用,很可能會考慮如何更好地在元件外部管理狀態,Vuex 將會成為自然而然的選擇。

二: Vuex狀態管理的demo學習
每一個Vuex應用的核心就是store(倉庫), store是儲存應用中大部分的狀態。
Vuex 和一般的全域性物件有以下幾點不同:
1. Vuex的狀態儲存是響應性的。
   當vue元件從store中讀取狀態的時候,若store中的狀態發生變化,那麼相對應的元件也就會得到相應的更新。
2. 我們不能直接修改store中的狀態。
   改變store中的狀態的唯一途徑是顯示地提交(commit)mutations.

2-1 單一狀態樹
Vuex使用的是單一狀態樹,用一個物件就包含了全部的應用層級狀態。這也意味著每個應用將僅僅包含一個store的實列。
Vuex的狀態儲存是響應性的,因此從store實列中讀取一個狀態的最簡單的方法是在計算屬性返回某個狀態。
比如demo2的程式碼:

<div id="app">
  <p>{{count}}
    <button @click="inc">+</button>
    <button @click="dec">-</button>
  </p>
</div>
<script>
  const store = new Vuex.Store({
    state: {
      count: 0
    },
    mutations: {
      inc: state => state.count++,
      dec: state => state.count--
    }
  });
  const app = new Vue({
    el: '#app',
    computed: {
      count() {
        return store.state.count;
      }
    },
    methods: {
      inc() {
        store.commit('inc');
      },
      dec() {
        store.commit('dec');
      }
    }
  });
</script>

如上程式碼,從store中讀取一個狀態可以從 computed中的count方法內就可以讀取到了。當 store.state.count變化的時候,都會重新求取計算屬性,並且觸發相關聯的DOM更新。
但是這種模式導致元件依賴的全域性狀態單列,在模組構建系統中,在每個需要使用state的元件中需要頻繁的匯入(因為每個頁面都需要
匯入 new Vuex.Store這樣的,但是一個應用系統僅僅包含一個store實列),並且在測試元件的時候需要模擬狀態。
因此vuex通過store選項,提供了一種機制將狀態從根元件注入到每一個子元件中。

使用vuex的示列:

new Vuex.Store({
  state: {
    // code 
  },
  mutations: {
    // code ....
  }
});

state是用來儲存初始化的資料的。如果要讀取資料使用 $store.state.資料變數。
修改資料使用mutations,它儲存的需要改變資料的所有方法,改變mutations裡的資料需要使用 $store.commit();

還是需要 vue-cli 中的專案來說明下:
1. 在scr目錄下新建一個vuex資料夾,在該資料夾下 新建 mystore.js檔案,程式碼如下:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 1
  },
  // 需要修改stats的資料的話,需要使用$store.commit()方法
  mutations: {
    add(state) {
      return state.count++;
    },
    reduce(state) {
      return state.count--;
    }
  }
});

2. 在src/views資料夾下 新建一個 count.vue,程式碼如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 獲取vuex檔案的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    /*
     引用mystore.js,store為資料倉儲
     */
    store: mystore
  }
</script>

3、在 src/router/index.js 路由配置檔案中配置 count.vue 的路由, 程式碼如下:

import Vue from 'vue';
import Router from 'vue-router';
// import HelloWorld from '@/views/HelloWorld';

Vue.use(Router);

const router = new Router({
  mode: 'history', // 訪問路徑不帶井號  需要使用 history模式,才能使用 scrollBehavior
  routes: [
    {
      path: '/count',
      name: 'count',
      component: resolve => require(['@/views/count'], resolve) // 使用懶載入
    }
  ]
});
export default router;

直接在瀏覽器訪問 http://localhost:8080/count 即可了。

三:學習Vuex state訪問狀態物件
上面我們的程式碼是訪問狀態物件的,是單頁應用程式中的共享值,現在我們再來看看狀態物件如何賦值給內部物件,也就是
把mystore.js中的值,賦值給模板裡data的值,也就是想直接在template中用 {{xxx}}直接呼叫資料。

我們知道vuex的狀態儲存是響應性的,從store實列中讀取狀態最簡單的方式是在計算屬性中返回某個狀態。

3-1 通過computed的計算屬性直接賦值
在src/views資料夾下新建 count2.vue, 程式碼如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 獲取vuex檔案的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed計算賦值結果是:{{ count }}</p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      count () {
        return this.$store.state.count
      }
    },
    /*
     引用mystore.js,store為資料倉儲
     */
    store: mystore
  }
</script>

在瀏覽中訪問 http://localhost:8080/count2 可以看到。

3-2 通過mapState的物件來賦值
當一個元件需要獲取多個狀態的時候,將這些狀態都宣告為計算屬性會有些重複和冗餘,為了解決這個問題,可以使用
mapState輔助函式來幫助我們生成計算屬性。

在src/views資料夾下新建 count3.vue, 程式碼如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 獲取vuex檔案的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed計算賦值結果是:{{ count }}</p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: mapState({
      count: function(state) {
        return state.count;
      }
    }),
    /*
     引用mystore.js,store為資料倉儲
     */
    store: mystore
  }
</script>

在瀏覽中訪問 http://localhost:8080/count3 可以看到。

3-3 通過mapState的陣列來賦值,
在src/views 下 新建 count4.vue, 程式碼如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 獲取vuex檔案的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed計算賦值結果是:{{ count }}</p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    /*
     * 陣列中的count 必須和 mystore.js定義的常量 mystate 中的 count同名,
     因為這是直接訪問mystate的count
    */
    computed: mapState(['count']),
    /*
     引用mystore.js,store為資料倉儲
     */
    store: mystore
  }
</script>

在瀏覽中訪問 http://localhost:8080/count4 可以看到。

四: getters計算過濾操作
    有時候我們需要從store中的state中派生出一些狀態,比如在使用store中的state之前,我們會對state中的某些欄位進行過濾一下,比如對state中的count欄位都進行加10這樣的資料;但是如果有多個元件需要用到這個操作的話,那麼我們就需要複製這個函式,或者抽取到一個共享函式內,
然後多處匯入這個函式,但是這上面兩種方式都不是太好,因為我們現在有更好的方式來解決它。
Vuex中允許我們在store中定義 getters,getters的返回值會根據它的依賴被快取起來,且只有當他的依賴值發生改變了
才會重新計算。
現在我們需要對mystore.js檔案中的count進行一個計算屬性的操作,在它輸出之前,加上10的操作。
如下程式碼有兩個按鈕,一個加5,一個減5,那麼在加5或者減5之前,先加20,然後再進行加5或者5操作。程式碼如下:
在資料夾 src/vuex/mystore.js程式碼如下:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 增加一個常量物件 state
const mystate = {
  count: 0
};
// mutations 儲存所有的方法,該方法可以改變state資料
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 減少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

// 增加一個getters物件
const mygetters = {
  mycount: function(state) {
    const count = state.count += 20;
    return count;
  }
};

// 封裝程式碼,讓外部可見
export default new Vuex.Store({
  state: mystate,  // state的固定寫法 儲存資料的狀態值
  mutations: mymutations, // mutations的固定寫法 改變資料的所有方法
  getters: mygetters
});

在src/views/下新建 count5.vue 程式碼如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 獲取vuex檔案的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed計算賦值結果是:{{ mycount }}</p>
    <p>
      <!-- 
        $store.commit('add', 5) 第一個引數是方法名,第二個是引數
      -->
      <button @click="$store.commit('add', 5)"> + </button>
      <button @click="$store.commit('reduce', 5)"> - </button>
    </p>
    <div>
      <p>使用mapMutations修改狀態:</p>
      <p>
        <button @click="add(10)">+</button>
        <button @click="reduce(10)">-</button>
      </p>
    </div>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState, mapMutations, mapGetters } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      // mapState(['count']) 此處的count必須和store.js定義的常量 mystate中的count同名,因為這是直接訪問 mystate的count
      ...mapState(['count']),
      // mapGetters 輔助函式,可以將store中的getter對映到區域性計算屬性mycount
      ...mapGetters(['mycount'])
    },
    methods: mapMutations(['add', 'reduce']),
    /*
     引用mystore.js,store為資料倉儲
     */
    store: mystore
  }
</script>

在瀏覽器下 訪問 http://localhost:8080/count5 即可。

五:Mutations修改狀態
Mutations是修改vuex中的store的唯一方法。每個mutations都有一個字串的事件型別(type)和一個回撥函式(handler)。這個回撥函式就是
我們進行更改的地方。它也會接受state作為第一個引數。

開啟上面src/vuex/mystore.js 程式碼中的 mutations 可以看到如下:

// mutations 儲存所有的方法,該方法可以改變state資料
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 減少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

我們之前呼叫 mutations的方法是這樣的 $store.commit()即可呼叫方法來改變state資料,現在我們想使用 @click="add()"來呼叫。
1. 我們在src/views/ 下新建一個count6.vue, 先匯入我們的 mapMutations方法

import { mapState, mapMutations } from 'vuex'; 

2. 使用methods屬性,並加入 mapMutations

methods: mapMutations(['add', 'reduce']); 

3. 在template中使用 @click="", 如下程式碼:

<button @click="add(5)">+</button>
<button @click="reduce(5)">-</button>

count6.vue 程式碼如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 獲取vuex檔案的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed計算賦值結果是:{{ count }}</p>
    <p>
      <!-- 
        $store.commit('add', 5) 第一個引數是方法名,第二個是引數
      -->
      <button @click="$store.commit('add', 5)"> + </button>
      <button @click="$store.commit('reduce', 5)"> - </button>
    </p>
    <div>
      <p>使用mapMutations修改狀態:</p>
      <p>
        <button @click="add(10)">+</button>
        <button @click="reduce(10)">-</button>
      </p>
    </div>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState, mapMutations } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      // mapState(['count']) 此處的count必須和store.js定義的常量 mystate中的count同名,因為這是直接訪問 mystate的count
      ...mapState(['count'])
    },
    methods: mapMutations(['add', 'reduce']),
    /*
     引用mystore.js,store為資料倉儲
     */
    store: mystore
  }
</script>

src/vuex/mystore.js程式碼修改如下:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 增加一個常量物件 state
const mystate = {
  count: 0
};
// mutations 儲存所有的方法,該方法可以改變state資料
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 減少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

// 封裝程式碼,讓外部可見
export default new Vuex.Store({
  state: mystate,  // state的固定寫法 儲存資料的狀態值
  mutations: mymutations // mutations的固定寫法 改變資料的所有方法
});

六: actions非同步修改狀態
actions是非同步修改state的狀態的。但是Mutations是同步改變狀態的。

6-1 在mystore.js中宣告actions
actions是可以呼叫Mutations的方法的。如下程式碼:

// 增加一個 actions
const myactions = {
  addAction(context) {
    console.log(context);
    context.commit('add', 5); // 呼叫mymutations 中的 add方法,並傳引數5
  },
  reduceAction(context) {
    context.commit('reduce', 5); // 呼叫mymutations中的reduce方法,並傳引數5
  }
};

myactions 裡有兩個方法 addAction 和 reduceAction , 在方法體內,我們都用 commit 呼叫了 Mutations
裡面的方法。
其中context,是上下文物件,在這邊可以理解為store本身。

在Vuex.store()中封裝

// 封裝程式碼,讓外部可見
export default new Vuex.Store({
  state: mystate,  // state的固定寫法 儲存資料的狀態值
  mutations: mymutations, // mutations的固定寫法 改變資料的所有方法
  getters: mygetters,
  actions: myactions
});

在conut7.vue中呼叫,程式碼如下:

<p>
  actions的非同步操作<br/>
  <button @click="addAction"> + </button>
  <button @click="reduceAction"> - </button>
</p>

import引用如下:

import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';

新增methods方法,程式碼如下:

methods: {
...mapMutations(['add', 'reduce']),
...mapActions(['addAction', 'reduceAction'])
}

下面是全部程式碼:

mystore.js程式碼如下:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 增加一個常量物件 state
const mystate = {
  count: 0
};
// mutations 儲存所有的方法,該方法可以改變state資料
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 減少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

// 增加一個getters物件
const mygetters = {
  mycount: function(state) {
    const count = state.count += 20;
    return count;
  }
};
// 增加一個 actions
const myactions = {
  addAction(context) {
    console.log(context);
    context.commit('add', 5); // 呼叫mymutations 中的 add方法,並傳引數5
  },
  reduceAction(context) {
    context.commit('reduce', 5); // 呼叫mymutations中的reduce方法,並傳引數5
  }
};
// 封裝程式碼,讓外部可見
export default new Vuex.Store({
  state: mystate,  // state的固定寫法 儲存資料的狀態值
  mutations: mymutations, // mutations的固定寫法 改變資料的所有方法
  getters: mygetters,
  actions: myactions
});

count7.vue程式碼如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 獲取vuex檔案的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed計算賦值結果是:{{ mycount }}</p>
    <p>
      <!-- 
        $store.commit('add', 5) 第一個引數是方法名,第二個是引數
      -->
      <button @click="$store.commit('add', 5)"> + </button>
      <button @click="$store.commit('reduce', 5)"> - </button>
    </p>
    <div>
      <p>使用mapMutations修改狀態:</p>
      <p>
        <button @click="add(10)">+</button>
        <button @click="reduce(10)">-</button>
      </p>
      <p>
        actions的非同步操作<br/>
        <button @click="addAction"> + </button>
        <button @click="reduceAction"> - </button>
      </p>
    </div>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      // mapState(['count']) 此處的count必須和store.js定義的常量 mystate中的count同名,因為這是直接訪問 mystate的count
      ...mapState(['count']),
      ...mapGetters(['mycount'])
    },
    methods: {
      ...mapMutations(['add', 'reduce']),
      ...mapActions(['addAction', 'reduceAction'])
    },
    /*
     引用mystore.js,store為資料倉儲
     */
    store: mystore
  }
</script>

可以檢視github上的程式碼

相關文章