vue 快速入門 系列 —— Vuex 基礎
其他章節請看:
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 並不限制你的代碼結構。但是,它規定了一些需要遵守的規則:
- 應用層級的狀態應該集中到單個 store 對象中。
- 提交 mutation 是更改狀態的唯一方法,並且這個過程是同步的。
- 異步邏輯都應該封裝到 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]
其他章節請看: