1. 什麼是Vuex
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。這是官網的說法,其實很簡單:就是一個加強版的data!
在單頁應用中會有一個data函數,裏面就存放了當前頁面的一些數據。比如:
<template>
<div>
<p>{{num}}</p>
</div>
</template>
<script>
export default {
data(){
return {
num:'99'
}
}
}
</script>
代碼中的data就相當於我們這裏描述的Vuex。
如果我們的頁面比較簡單,切記千萬不要沒事找事引入Vuex,我們使用Vuex是因爲項目變得複雜之後,有很多數據需要在父組件、子組件和孫組件之間傳遞,處理起來很繁瑣,於是就需要Vuex這樣一個可以對這一部分數據進行統一管理。
2. 全局data就行了,非要這麼複雜?
對於我剛剛提到的需求:處理大量的需要在組件間傳遞的數據,直接定義一個全局的data屬性保存就行了,比如這樣:this.$root.$data
。爲什麼還要搞一個這麼複雜的狀態管理?
如果我們按照剛剛所說的搞一個全局變量存放數據其實也行,但是這樣有一個問題,就是數據改變後,不會留下變更過的記錄,這樣不利於調試。
所以我們稍微搞得複雜一點。我們約定組件不能直接修改屬於store實例的state,組件必須通過Mutation來改變state,也就是說,組件裏面應該執行分發(dispatch)事件通知store去改變。這樣約定的好處是,我們能夠記錄所有store中發生的state改變,同時實現能做到記錄變更、保存狀態快照、歷史回滾/時光旅行的先進的調試工具。
3. 先來一個簡單的State
知道了個大概,那我們就實際操作一遍,感覺一下這個聽起來很牛逼的東西用起來到底怎麼樣。
我們使用vue-cli腳手架生成一個Vue項目,並且安裝Vuex,最終的項目結構是這樣的:
我們在components裏面新建一個組件,然後加入以下代碼:
<p>{{ count }}</p>
<div>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
methods:{
increment(){
},
decrement(){
},
},
接着新建一個vuex文件夾,裏面新建一個store.js文件,並加入以下代碼:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 應用初始狀態
const state = {
count: 0,
}
// 創建 store 實例
export default new Vuex.Store({
state,
})
然後我們在組件中就可以使用我們的count變量了:
computed:{
count(){
return this.$store.state.count;
},
}
我們定義一個計算屬性,返回store中的count變量:
3. 計算屬性?引入Getter
這裏我們可以假想一個場景,我們的頁面中現在顯示的數字是0,如果我們的需求是這樣的,頁面中數字如果小於10,就顯示爲00。那我們可以在剛剛的組件中將計算屬性返回的值修改成這樣:return this.$store.state.count > 9 ? this.$store.state.count : '0'+this.$store.state.count;
就可以了,但是如果我們有很多組件都使用了這個count的話,那我們在每一個使用這個變量的地方都需要寫一遍這個判斷,那爲什麼不在取數據的時候就把數據整理成想要的樣子呢?就和我們組件中的計算屬性一樣!爲了達到目的,我們修改一下store.js:
import Vue from 'vue'
import Vuex from 'vuex'
import * as getters from './getters' //新增
Vue.use(Vuex)
// 應用初始狀態
const state = {
count: 0,
}
// 創建 store 實例
export default new Vuex.Store({
state,
getters //新增
})
然後我們新建一個getters.js文件:
export const countaddzero = state => {
return state.count > 9 ? state.count : '0' + state.count;
}
最後我們改一改組件中的計算屬性:
return this.$store.getters.countaddzero;
保存之後發現頁面上的數字果然變成了00。
4. 試試修改State,引入Mutation
我們的本意是要做一個點擊按鈕數字可以增加的一個小demo,現在把數據存到store是完成了。接下來說一下如何修改數據。
爲了方便查看,我們先修改一下store.js這個文件:
import Vue from 'vue'
import Vuex from 'vuex'
import * as getters from './getters'
Vue.use(Vuex)
// 應用初始狀態
const state = {
count: 0,
}
// 定義所需的 mutations //新增
const mutations = { //新增
increment(state, val = 1) { //新增
state.count += val; //新增
}, //新增
decrement(state, val = 1) { //新增
state.count -= val; //新增
} //新增
} //新增
// 創建 store 實例
export default new Vuex.Store({
state,
getters,
mutations //新增
})
其中的val表示點擊按鈕每次需要增加(減少)多少。
有了mutations我們就可以在組件中對store裏面的state進行修改了,我們先處理一下組件中的點擊事件,當點擊加或者減按鈕的時候纔去修改store裏面的值。我們修改一下組件的methods:
methods:{
increment(){
this.$store.commit('increment',2); //新增
},
decrement(){
this.$store.commit('decrement',2); //新增
},
},
其中的2就是之前提到的每一次增加多少,也就是前文的val。我們約定只能通過commit的方式修改store的變量,爲什麼說是預定呢?手賤的我決定試試:
methods:{
increment(){
// this.$store.commit('increment',2); //修改
this.$store.state.count=1000; //新增
},
},
其實這樣也可以修改store裏面的值,但是這樣就不能達到我們剛開始所說的目的了。
一條重要的原則就是要記住mutation必須是同步函數。因爲我們不知道什麼時候回調函數實際上被調用——實質上任何在回調函數中進行的狀態的改變都是不可追蹤的。
5. 異步修改?引入Action
那如果我們就想異步的修改store的值呢?也是有辦法的,這時候就需要我們的Action出場了:
Action 類似於 mutation,不同在於:
Action 提交的是 mutation,而不是直接變更狀態。
Action 可以包含任意異步操作。
我們還是先改改代碼,我們新建一個actions.js:
export const incrementAsync = ({ commit }, val = 1) => {
setTimeout(() => {
commit('increment', val)
}, 1000)
}
export const decrementAsync = ({ commit }, val = 1) => {
setTimeout(() => {
commit('decrement', val)
}, 1000)
}
這裏我們使用setTimeout模擬一下異步執行。接着修改我們的組件
<p>
異步:
<button @click="increment2">+</button>
<button @click="decrement2">-</button>
</p>
修改一下methods:
methods:{
increment2(){
this.$store.dispatch('incrementAsync',2);
},
decrement2(){
this.$store.dispatch('decrementAsync',2);
},
},
Action通過store.dispatch
方法觸發,乍一眼看上去感覺多此一舉,我們直接分發mutation豈不更方便?實際上並非如此,還記得mutation必須同步執行這個限制麼?Action 就不受約束!我們可以在 action 內部執行異步操作。
最後修改一下store.js文件:
在頂部導入actions:import * as actions from './action'
然後在實例化的時候加入actions:
// 創建 store 實例
export default new Vuex.Store({
actions, //新增
getters,
state,
mutations
})
這樣就可以實現異步修改store啦!
6. 不方便維護?引入Module
由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常複雜時,store 對象就有可能變得相當臃腫。
爲了解決以上問題,Vuex允許我們將store分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊——從上至下進行同樣方式的分割:
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
7. 一點點注意事項
當在嚴格模式中使用 Vuex 時,在屬於 Vuex 的 state 上使用 v-model 會比較棘手:
<input v-model="obj.message">
假設這裏的 obj 是在計算屬性中返回的一個屬於Vuex store的對象,在用戶輸入時,v-model會試圖直接修改obj.message。在嚴格模式中,由於這個修改不是在 mutation 函數中執行的, 這裏會拋出一個錯誤。
也就是說其實雙向數據綁定和vuex是會有一點衝突的,不過化解的方法也有:
第一種方法:
給 中綁定 value,然後偵聽input或者change事件,在事件回調中調用 action:
<input :value="message" @input="updateMessage">
也就是不實用v-model。
第二種方法:
雙向綁定的計算屬性
<input v-model="message">
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}