vue 快速入門 系列 —— Vuex 基礎

彭加李發表於2021-10-18

其他章節請看:

vue 快速入門 系列

Vuex 基礎

Vuex 是 Vue.js 官方的狀態管理器

vue 的基礎應用(上)一文中,我們已知道父子之間通訊可以使用 props$emit,而非父子元件通訊(兄弟、跨級元件、沒有關係的元件)使用 bus(中央事件匯流排)來起到通訊的作用。而 Vuex 作為 vue 的一個外掛,解決的問題與 bus 類似。bus 只是一個簡單的元件,功能也相對簡單,而 Vuex 更強大,使用起來也複雜一些。

現在的感覺就是 Vuex 是一個比 bus 更厲害的東西,可以解決元件之間的通訊。更具體些,就是 vuex 能解決多個元件共享狀態的需求:

  • 多個檢視(元件)依賴於同一狀態
  • 來自不同檢視(元件)的行為需要變更同一狀態。

Vuex 把元件的共享狀態抽取出來,以一個全域性單例模式管理。在這種模式下,我們的元件樹構成了一個巨大的“檢視”,不管在樹的哪個位置,任何元件都能獲取狀態或者觸發行為。

環境準備

通過 vue-cli 建立專案

// 專案預設 `[Vue 2] less`, `babel`, `router`, `vuex`, `eslint`
$ vue create test-vuex

Tip:環境與Vue-Router 基礎相同

核心概念

Vuex 的核心概念有 State、Getters、Mutations、Actions和Modules。

我們先看一下專案 test-vuex 中的 Vuex 程式碼:

// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // vuex 中的資料
  state: {
  },
  // 更改 vuex 中 state(資料)的唯一方式
  mutations: {
  },
  // 類似 mutation,但不能直接修改 state
  actions: {
  },
  // Vuex 允許將 store 分割成模組(module),每個模組可以擁有自己的 state、mutation、action、getter
  modules: {
  }
})

Getters,可以認為是 store 的計算屬性

State

state 是 Vuex 中的資料,類似 vue 中的 data。

需求:在 state 中定義一個屬性 isLogin,從 About.vue 中讀取該屬性。

直接上程式碼:

// store/index.js
export default new Vuex.Store({
  state: {
    isLogin: true
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ this.$store.state.isLogin }}</p>
  </div>
</template>

頁面輸出 true

Vuex 通過 store 選項,提供了一種機制將狀態從根元件“注入”到每一個子元件中(需呼叫 Vue.use(Vuex)),子元件能通過 this.$store 訪問,這樣就無需在每個使用 state 的元件中頻繁的匯入。

// main.js
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')
// store/index.js
Vue.use(Vuex)

Tip:Vuex 的狀態儲存是響應式。

mapState 輔助函式

從 store 例項中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態。

當一個元件需要獲取多個狀態的時候,將這些狀態都宣告為計算屬性會有些重複和冗餘。為了解決這個問題,我們可以使用 mapState 輔助函式幫助我們生成計算屬性,讓你少按幾次鍵。

// views/About.vue
<template>
  <div class="about">
    <p>{{ isLogin }}</p>
  </div>
</template>
<script>
// 在單獨構建的版本中輔助函式為 Vuex.mapState
import { mapState } from 'vuex'

export default {
  computed: mapState([
    // 對映 this.isLogin 為 store.state.isLogin
    'isLogin'
  ])
}
</script>

頁面同樣輸出 true。

Tip:更多特性請看官網

Getters

Getters,可以認為是 store 的計算屬性。

getter 的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。

需求:從 isLogin 派生出一個變數,從 About.vue 中讀取該屬性

直接上程式碼:

// store/index.js
export default new Vuex.Store({
  state: {
    isLogin: true
  },
  getters: {
    translationIsLogin: state => {
      return state.isLogin ? '已登入' : '未登入'
    }
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ this.$store.getters.translationIsLogin }}</p>
  </div>
</template>

頁面輸出“已登入”

Tip:更多特性請參考官網。

  • 可以給 getter 傳參
  • 有與 state 類似的輔助函式,這裡是 mapGetters

Mutations

mutation 是更改 vuex 中 state(資料)的唯一方式。

mutation 類似事件,每個 mutation 都有一個字串的事件型別和 一個回撥函式。不能直接呼叫一個 mutation handler,只能通過 store.commit 方法呼叫。

需求:定義一個 mutation(更改 isLogin 狀態),在 About.vue 中過三秒觸發這個 mutation。

直接上程式碼:

// store/index.js
export default new Vuex.Store({
  state: {
    isLogin: true
  },
  mutations: {
    toggerIsLogin(state) {
      state.isLogin = !state.isLogin
    }
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ isLogin }}</p>
  </div>
</template>
<script>
export default {
  created() {
    setInterval(()=>{
      this.$store.commit('toggerIsLogin')
    }, 3000)
  },
}
</script>

頁面每三秒會依次顯示 true -> false -> true ...

Mutation 必須是同步函式
  • 筆者在 mutation 中寫非同步函式(使用 setTimeout)測試,沒有報錯
  • 在 mutation 中混合非同步呼叫會導致程式很難除錯(使用 devtools)
  • 當呼叫了兩個包含非同步回撥的 mutation 來改變狀態,不知道什麼時候回撥和哪個先回撥

結論:在 mutation 中只使用同步函式,非同步操作放在 action 中。

Tip:更多特性請參考官網。

  • 可以給 mutation 傳參
  • 觸發(commit)方式可以使用物件
  • 有與 state 類似的輔助函式,這裡是 mapMutations

Actions

Action 類似於 mutation,不同在於:

  • Action 提交的是 mutation,而不是直接變更狀態。
  • Action 可以包含任意非同步操作。

需求:定義一個 action,裡面有個非同步操作,過三秒更改 isLogin 狀態。

直接上程式碼:

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    isLogin: true
  },
  mutations: {
    toggerIsLogin(state) {
      state.isLogin = !state.isLogin
    }
  },
  actions: {
    toggerIsLogin(context) {
      setInterval(() => {
        context.commit('toggerIsLogin')
      }, 3000)
    }
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ isLogin }}</p>
  </div>
</template>
<script>
export default {
  created() {
    // 通過 dispatch 分發
    this.$store.dispatch('toggerIsLogin')
  },
}
</script>

過三秒,頁面的 true 變成 false。

實踐中,我們會經常用到 ES2015 的引數解構來簡化程式碼:

actions: {
    toggerIsLogin({ commit }) {
      setInterval(() => {
        commit('toggerIsLogin')
      }, 3000)
    }
  },

Tip:更多特性請參考官網。

  • 可以給 Actions 傳參
  • 觸發(dispatch)方式可以使用物件
  • 有與 state 類似的輔助函式,這裡是 mapActions
  • 組合多個 Action

Modules

目前我們的 store 都寫在一個檔案中,當應用變得複雜時,store 物件就有可能變得相當臃腫。

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

Vuex 允許將 store 分割成模組(module),每個模組可以擁有自己的 state、mutation、action、getter。

需求:定義兩個模組,每個模組定義一個狀態,在 About.vue 中顯示這兩個狀態

直接上程式碼:

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

Vue.use(Vuex)

const moduleA = {
  state: () => ({ name: 'apple' }),
}

const moduleB = {
  state: () => ({ name: 'orange' }),
}

export default new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB,
  }
})
// views/About.vue
<template>
  <div class="about">
    <!-- 即使給這兩個模組都加上名稱空間,這樣寫也是沒問題的 -->
    <p>{{ this.$store.state.a.name }} {{ this.$store.state.b.name }}</p>
  </div>
</template>

頁面顯示 “apple orange”。

模組的區域性狀態

對於模組內部的 mutation 和 getter,接收的第一個引數是模組的區域性狀態物件。就像這樣:

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 這裡的 `state` 物件是模組的區域性狀態
      state.count++
    }
  },
}

對於模組內部的 action,區域性狀態通過 context.state 暴露出來,根節點狀態則為 context.rootState。就像這樣:

const moduleA = {
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}
名稱空間

預設情況下,模組內部的 action、mutation 和 getter 是註冊在全域性名稱空間的。

如果希望模組具有更高的封裝度和複用性,可以通過新增 namespaced: true 的方式使其成為帶名稱空間的模組。請看示意程式碼:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模組內容(module assets)
      state: () => ({ ... }), // 模組內的狀態已經是巢狀的了,使用 `namespaced` 屬性不會對其產生影響
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 巢狀模組
      modules: {
        // 繼承父模組的名稱空間
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 進一步巢狀名稱空間
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

小練習

請問 About.vue 會輸出什麼?(答案在文章底部)

// views/About.vue
<template>
  <div class="about">
    <p>{{ this.$store.state.a.name }} {{ this.$store.state.b.name }}</p>
    <p>
      {{ this.$store.getters.nameA }} {{ this.$store.getters.nameB }}
      {{ this.$store.getters["b/nameB"] }}
    </p>
  </div>
</template>
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const moduleA = {
  namespaced: true,
  state: () => ({ name: 'apple' }),
}

const moduleB = {
  namespaced: true,
  state: () => ({ name: 'orange' }),
  getters: {
    nameB: state => `[${state.name}]`
  }
}

export default new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB,
  },
  getters: {
    nameA: state => state.a.name,
    nameB: state => state.b.name
  }
})

Tip: 更多特性請參考官網。

專案結構

Vuex 並不限制你的程式碼結構。但是,它規定了一些需要遵守的規則:

  1. 應用層級的狀態應該集中到單個 store 物件中。
  2. 提交 mutation 是更改狀態的唯一方法,並且這個過程是同步的。
  3. 非同步邏輯都應該封裝到 action 裡面。

只要你遵守以上規則,如何組織程式碼隨你便。如果你的 store 檔案太大,只需將 action、mutation 和 getter 分割到單獨的檔案。

對於大型應用,官網給出了一個專案結構示例:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API請求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我們組裝模組並匯出 store 的地方
    ├── actions.js        # 根級別的 action
    ├── mutations.js      # 根級別的 mutation
    └── modules
        ├── cart.js       # 購物車模組
        └── products.js   # 產品模組

Tip:在筆者即將完成的另一篇文章“使用 vue-cli 3 搭建一個專案”中會有更詳細的介紹

附錄

小練習答案

apple orange

apple orange [orange]

其他章節請看:

vue 快速入門 系列

相關文章