請啟用 Javascript 查看內容

Vuex - 核心概念

 ·  ☕ 6 分鐘

竹白的 Vue 記事本 目錄

核心概念

一般元件上,我們會用 data 儲存狀態資料、用 method 操作狀態資料,還會用 computed 衍生狀態資料。

而在 Vuex 則是透過以下選項來管理狀態資料:

  1. State:儲存狀態資料,類似全域的 data
  2. Mutations:更新態的方法,只能同步操作。
  3. Actions:進行非同步狀態更新操作,類似全域的 method
  4. Getters:衍生狀態資料,類似全域的 computed

State

State 就是用來存放共享狀態資料的倉庫。

定義 state

1
2
3
4
5
// store.js

state: {
  count: 0,
}

data 一樣需要遵守 Vue 的響應規則。

1. 訪問狀態

在元件內,我們可以透過 $store(Store 實體)訪問到這個 state 物件。

1
$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 輔助函式可以傳入物件或陣列。如果傳入一個物件有三種寫法:

  1. 箭頭函式,可以使程式碼更簡潔。
  2. 字串,等同使用箭頭函式。
  3. 如果要使用 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. 響應規則

之前提到,storedata 一樣需要遵守 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.statecontext.getters 來獲取 stategetters 物件。

可以使用 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']) },

竹白
作者
竹白
前端筆記

文章目錄