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

其他章節請看:

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 快速入門 系列

Tags: