Vuex
當我們的應用遇到多個組件共享狀態時,單向數據流的簡潔性很容易被破壞:
- 多個視圖依賴於同一狀態。
- 來自不同視圖的行爲需要變更同一狀態。
對於問題一,傳參的方法對於多層嵌套的組件將會非常繁瑣,並且對於兄弟組件間的狀態傳遞無能爲力。對於問題二,我們經常會採用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。
什麼情況下我應該使用 Vuex?
Vuex 可以幫助我們管理共享狀態,並附帶了更多的概念和框架。這需要對短期和長期效益進行權衡。
如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗餘的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex。一個簡單的 store 模式簡單狀態管理起步使用)就足夠您所需了。但是,如果您需要構建一箇中大型單頁應用,您很可能會考慮如何更好地在組件外部管理狀態,Vuex 將會成爲自然而然的選擇。
Vuex是vue的狀態管理工具,爲了更方便的實現多個組件共享狀態。
(以上內容來自官網)
基本使用
安裝
npm install vuex --save
使用
- 引入vuex
import Vuex from 'vuex';
- 使用vuex
Vue.use(Vuex);
- 創建store實例
const store = new Vuex.Store({
state: {
count: 0
}
})
- 將store掛載上
new Vue({
store,
})
State
state
故名思意是狀態,其實就是vuex存放數據的地方,我們可以將多個組件需要共同的使用的變量放在state
中,state
是一個對象。
使用state
我們將變量放在state
中,那麼如何使用呢?
Vuex
通過store
選項,提供了一種機制將狀態從跟組件“注入”到每一個子組件中(調用Vue.use(Vuex)
)。通過在根實例中註冊store
選項,該store
實例會注入到根組件下的所有子組件中,且子組件能通過this.$store
訪問。像這樣:
<div>
喜歡人數: {{ $store.state.count }}
</div>
由於 Vuex 的狀態存儲是響應式的,從 store
實例中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態:
computed: {
count () {
return this.$store.state.count
}
}
mapState 輔助函數
當一個組件需要獲取多個狀態時,將這些狀態都聲明爲計算屬性會有些重複和冗餘。爲了解決這個問題,我們可以使用mapState
輔助函數幫助我們生成計算屬性:
import { mapState } from 'vuex';
computed: {
...mapState(['count']),
}
此時就有一個計算屬性count
,我們就可以直接使用它了,也可以爲它重新起一個別名:
computed: {
...mapState({
storeCount: state => state.count,
})
}
如果僅僅只是返回一個state中的一個數據,我們也可以簡寫:
computed: {
...mapState({
storeCount: 'count', // 等同於 state => state.count
})
}
Getter
有時候我們可以能需要從state
中派生一下狀態,比如說,我們需要對count
進行雙倍處理,我們就可以使用getter
,我們可以把getter
認爲是store
中的計算屬性。getter
的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變纔會被重新計算。
Getter
接收state
作爲其第一個參數,Getter
也可以接受其他 getter
作爲第二個參數
getters: {
countDouble (state) {
return state.count * 2;
}
}
通過屬性訪問
Getter
會暴露爲store.getters
對象:this.$store.getters.countDouble
<div>
喜歡人數的兩倍: {{ $store.getters.countDouble }}
</div>
通過方法訪問
也可以讓getter
返回一個函數,來實現給getter
傳參
getters: {
countAdd: state => num => state.count + num;
/*
countAdd:function(state){
return function(num){
return state.count + num
}
}
*/
}
this.$store.countAdd(3);
mapGetters 輔助函數
與mapState
相似,我們也可以通過mapGetters
將store
種的getters
映射到計算屬性種
import { mapsGetters } from 'vuex';
computed: {
...mapGetters([
'countDouble',
'countAdd',
])
}
如果你想將一個 getter
屬性另取一個名字,使用對象形式:
mapGetters({
storeCountDouble: 'countDouble'
})
Mutation
在嚴格模式下,更改 Vuex
的 store
中的狀態的唯一方法是提交 mutation
。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 變更狀態
state.count++
}
}
})
在組件中提交 Mutation
但是我們不能直接調用mutation,當我們想要使用時,我們可以條用store.commit
方法來觸發一個mutation handler
。像這樣:
this.$store.commit('increment');
mapMutations 輔助函數
當然我們也可以使用輔助函數,例如:
import { mapMutations } from 'vuex'
methods: {
...mapMutations([
'increment',
]),
...mapMutations({ //同樣的起一個別名
add: 'increment'
})
}
輔助函數會將this.increment()
或this.add()
映射爲this.$store.commit('increment')
提交載荷(Payload)
你可以向store.commit
傳入額外的參數,官方的學術名稱叫載荷(payload):
mutations: {
increment (state, num) {
state.count += num
}
}
this.$store.commit('increment', 10)
在大多數情況下,載荷應該是一個對象,這樣可以包含多個字段並且記錄的mutation
會更易讀:
mutations: {
increment (state, payload) {
state.count += payload.num
}
}
this.$store.commit('increment', {
num: 10
})
對象風格的提交方式
提交 mutation
的另一種方式是直接使用包含 type
屬性的對象:
this.$store.commit({
type: 'increment',
num: 10
})
當使用對象風格的提交方式,整個對象都作爲載荷傳給 mutation
函數,因此 handler
保持不變:
mutations: {
increment (state, payload) {
state.count += payload.num
}
}
使用常量替代 Mutation 事件類型
把這些常量放在單獨的文件中可以讓你的代碼合作者對整個 app
包含的 mutation
一目瞭然:
// mutation-types.js
export const COUNT_INCREMENT = 'COUNT_INCREMENT'
// store.js
import Vuex from 'vuex'
import { COUNT_INCREMENT } from './mutation-types'
const store = new Vuex.Store({
state: { count:0 },
mutations: {
[COUNT_INCREMENT] (state) {
state.count++
}
}
})
用不用常量取決於自己,在需要多人協作的大型項目中,這會很有幫助。我在開發大型項目的時候,通常都會用。
Mutation 需遵守 Vue 的響應規則
下面是官網的解釋:
既然 Vuex
的 store
中的狀態是響應式的,那麼當我們變更狀態時,監視狀態的 Vue
組件也會自動更新。這也意味着 Vuex
中的 mutation
也需要與使用 Vue
一樣遵守一些注意事項:
- 最好提前在你的
store
中初始化好所有所需屬性。 - 當需要在對象上添加新屬性時,你應該
-
使用
Vue.set(obj, 'newProp', 123)
, 或者 -
以新對象替換老對象。例如,利用對象展開運算符,我們可以這樣寫:
state.obj = { ...state.obj, newProp: 123 }
其實原因很簡單,當我們使用對象中未有的屬性時,vue
不會監聽,這和vue
的知識有關,解決的方法也就是以上兩種。
表單處理
在Vuex
的 state
上使用 v-model
時,由於會直接更改state
的值,所以Vue
會拋出錯誤。因爲vuex
是不允許直接修改state
的,如果想要使用雙向數據的功能,就需要自己模擬一個 v-model: :value="msg"
、@input="updateMsg"
雙向綁定的計算屬性
上面的做法,比v-model
本身繁瑣很多,所以我們還可以使用計算屬性的setter
來實現雙向綁定:
<input v-model="mycount">
computed: {
mycount: {
get () {
return this.$store.state.mycount;
},
set (value) {
this.$store.commit(COUNT_INCREMENT, { num: value });
}
}
}
Mutation 必須是同步函數
要記住 mutation 必須是同步函數。
mutations: {
increment (state) {
setTimeout(() => {
state.count ++;
}, 1000)
}
}
假如,我們正在 debug
一個 app
並且觀察 devtool
中的 mutation
日誌,執行上端代碼,我們會發現更改state的操作是在回調函數中執行的,當 mutation
觸發的時候,回調函數還沒有被調用,devtools
不知道什麼時候回調函數實際上被調用——實質上任何在回調函數中進行的狀態的改變都是不可追蹤的
嚴格模式
開啓嚴格模式,僅需在創建 store
的時候傳入 strict: true
:
const store = new Vuex.Store({
// ...
strict: true
})
在嚴格模式下,無論何時發生了狀態變更且不是由 mutation
函數引起的,將會拋出錯誤。這能保證所有的狀態變更都能被調試工具跟蹤到。
開發環境與發佈環境
不要在發佈環境下啓用嚴格模式!嚴格模式會深度監測狀態樹來檢測不合規的狀態變更,要確保在發佈環境下關閉嚴格模式,以避免性能損失。我們可以通過 webpack 配合使用:
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
Action
Action 類似於 mutation,不同在於:
- Action 提交的是 mutation,而不是直接變更狀態。
- Action 可以包含任意異步操作
Action 函數接收一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調用 context.commit
提交一個 mutation,或者通過 context.state
和 context.getters
來獲取 state 和 getters:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
console.log(context.state.count) //0
context.commit('increment')
}
}
})
分發Action
同樣的我們在組件重調用action,需要使用 $store.dispatch
:
this.$store.dispatch('increment')
雖然和mutation
差不多,但是在action
中,可以執行異步操作,但是mutation
中不行!!!
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
組合 Action
Action 通常是異步的,那麼如何知道 action 什麼時候結束呢?我們可以使用ES6中的Promise,在項目中,通常也是會配合Promise使用
actions: {
incrementAsync ({ commit }) {//直接使用結構賦值,拿到context中的commit方法
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment')
resolve()
}, 1000)
})
}
}
this.$store.dispatch('incrementAsync').then(() => {
// ...
console.log("提交完成!")
})
mapActions 輔助函數
你在組件中使用 this.$store.dispatch('xxx')
分發 action,或者使用 mapActions
輔助函數將組件的 methods 映射爲 store.dispatch
調用(需要先在根節點注入 store
):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment',
]),
...mapActions({
add: 'increment'
})
}
}
輔助函數會將 this.increment()
映射爲 this.$store.dispatch('increment')
,將 this.add()
映射爲 this.$store.dispatch('increment')
Module
由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常複雜時,store
對象就有可能變得相當臃腫。爲了解決以上問題,Vuex
允許我們將 store
分割成模塊(module
)。每個模塊擁有自己的 state
、mutation
、action
、getter
。像這樣:
//count.js
export const count = {
state: {
count:0
},
getters:{
countDouble(state){
return state.count * 2
}
},
mutations: {
increment(state,payload){
state.count+=payload.num
}
}
}
import {count} from './modules/count'
//...
const store = new Vuex.Store({
modules: {
count
}
})
默認情況下,模塊內部的 action、mutation 和 getter 是註冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。但對於模塊內部的 state 比較特殊,會將 module 模塊中的 state 放在一個對象中,對象名就是模塊的名字,在開發者工具中我們可以看到如下:
- 獲取 state:state,所以也無法直接通過mapState獲取state,需要加上模塊名,想這樣:
this.$store.state.count.count
//mapState方式
computed:{
...mapState({
count:state=>state.count.count
})
},
- 獲取 getter:
this.$store.getters.countDouble
//mapGetter方式
computed:{
...mapGetters({
countDouble:"countDouble"
})
}
- 提交 mutation:
this.$store.commit('increment',{num:2});
//mapMutation方式
methods:{
...mapMutations(['increment'])
}
- 分發 action:
this.$store.dispatch('xxx');
//mapActions方式
methods:{
...mapActions方式(['xxx'])
}
命名空間
我們上面也說過了,默認情況下,模塊內部的 action、mutation 和 getter 是註冊在全局命名空間的,如果你想要action、mutation 和 getter 同樣是局部的話,可以通過添加 namespaced: true
的方式使其成爲帶命名空間的模塊。
//count.js
export const count = {
//...
namespaced: true,
}
- 獲取 state:和沒用命名空間相同
this.$store.state.count.count
//mapState方式
computed:{
...mapState({
count:state=>state.count.count
})
}
- 獲取 getter:
this.$store.getters['count/doubleCount']
this.$store.getters['count/countDouble']
//mapGetter方式
computed:{
...mapGetters({
countDouble:"count/countDouble"
})
}
- 提交 mutation:
this.$store.commit('moduleName/xxx')
this.$store.commit("count/increment",{num:2})
//mapMutation方式
methods:{
...mapMutations({
add:'count/increment'
})
}
在使用的時候,最好像上面例子中一樣爲mutaions
起一個別名,否則在使用的時候就是這樣調用的了this[count/increment]
- 分發 action:
this.$store.dispatch('moduleName/xxx')
this.$store.dispatch("count/xxx")
//mapActions方式
methods:{
...mapActions方式({
actionCount:'count/xxx'
})
}
以上就是使用命名空間的時候使用方法,雖然我在項目中沒有使用過,但感覺應該有用。
帶命名空間的模塊內訪問全局內容
如果我們想要在命名空間的模塊內訪問全局內容呢?我們看一下官網上的用法,
如果你希望使用全局 state 和 getter,rootState
和 rootGetters
會作爲第三和第四參數傳入 getter,也會通過 context
對象的屬性傳入 action。
modules: {
foo: {
namespaced: true,
getters: {
// 在這個模塊的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四個參數來調用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
//...
},
},
}
}
再具體用法見官網吧:https://vuex.vuejs.org/zh/guide/modules.html