1. 程式人生 > >Vue躬行記(9)——Vuex

Vue躬行記(9)——Vuex

  Vuex是一個專為Vue.js設計的狀態管理庫,適用於多元件共享狀態的場景。Vuex能集中式的儲存和維護所有元件的狀態,並提供相關規則保證狀態的獨立性、正確性和可預測性,這不僅讓除錯變得可追蹤,還讓程式碼變得更結構化且易維護。本文所使用的Vuex,其版本是3.1.1。

一、基本用法

  首先需要引入Vue和Vuex兩個庫,如果像下面這樣在Vue之後引入Vuex,那麼Vuex會自動呼叫Vue.use()方法註冊其自身;但如果以模組的方式引用,那麼就得顯式地呼叫Vue.use()。注意,因為Vuex依賴Promise,所以對於那些不支援Promise的瀏覽器,要使用Vuex的話,得引入相關的polyfill庫,例如es6-promise。

<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>

  然後建立Vuex應用的核心:Store(倉庫)。它是一個容器,儲存著大量的響應式狀態(State),並且這些狀態不能直接修改,需要顯式地將修改請求提交到Mutation(變更)中才能實現更新,因為這樣便於追蹤每個狀態的變化。在下面的示例中,初始化了一個digit狀態,並在mutations選項中添加了兩個可將其修改的方法。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  mutations: {
    add: state => state.digit++,
    minus: state => state.digit--
  }
});

  接著建立根例項,並將store例項注入,從而讓整個應用都能讀寫其中的狀態,在元件中可通過$store屬性訪問到它,如下所示,以計算屬性的方式讀取digit狀態,並通過呼叫commit()方法來修改該狀態。

var vm = new Vue({
  el: "#container",
  store: store,
  computed: {
    digit() {
      return this.$store.state.digit;
    }
  },
  methods: {
    add() {
      this.$store.commit("add");
    },
    minus() {
      this.$store.commit("minus");
    }
  }
});

  最後將根例項中的方法分別註冊到兩個按鈕的點選事件中,如下所示,每當點選這兩個按鈕時,狀態就會更新,並在頁面中顯示。

<div id="container">
  <p>{{digit}}</p>
  <button @click="add">增加</button>
  <button @click="minus">減少</button>
</div>

二、主要組成

  Vuex的主要組成除了上一節提到的Store、State和Mutation之外,還包括Getter和Action,本節會對其中的四個做重點講解,它們之間的關係如圖2所示。

圖2  四者的關係

1)State

  State是一個可儲存狀態的物件,在應用的任何位置都能被訪問到,並且作為單一資料來源(Single Source Of Truth)而存在。

  當元件需要讀取大量狀態時,一個個的宣告成計算屬性會顯得過於繁瑣而冗餘,於是Vuex提供了一個名為mapState()的輔助函式,用來將狀態自動對映成計算屬性,它的引數既可以是陣列,也可以是物件。

  當計算屬性的名稱與狀態名稱相同,並且不需要做額外處理時,可將名稱組成一個字串陣列傳遞給mapState()函式,在元件中可按原名呼叫,如下所示。

var vm = new Vue({
  computed: Vuex.mapState([ "digit" ])
});

  當計算屬性的名稱與狀態名稱不同,或者計算屬性讀取的是需要處理的狀態時,可將一個物件傳遞給mapState()函式,其鍵就是計算屬性的名稱,而其值既可以是函式,也可以是字串,如下程式碼所示。如果是函式,那麼它的第一個引數是state,即狀態物件;如果是字串,那麼就是從state中指定一個狀態作為計算屬性。

var vm = new Vue({
  computed: Vuex.mapState({
    digit: state => state.digit,
    alias: "digit"        //相當於state => state.digit
  })
});

  因為mapState()函式返回的是一個物件,所以當元件內已經包含計算屬性時,可以對其應用擴充套件運算子(...)來進行合併,如下所示,這是一種極為簡潔的寫法。

var vm = new Vue({
  computed: {
    name() {},
    ...Vuex.mapState([ "digit" ])
  }
});

2)Getter

  Getter是從State中派生出的狀態,當多個元件要對同一個狀態進行相同的處理時,就需要將狀態轉移到Getter中,以免產生重複的冗餘程式碼。

  Getter相當於Store的計算屬性,它能接收兩個引數,第一個是state物件,第二個是可選的getters物件,該引數能讓不同的Getter之間相互訪問。Getter的返回值會被快取,並且只有當依賴值發生變化時才會被重新計算。不過當返回值是函式時,其結果就不會被快取,如下所示,其中caculate返回的是個數字,而sum返回的是個函式。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  getters: {
    caculate: state => {
      return state.digit + 2;
    },
    sum: state => right => {
      return state.digit + right;
    }
  }
});

  在元件內可通過this.$store.getters訪問到Getter中的資料,如下所示,讀取了上一個示例中的兩個Getter。

var vm = new Vue({
  methods: {
    add() {
      this.$store.getters.caculate;
      this.$store.getters.sum(1);
    }
  }
});

  Getter也有一個輔助函式,用來將Getter自動對映為元件的計算屬性,名字叫mapGetters(),其引數也是陣列或物件。但與之前的mapState()不同,當引數是物件時,其值不能是函式,只能是字串,如下所示,為了對比兩種寫法,聲明瞭兩個computed選項。

var vm = new Vue({
  computed: Vuex.mapGetters([ "caculate" ]),
  computed: Vuex.mapGetters({
    alias: "caculate"
  })
});

3)Mutation

  更改狀態的唯一途徑是提交Mutation,Vuex中的Mutation類似於事件,也包含一個型別和回撥函式,在函式體中可進行狀態更改的邏輯,並且它能接收兩個引數,第一個是state物件,第二個是可選的附加資料,叫載荷(Payload)。下面這個Mutation的型別是“interval”,接收了兩個引數。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  mutations: {
    interval: (state, payload) => state.digit += payload.number
  }
});

  在元件中不能直接呼叫Mutation的回撥函式,得通過this.$store.commit()方法觸發更新,如下所示,採用了兩種提交方式,第一種是傳遞type引數,第二種是傳遞包含type屬性的物件。

var vm = new Vue({
  methods: {
    interval() {
      this.$store.commit("interval", { number: 2 });             //第一種
      this.$store.commit({ type: "interval", number: 2 });       //第二種
    }
  }
});

  當多人協作時,Mutation的型別適合寫成常量,這樣更容易維護,也能減少衝突。

const INTERVAL = "interval";

  Mutation有一個名為mapMutations()的輔助函式,其寫法和mapState()相同,它能將Mutation自動對映為元件的方法,如下所示。

var vm = new Vue({
  methods: Vuex.mapMutations(["interval"])
  //相當於
  methods: {
    interval(payload) {
      this.$store.commit(INTERVAL, payload);
    }
  }
});

  注意,為了能追蹤狀態的變更,Mutation只支援同步的更新,如果要非同步,那麼得使用Action。

4)Action

  Action類似於Mutation,但不同的是它可以包含非同步操作,並且只能用來通知Mutation,不會直接更新狀態。Action的回撥函式能接收兩個引數,第一個是與Store例項具有相同屬性和方法的context物件(注意,不是Store例項本身),第二個是可選的附加資料,如下所示,呼叫commit()方法提交了一個Mutation。

const store = new Vuex.Store({
  actions: {
    interval(context, payload) {
      context.commit("interval", payload);
    }
  }
});

  在元件中能通過this.$store.dispatch()方法分發Action,如下所示,與commit()方法一樣,它也有兩種提交方式。

var vm = new Vue({
  methods: {
    interval() {
      this.$store.dispatch("interval", { number: 2 });            //第一種
      this.$store.dispatch({type: "interval", number: 2});        //第二種
    }
  }
});

  注意,由於dispatch()方法返回的是一個Promise物件,因此它能以一種更優雅的方式來處理非同步操作,如下所示。

var vm = new Vue({
  methods: {
    interval() {
      this.$store.dispatch("interval", { number: 2 }).then(() => {
        console.log("success");
      });
    }
  }
});

  Action有一個名為mapActions()的輔助函式,其寫法和mapState()相同,它能將Action自動對映為元件的方法,如下所示。

var vm = new Vue({
  methods: Vuex.mapActions([ "interval" ])
});

三、模組

  當應用越來越大時,為了避免Store變得過於臃腫,有必要將其拆分到一個個的模組(Module)中。每個模組就是一個物件,包含屬於自己的State、Getter、Mutation和Action,甚至還能巢狀其它模組。

1)區域性狀態

  對於模組內部的Getter和Mutation,它們接收的第一個引數是模組的區域性狀態,而Getter的第三個引數rootState和Action的context.rootState屬性可訪問根節點狀態(即全域性狀態),如下所示。

const moduleA = {
  state: { digit: 0 },
  mutations: {
    add: state => state.digit++
  },
  getters: {
    caculate: (state, getter, rootState) => {
      return state.digit + 2;
    }
  },
  actions: {
    interval(context, payload) {
      context.commit("add", payload);
    }
  }
};

2)名稱空間

  預設情況下,只有在訪問State時需要帶名稱空間,而Getter、Mutation和Action的呼叫方式不變。將之前的moduleA模組註冊到Store例項中,如下所示,modules選項的值是一個子模組物件,其鍵是模組名稱。

const store = new Vuex.Store({
  modules: {
    a: moduleA
  }
});

  如果要訪問模組中的digit狀態,那麼可以像下面這樣寫。

store.state.a.digit;

  當模組的namespaced屬性為true時,它的Getter、Mutation和Action也會帶名稱空間,在使用時,需要新增名稱空間字首,如下程式碼所示,此舉大大提升了模組的封裝性和複用性。

const moduleA = {
  namespaced: true
};
var vm = new Vue({
  el: "#container",
  store: store,
  methods: {
    add() {
      this.$store.commit("a/add");
    },
    caculate() {
      this.$store.getters["a/caculate"];
    }
  }
});

  如果要在帶名稱空間的模組中提交全域性的Mutation或分發全域性的Action,那麼只要將{root: true}作為第三個引數傳給commit()或dispatch()就可實現,如下所示。

var vm = new Vue({
  methods: {
    add() {
      this.$store.dispatch("add", null, { root: true });
      this.$store.commit("add", null, { root: true });
    }
  }
});

  如果要在帶名稱空間的模組中註冊全域性的Action,那麼需要將其修改成物件的形式,然後新增root屬性並設為true,再將Action原先的定義轉移到handler()函式中,如下所示。

const moduleA = {
  actions: {
    interval: {
      root: true,
      handler(context, payload) {}
    }
  }
};

3)輔助函式

  當使用mapState()、mapGetters()、mapMutations()和mapActions()四個輔助函式對帶名稱空間的模組做對映時,需要顯式的包含名稱空間,如下所示。

var vm = new Vue({
  computed: Vuex.mapState({
    digit: state => state.a.digit
  }),
  methods: Vuex.mapMutations({
    add: "a/add"
  })
});

  這四個輔助函式的第一個引數都是可選的,用於繫結名稱空間,可簡化對映過程,如下所示。

var vm = new Vue({
  computed: Vuex.mapState("a", {
    digit: state => state.digit
  }),
  methods: Vuex.mapMutations("a", {
    add: "add"
  })
});

  Vuex還提供了另一個輔助函式createNamespacedHelpers(),可建立繫結名稱空間的輔助函式,如下所示。

const { mapState, mapMutations } = Vuex.createNamespacedHelpers("a");

四、動態註冊

  在建立Store例項後,可通過registerModule()方法動態註冊模組,如下程式碼所示,呼叫了兩次registerModule()方法,第一次註冊了模組“a”,第二次註冊了巢狀模組“a/b”。

const store = new Vuex.Store();
store.registerModule("a", moduleA);    
store.registerModule(["a", "b"], moduleAB);

  通過store.state.a和store.state.a.b可訪問模組的區域性狀態。如果要解除安裝動態註冊的模組,那麼可以通過unregisterModule()方法實現。

  registerModule()方法的第三個引數是可選的配置物件,當preserveState屬性的值為true時(如下所示),在註冊模組時會忽略模組中的狀態,即無法在store中讀取模組中的狀態。

store.registerModule("a", moduleA, { preserveState: true });

五、表單處理

  表單預設能直接修改元件的狀態,但是在Vuex中,狀態只能由Mutation觸發更新。為了能更好的追蹤狀態的變化,也為了能更符合Vuex的思維,需要讓表單控制元件與狀態繫結在一起,並通過input或change事件監聽狀態更新的行為,如下所示。

<div id="container">
  <input :value="digit" @input="add" />
</div>

  然後在Store例項中初始化digit狀態,並新增更新狀態的Mutation,如下所示。

const store = new Vuex.Store({
  state: {
    digit: 0
  },
  mutations: {
    add: (state, value) => {
      state.digit = value;
    }
  }
});

  最後在建立根例項時,將digit狀態對映成它的計算屬性,在事件處理程式add()中呼叫commit()方法,並將控制元件的值作為第二個引數傳入,如下所示。

var vm = new Vue({
  el: "#container",
  store: store,
  computed: Vuex.mapState(["digit"]),
  methods: {
    add(e) {
      this.$store.commit("add", e.target.value);
    }
  }
});

  還有一個方法也能實現相同的功能,那就是在控制元件上使用v-model指令,但需要與帶setter的計算屬性配合,如下所示(只給出了關鍵部分的程式碼)。

<div id="container">
  <input v-model="digit" />
</div>
<script>
  var vm = new Vue({
    computed: {
      digit: {
        get() {
          return this.$store.state.digit;
        },
        set(value) {
          this.$store.commit("add", value);
        }
      }
    }
  });
</script>

&n