竹白的 Vue 記事本 目錄
  
  
核心概念
一般元件上,我們會用 data 儲存狀態資料、用 method 操作狀態資料,還會用 computed 衍生狀態資料。
而在 Vuex 則是透過以下選項來管理狀態資料:
- State:儲存狀態資料,類似全域的 data。
- Mutations:更新態的方法,只能同步操作。
- Actions:進行非同步狀態更新操作,類似全域的 method。
- Getters:衍生狀態資料,類似全域的 computed。

State
State 就是用來存放共享狀態資料的倉庫。
定義 state :
| 1
2
3
4
5
 | // store.js
state: {
  count: 0,
}
 | 
 
與 data 一樣需要遵守 Vue 的響應規則。
1. 訪問狀態
在元件內,我們可以透過 $store(Store 實體)訪問到這個 state 物件。
通常會使用計算屬性回傳狀態:
| 1
2
3
4
5
6
7
 | // Component.vue
computed: {
  count() {
    return this.$store.state.count;
  },
}
 | 
 
CodePen Demo:Vue 2.x - Vuex State
  注意,
state 物件,只能透過 Mutations 更新。
2. 多個狀態
當一個元件需要獲取多個狀態時候,將這些狀態都宣告為計算屬性會有些重複和冗餘。因此我們可以使用 mapState 輔助函式 ,取得多個 state 裡面的狀態資料。
假設 state 物件內有多筆狀態資料:
| 1
2
3
4
5
6
 | // store.js
state: {
  count: 0,
  total: 0,
}
 | 
 
首先我們必須在元件中引入 mapState 輔助函式:
| 1
2
3
 | // Component.vue
import { mapState } from 'vuex';
 | 
 
mapState 輔助函式可以傳入物件或陣列。如果傳入一個物件有三種寫法:
- 箭頭函式,可以使程式碼更簡潔。
- 字串,等同使用箭頭函式。
- 如果要使用 this獲取區域狀態,需要改用一般的函式。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | // Component.vue
computed: mapState({
  // 箭頭函式
  count: state => state.count,
  // 字串
  total: 'total',
  // 一般的函式
  countPlusLocalState(state) {
    return state.count + this.localCount;
  },
})
 | 
 
當映射的計算屬性的名稱與 state 名稱相同時,我們也可以給 mapState 輔助函式傳一個字串陣列。
| 1
2
3
 | // Component.vue
computed: mapState(['count', 'total']),
 | 
 
3. 區域狀態
使用 Vuex 並不意味著你需要將所有的狀態放入 Vuex。如果有些狀態嚴格屬於單個元件,最好還是作為元件的區域狀態。
另外,如果我們有區域計算屬性要用,可以使用 ... 展開運算子將 mapState 輔助函式合併:
| 1
2
3
4
5
6
7
8
9
 | // Component.vue
computed: {
  localComputed () {
    // ...
  },
  // 展開回傳的物件,將它混入到 computed 物件中
  ...mapState(['count', 'total'])
}
 | 
 
CodePen Demo:Vue 2.x - Vuex mapState 
Mutations
前面有提到,state 狀態不能直接改變。
舉例來說:
| 1
 | <button @click="$store.state.count += 1">++</button>
 | 
 
雖然這樣不會拋出錯誤,也不會跳出警告,但這是錯誤的非法操作。
  如果想要狀態非 
mutation 函式引起的更新拋出錯誤,可以使用 
嚴格模式。
我們只能透過 Vuex 中的 mutation 來更新 state。雖然這種操作較繁瑣,但可以集中監控狀態的變化,避免不可預期情況發生。
定義 mutation:
| 1
2
3
4
5
6
7
 | // store.js
mutations: {
  increment(state) {
    state.count += 1; // 變更狀態
  }
}
 | 
 
1. 提交 Mutation
mutation 類似事件監聽器,無法直接呼叫,我們必須透過 store.commit 方法來呼叫。
| 1
2
3
4
5
6
7
 | // Component.vue
methods: {
  increment() {
    this.$store.commit('increment');
  },
}
 | 
 
CodePen Demo:Vue 2.x -Vuex Mutations
2. Payload
可以向 store.commit 傳入額外的參數,即 mutation 的第二個參數 payload:
| 1
2
3
4
5
6
7
 | // store.js
mutations: {
  increment(state, n) {
    state.count += n
  }
}
 | 
 
| 1
 | $store.commit('increment', 10);
 | 
 
在大多數情況下,payload 應該是一個物件,這樣可以包含多個字段並且記錄的 mutation 會更易讀:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 | // store
mutations: {
  increment(state, payload) {
    state.count += payload.amount;
  }
}
// 或是使用解構賦值
mutations: {
  increment(state, { amount }) {
    state.count += amount;
  }
}
 | 
 
| 1
 | $store.commit('increment', {amount: 10});
 | 
 
CodePen Demo:Vue 2.x -Vuex Mutations Payload
3. 物件風格的提交方式
提交 mutation 的另一種方式是直接使用包含 type 屬性的物件:
| 1
2
3
4
 | $store.commit({
  type: 'increment',
  amount: 10
});
 | 
 
4. mapMutations
mutation 一樣有 mapMutations 輔助函式可以使用:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 | // Component.vue
import { mapMutations } from 'vuex';
// 物件
methods: {
  ...mapMutations({
    increment: 'increment',
  }),
}
// 陣列
methods: {
  ...mapMutations(['increment']),
}
 | 
 
mapMutations 輔助函式也支持 payload,只需要在呼叫的地方傳入參數,會自動傳遞:
| 1
 | <button @click="decrement(10)">++</button>
 | 
 
CodePen Demo:Vue 2.x -Vuex mapMutations
5. 使用常數替代 Mutation 類型
當 Vuex 管理的狀態越多,我們就會需要用到大量的 mutation 去更新,你不可能記住全部,因此可能會反覆查看。
為了解決上述問題,使用常數替代 mutation 類型事最常見的方案之一,可以使 mutation 更容易識別。
| 1
2
3
 | // mutation-types.js
export const EDIT_TASK = 'EDIT_TASK';
...
 | 
 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 | // store.js
import Vuex from 'vuex';
import { EDIT_TASK } from './mutation-types';
export default new Vuex.Store({
  state: {
    task: '';
  },
  mutations: {
    [EDIT_TASK](state, { value }) {
      state.task = value;
    },
  },
});
 | 
 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
 | // Component.vue
import { EDIT_TASK } from '../mutation-types';
// ...
methods: {
  [EDIT_TASK]() {
    this.$store.commit(EDIT_TASK, {});
  },
}
 | 
 
若要一次性導入,可以改成:
| 1
2
3
4
 | import * as types from './mutation-types';
// mutations ...
[types.EDIT_TASK](state) {}
 | 
 
6. 響應規則
之前提到,store 與 data 一樣需要遵守 Vue 的響應規則。
因此物件新增屬性時,需要使用 Vue.set() 來處理:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | // store.js
import Vue from 'vue';
// ...
state: {
  obj: {};
},
mutations: {
  ADD_OBJ(state, { prop, value }) {
    Vue.set(state.obj, prop, value);
  }
}
 | 
 
或者運用展開運算子替換舊物件:
| 1
2
3
4
5
 | mutations: {
  ADD_OBJ(state, { value }) {
    state.obj = { ...state.obj, newProp: 'value' };
  }
}
 | 
 
陣列修改也一樣,也需要使用 Vue.set() 來處理:
| 1
2
3
4
5
6
7
8
 | state: {
  list: ['a', 'b', 'c', 'd'],
},
mutations: {
  EDIT_LIST(state, { index, value }) {
    Vue.set(state.list, index, value);
  }
}
 | 
 
詳細可以參考 深入響應式原理。
7. 非同步處理
當我們使用 mutation 更新狀態時,必須是同步處理,否則 devtool 工具將無法追蹤到狀態的改變。
Actions
如果我們要非同步處理狀態資料,就無法直接使用 mutation,而必須透過 action 間接的提交 mutation 來變更狀態資料。
定義一個簡單的非同步處理操作的 action:
| 1
2
3
4
5
6
7
8
9
 | // store.js
actions: {
  incrementAsync(context) {
    setTimeout(() => {
      context.commit('increment');
    }, 1000);
  }
}
 | 
 
actions 函式接受一個與 store 實體具有相同方法和屬性的 context 物件,包含以下屬性:
| 1
2
3
4
5
6
7
8
 | {
  state,      // 等同於 store.state,若在模組中則為區域狀態
  rootState,  // 等同於 store.state,只存在於模組中
  commit,     // 等同於 store.commit
  dispatch,   // 等同於 store.dispatch
  getters,    // 等同於 store.getters
  rootGetters // 等同於 store.getters,只存在於模組中
}
 | 
 
因此我們可以用來呼叫 context.commit 提交一個 mutation,或者透過 context.state 和 context.getters 來獲取 state 和 getters 物件。
可以使用 ES6 解構賦值來簡化程式碼:
| 1
2
3
4
5
6
7
8
9
 | // store.js
actions: {
  incrementAsync({ commit }) {
    setTimeout(() => {
      commit('increment');
    }, 1000);
  },
}
 | 
 
1. 調度 Action
action 是透過 store.dispatch 方法來呼叫:
| 1
2
3
4
5
6
7
 | // Component.vue
methods: {
  incrementAsync() {
    this.$store.dispatch('incrementAsync');
  },
}
 | 
 
CodePen Demo:Vue 2.x -Vuex Actions
action 同樣有 payload 參數和物件風格,也有 mapActions 輔助函式可以使用,用法與 mutation 差不多,這裡就不多加說明。
2. 組合 Action
因為 action 通常用來處理非同步操作,所以我們可以回傳 Promise,用來處理更佳複雜的非同步流程。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | // store.js
actions: {
  actionA({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation');
        resolve();
      }, 1000);
    });
  },
}
 | 
 
Getters
你可以將 Getters 看成全域的計算屬性。
有時候我們需要從 State 中衍生一些狀態,例如對陣列過濾,假設有很多元件都會用到,那麼我們就定義 getter,它與計算屬性一樣會緩存結果,只有當它的依賴值發生了改變才會被重新計算。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
 | // store.js
state: {
  numbers: [1, 2, 3, 4, 5, 6, 7, 8],
},
getters: {
  even(state) {
    return state.numbers.filter((n) => n % 2 === 0);
  },
}
 | 
 
getter 函式的第一個參數為 state 物件,若希望傳入其他 getter,可以使用第二個參數 getters 物件:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
 | // store.js
getters: {
  even(state) {
    // ...
  },
  evenCount(state, getters) {
    return getters.even.length;
  },
}
 | 
 
1. 訪問
要在元件中訪問 getter 一樣可以透過 $stroe 實體:
| 1
2
3
4
5
6
7
 | // Component.vue
computed: {
  even() {
    return this.$store.getters.even;
  }
}
 | 
 
CodePen Demo:Vuex Getters
或者使用 mapGetters 輔助函數:
| 1
2
3
4
5
6
 | // Component.vue
import { mapGetters } from 'vuex';
//...
computed: {...mapGetters(['even']) },
 |