其他章節請看:
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]
其他章節請看: