Vue躬行記(9)——Vuex

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

1、基本用法

  首先須要引入Vue和Vuex兩個庫,若是像下面這樣在Vue以後引入Vuex,那麼Vuex會自動調用Vue.use()方法註冊其自身;但若是以模塊的方式引用,那麼就得顯式地調用Vue.use()。注意,由於Vuex依賴Promise,因此對於那些不支持Promise的瀏覽器,要使用Vuex的話,得引入相關的polyfill庫,例如es6-promise。git

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

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

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

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

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>

2、主要組成

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

圖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" ])
});

3、模塊

  當應用愈來愈大時,爲了不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");

4、動態註冊

  在建立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 });

5、表單處理

  表單默認能直接修改組件的狀態,可是在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>
相關文章
相關標籤/搜索