【Vue】Vuex 從使用到原理分析(上篇)

前言

在使用 Vue 開發的過程當中,一旦項目到達必定的體量或者模塊化粒度小一點,就會遇到兄弟組件傳參,全局數據緩存等等問題。這些問題也有其餘方法能夠解決,可是經過 Vuex 來管理這些數據是最好不過的了。html

該系列分爲上中下三篇:vue

1. 什麼是 Vuex ?

Vuex 是一種狀態管理模式,它集中管理應用內的全部組件狀態,並在變化時可追蹤、可預測。vuex

也能夠理解成一個數據倉庫,倉庫裏數據的變更都按照某種嚴格的規則。vue-cli

國際慣例,上張看不懂的圖。慢慢看完下面的內容,相信這張圖再也不那麼難以理解。api

vuex

狀態管理模式

在 vue 單文件組件內,一般咱們會定義 datatemplatemethods 等。數組

  • data 一般做爲數據源保存咱們定義的數據緩存

  • template 做爲視圖映射咱們 data 中的數據app

  • methods 則會響應咱們一些操做來改變數據異步

這樣就構成了一個簡單的狀態管理。async

2. 爲何要使用 Vuex?

不少狀況下,咱們使用上面舉例的狀態自管理應用也能知足場景了。可是如前言裏所說,全局數據緩存(例如省市區的數據),兄弟組件數據響應(例如單頁下 Side 組件和 Header 組件參數傳遞)就會破壞單向數據流,而破壞的代價是很大的,輕則「臥槽,這是誰寫的不可回收垃圾,噢,是我!」,重則都沒法理清邏輯來重構。

Vuex 的出現則解決了這一難題,咱們不須要知道數據具體在哪使用,只須要去通知數據改變,而後在須要使用到的地方去使用就能夠了。

3. 需不須要使用 Vuex?

首先要肯定本身的需求是否是有那麼大...

若是肯定數據寥寥無幾,那使用一個 store 模式來管理就能夠了,殺雞不用宰牛刀。

下面用一個全局計數器來舉例。

store.js&main.js

// store.js
export default {
  state: {
    count: 0,
  },
  // 計數增長
  increaseCount() {
    this.state.count += 1;
  },

  // 計數歸零
  resetCount() {
    this.state.count = 0;
  },
};

// main.js
import store form './store';
Vue.prototype.$store = store;
複製代碼

App.vue

<template>
  <div id="app">{{ state.count }}</div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      state: this.$store.state,
    };
  },
  mounted() {
    // 2 秒後計數加 1,視圖會變化
    setTimeout(() => {
      this.$store.increaseCount();
    }, 2000);
  },
};
</script>
複製代碼

像這樣就完成了一個簡單的全局狀態管理,可是,這樣 state 中的數據不是響應式的,這裏是經過綁定到了 data 下的 state 中達到響應的目的,當咱們須要用到共享的數據是實時響應且能引起視圖更新的時候該如何作呢?

4. 如何使用 Vuex?

既然知道了本身要使用 Vuex,那麼如何正確地使用也是一門學問。

4.1 Vuex 核心概念簡介

Vuex的核心無外 StateGetterMutationActionModule 五個,下面一個個來介紹他們的做用和編寫方式。

State

Vuex的惟一數據源,咱們想使用的數據都定義在此處,惟一數據源確保咱們的數據按照咱們想要的方式去變更,能夠經過 store.state 來取得內部數據。

Getter

store 的計算屬性,當咱們須要對 state 的數據進行一些處理的時候,能夠先在 getters 裏進行操做,處理完的數據能夠經過 store.getters 來獲取。

Mutation

Vuex讓咱們放心使用的地方也就是在這,store 裏的數據只容許經過 mutation 來修改,避免咱們在使用過程當中覆蓋 state 形成數據丟失。

若是咱們直接經過 store.state 來修改數據,vue 會拋出警告,並沒有法觸發 mutation 的修改。

image-20190911172051077

每個 mutation 都有一個 type 和 callback 與之對應,能夠經過 store.commit(type[, payload]) 來提交一個 mutation 到 store,修改咱們的數據。

ps: payload 爲額外參數,能夠用來修改 state。

Action

在 Mutation 中並不容許使用異步操做,當咱們有異步操做(例如 http 請求)時,就必須在 Action 中來完成了。

Action 中提交的是 mutation,不是直接變動數據,因此也是容許的操做。

咱們能夠經過 store.dispatch(action) 來觸發事件,進行相關操做後,經過 commit 方法來提交 mutation。

Module

Vuex 的管理已經很美好了,可是!全局數據還好,反正不少地方會要用到,若是隻是某個單頁下的兄弟組件共享某些數據呢,那這樣大張旗鼓地放到Vuex中,長此以往便會臃腫沒法管理,故 Module 就是解決這個問題。

每一個 Module 就是一個小型的 store,與上面幾個概念一模一樣,惟一不一樣的是 Module 支持命名空間來更好的管理模塊化後的 store。

強烈建議每個模塊都寫上。

namespaced: true

設置這個屬性後,咱們就須要經過 store.state.moduleName 獲取 Module 下的 state 裏的數據,而 commit 和 dispatch 須要在傳遞 type 的時候加入路徑(也就是模塊名)store.commit('moduleName/type', payload)。

4.2 入門版食用方式

咱們如下所說都是以 vue-cli 腳手架生成的項目爲例來介紹幾種常規寫法。

當咱們的須要共享的數據量很小時,只須要簡單的寫在 store.js 文件裏就能夠了,並且不須要使用到 Module,使用方式也比較簡單。

store.js

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);
const vm = new Vue();

export default new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    setCount(state, count) {
      state.count = count;
    },
  },
  getters: {
    getCountPlusOne(state) {
      return state.count + 1;
    },
  },
  actions: {
    setCountAsync({ commit }, count) {
      return new Promise(async resolve => {
        const { data: count } = await vm.$http('/api/example', { count });
        commit('setCount', count);
        resolve(count);
      });
    },
  },
});
複製代碼

main.js

// main.js
import Vue from 'vue';
import App from './App';
import store from './store';

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  template: '<App />',
  components: { App },
});
複製代碼

那麼咱們在組件中須要用到時就能夠經過如下操做得到想要的結果了:

  • this.$store.state.count:實時獲取 count 的值
  • this.$store.getters.getCountPlusOne:獲取 count + 1 的值並緩存
  • this.$store.getters.getCountPlusOne():每次都會計算一個新的結果
  • this.$store.commit('setCount', 5):實時改變 count 的值
  • this.$store.dispatch('setCountAsync', 3):派發事件,異步更新 count 的值

在 commit 和 dispatch 的時候,若是須要傳入多個參數,可使用對象的方式。

e.g.

this.$store.commit('setCount', { 
  count: 5,
  other: 3 
}); 
// 或者以下
thit.$store.commit({ 
  type: 'setCount',
  count: 5,
  other: 3,
});
複製代碼

4.3 進階版食用方式

通常來講,入門版食用方式真的不太推薦,由於項目寫着寫着就和你的身材同樣,一每天不忍直視。故須要進階版來優化優化咱們的「數據倉庫」了。

總的來講,核心概念仍是那樣,只是咱們將 store 按照核心概念進行拆分,並將一些常數固定起來,避免拼寫錯誤這種弱智出現,也方便哪天須要修改。

整個項目結構以下:

store/

├── actions.js

├── getters.js

├── index.js

├── mutation-types.js

├── mutations.js

└── state.js

咱們依舊以上面的例子爲例來改寫:

mutation-types.js

這個文件也是強烈建議編寫的,將 mutation 的方法名以常量的方式定義在此處,在其餘地方經過 import * as types from './mutation-types'; 來使用,第一能夠避免手抖拼寫錯誤,第二能夠方便哪天須要改動變量名,改動一處便可。

export const SET_COUNT = 'SET_COUNT';
複製代碼

state.js

export default {
  count: 0,
}
複製代碼

getters.js

getCountPlusOne(state) {
  return state.count + 1;
};
複製代碼

actions.js

import Vue from 'vue';
import * as types from './mutation-types';

const vm = new Vue();

export const setCountAsync = ({ commit }, count) => {
  return new Promise(async resolve => {
    const { data: count } = await vm.$http('/api/example', { count });
    commit('setCount', count);
    resolve(count);
  });
};
複製代碼

mutations.js

import * as types from './mutation-types';

export default {
  [types.SET_COUNT](state, count) {
    state.count = count;
  },
};
複製代碼

index.js

import Vue from 'vue';
import Vuex from 'vuex';
import createLogger from 'vuex/dist/logger';
import * as actions from './actions';
import * as getters from './getters';
import state from './state';
import mutations from './mutations';
import * as types from './mutation-types';

Vue.use(Vuex);

const debug = process.env.NODE_ENV !== 'production';

const logger = createLogger(); // 引入日誌,幫助咱們更好地追蹤 mutaion 觸發的 state 變化

export default new Vuex.Store({
  actions,
  getters,
  state,
  mutations,
  strict: debug,
  plugins: debug ? [logger] : [],
});
複製代碼

在項目中咱們能夠經過 this.$store 來獲取數據或者提交 mutation 等。同時也可使用輔助函數來幫助咱們更加便捷的操做 store,這部份內容放到[高級版食用方式](#4.4 高級版食用方式)裏介紹。

至此,咱們編寫了一個完成進階版的食用方式,大多數項目經過這樣的結構來管理 store 也不會讓一個 store.js 文件成百上千行了。

可是,又是可是。我有些數據僅僅在小範圍內使用,寫在這個裏面,體量一多不仍是會找不到北嗎?

那請繼續看下面的高級版食用方式。

4.4 高級版食用方式

所謂的高級版食用方式也就是在進階版的基礎上將大大小小的數據分解開來,讓他們找到本身的歸宿。

關鍵的概念就是 Module了,經過這個屬性,咱們能夠將數據倉庫拆解成一個個小的數據倉庫來管理,從而解決數據冗雜問題。

這裏的寫法有兩種,一種是將 module 寫到 store 中進行統一管理;另外一種則是寫在模塊處,我我的喜歡第二種,就近原則。

// store/
// + └── modules.js 

// src/helloWorld/store.js
export const types = {
  SET_COUNT: 'SET_COUNT',
};

export const namespace = 'helloWorld';

export default {
  name: namespace, // 這個屬性不屬於 Vuex,可是經過常量定義成模塊名,避免文件間耦合字符串
  namespaced: true, // 開啓命名空間,方便判斷數據屬於哪一個模塊
  state: {
    count: 0,
  },
  mutations: {
    [types.SET_COUNT](state, count) {
      state.count = count;
    },
  },
  getters: {
    getCountPlusOne(state) {
      return state.count + 1;
    },
  },
};


// store/modules.js
import helloWorld from '@/views/helloWorld/store';
export default {
  [helloWorld.name]: helloWorld,
}

// store/index.js
+ import modules from './modules';
export default new Vuex.Store({
+  modules,
});
複製代碼

因爲 module 裏的數據很少,因此咱們寫在一個 store 文件內更爲方便,固然了,若是你的數據夠多,這裏繼續拆分也是能夠的。 高級版食用方式通常這樣也差很少了,下面補充幾個注意點。

module store 的操做方式

與全局模式差很少,只多了在獲取使用到命名空間。

  • 提交 Mutation:this.$store.commit('helloWorld/SET_COUNT', 1);
  • 提交 Action:this.$store.dispatch('helloWorld/SET_COUNT', 2);
  • 獲取 Gtters:this.$store.getters['helloWorld/getCountPlusOne'];
  • 獲取 State:this.$store.state.helloWorld.count;

在 module 內獲取總倉庫的狀態

// store.js
// ...
export default {
  // ...
  getters: {
    /** * state、getters:當前模塊的 state、getters * rootState、rootGetters:總倉庫的狀態 */
    getTotalCount(state, getters, rootState, rootGetters) {
      return state.count + rootState.count;
    },
  },
  actions: {
    // 同上註釋
    setTotalCount({ dispatch, commit, getters, state, rootGetters, rootState}) {
      const totalCount = state.count + rootState.count;
      commit('SET_COUNT', totalCount);
    },
  },
}
複製代碼

這樣一來,咱們一個健壯的Vuex共享數據倉庫就建造完畢了,下面會介紹一些便捷的操做方法,也就是Vuex提供的輔助函數。

在子 module 內派發總倉庫的事件

有時候咱們須要在子模塊內派發一些全局的事件,那麼能夠經過分發 action 或者提交 mutation 的時候,將 { root: true } 做爲第三個參數傳遞便可。

// src/helloWorld/store.js
export default {
  // ...
  actions: {
    setCount({ commit, dispatch }) {
      commit('setCount', 1, { root: true });
      dispatch('setCountAsync', 2, { root: true });
    },
  },
};
複製代碼

4.5 輔助函數

當咱們須要頻繁使用 this.$stoe.xxx 時,就總是須要寫這麼長一串,並且 this.$store.commit('aaaaa/bbbbb/ccccc', params) 也很是的不優雅。Vuex 提供給了咱們一些輔助函數來讓咱們寫出更清晰明朗的代碼。

mapState

import { mapState } from 'vuex';

export default {
  // ...
  computed: {
    // 參數是數組
    ...mapState([
      'count', // 映射 this.count 爲 this.$store.state.count
    ]),
    // 取模塊內的數據
    ...mapState('helloWorld', {
      localCount: 'count', // 映射 this.localCount 爲 this.$store.state.helloWorld.count
    }),
    // 少見寫法
    ...mapState({
      count: (state, getters) => state.count,
    }),
  },
  created() {
    console.log(this.count); // 輸出 this.$store.state.count 的數據
  },
};
複製代碼

mapGetters

使用f方式與 mapState 如出一轍,畢竟只是對 state 作了一點點操做而已。

mapMutation

必須是同步函數,不然 mutation 沒法正確觸發回調函數。

import { mapMutations } from 'vuex';

export default {
  // ...
  methods: {
    ...mapMutaions([
      'setCount', // 映射 this.setCount() 映射爲 this.$store.commit('setCount');
    ]),
    // 帶命名空間
    ...mapMutations('helloWorld', {
      setCountLocal: 'setCount', // 映射 this.setCountlocal() 爲 this.$store.commit('helloWorld/setCount');
    }),
    // 少見寫法
    ...mapMutaions({
      setCount: (commit, args) => commit('setCount', args),
    }),
  },
};
複製代碼

mapAction

與 mapMutation 的使用方式如出一轍,畢竟只是對 mutation 作了一點點操做而已, 少見寫法裏 commit 換成了 dispatch。

4.6 其餘補充

動態模塊

模塊動態註冊功能使得其餘 Vue 插件能夠經過在 store 中附加新模塊的方式來使用 Vuex 管理狀態

試想有一個通知組件,屬於外掛組件,須要用到Vuex來管理數據,那麼咱們能夠這樣作:

  • 註冊:this.$store.registerModule(moduleName)
  • 銷燬:this.$store.unregisterModule(moduleName)

store.js:

export const namespace = 'notice';
export const store = {
  namespaced: true,
  state: {
    count: 1,
  },
  mutations: {
    setCount(state, count) {
      state.count += count;
    },
  },
};
複製代碼

Notice.vue:

<template>
  <div>
    {{ count }}
    <button @click="handleBtnClick">add count</button>
  </div>
</template>

<script>
import { store, namespace } from './store';

export default {
  name: 'Notice',
  computed: {
    count() {
      return this.$store.state[namespace].count;
    },
  },
  beforeCreate() {
    // 註冊 notice 模塊
    this.$store.registerModule(namespace, store);
  },
  methods: {
    handleBtnClick() {
      this.$store.commit(`${namespace}/setCount`, 1);
    },
  },
//  beforeDestroy() {
//    // 銷燬 notice 模塊
//    this.$store.unregisterModule(namespace);
//  },
}  
</script>
複製代碼

模塊重用

有時候須要建立一個模塊的多個實例,那 state 可能會形成混亂。咱們能夠相似 vue 中 data 的作法,將 state 寫成函數返回對象的方式:

export default {
// old state
// state: {
// count: 0,
// },
  
// new state
  state() {
    return {
      count: 0,
    };
  },
};
複製代碼

createNamespacedHelpers

在模塊有命名空間的時候,咱們在使用數據或者派發事件的時候須要在常量前加上命名空間的值,有些時候寫起來也不是很舒服,Vuex提供了一個輔助方法createNamespacedHelpers,能幫助咱們直接生成帶命名空間的輔助函數。

// old
import { mapState } from 'vuex';

export default {
  // ...
  computed: {
    ...mapState('helloWorld', [
      'count',
    ]);
  },
};


// use createNamespacedHelpers function
import { createNamespacedHelpers } from 'vuex';
const { mapState } = createNamespacedHelpers('helloWorld');

export default {
  // ...
  computed: {
    ...mapState([
      'count',
    ]);
  },
};
複製代碼

插件

在上面咱們已經使用到了一個vuex/dist/logger插件,他能夠幫助咱們追蹤到 mutaion 的每一次變化,而且在控制檯打出,相似下圖。

image-20190916154825437

能夠清晰地看到變化前和變化後的數據,進行對比。

插件還會暴露 mutaion 鉤子,能夠在插件內提交 mutaion 來修改數據。

更多神奇的操做能夠參考官網慢慢研究,這裏不是重點不作更多介紹(實際上是我想象不到要怎麼用)。

嚴格模式

const store = new Vuex.Store({
  // ...
  strict: true
})
複製代碼

當開啓嚴格模式,只要 state 變化不禁 mutaion 觸發,則會拋出錯誤,方便追蹤。

生產環境請關閉,避免性能損失。

能夠經過構建工具來幫助咱們process.env.NODE_ENV !== 'production'

表單處理

當在表單中經過v-model使用Vuex數據時,會有一些意外狀況發生,由於用戶的修改並非由 mutaion 觸發,因此解決的問題是:使用帶有setter的雙向綁定計算屬性。

// template
<input v-model="message">
  
// script
export default {
  // ...
  computed: {
    message: {
      get () {
        return this.$store.state.obj.message
      },
      set (value) {
        this.$store.commit('updateMessage', value)
      },
    },
  },
};
複製代碼

總結

經過上面的一些例子,咱們知道了如何來正確又優雅地管理咱們的數據,如何快樂地編寫Vuex。回到開頭,若是你尚未理解那張圖的話,不妨再把這個過程多看一下,而後再看看Vuex 從使用到原理分析(中篇)更深刻地瞭解Vuex

相關文章
相關標籤/搜索