Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應(yīng)用的所有組件的狀態(tài)脖母,并以相應(yīng)的規(guī)則保證狀態(tài)以一種可預(yù)測的方式發(fā)生變化士鸥。Vuex 也集成到 Vue 的官方調(diào)試工具 devtools extension,提供了諸如零配置的 time-travel 調(diào)試谆级、狀態(tài)快照導(dǎo)入導(dǎo)出等高級調(diào)試功能
為什么要使用vuex?記得在vue的props中說過,vue的數(shù)據(jù)是單向流動的,每一次更改數(shù)據(jù)都必須通過父組件的方法更改,但是因為數(shù)據(jù)是單向流動的,當(dāng)我們出現(xiàn)這么一個場景,兩個同級組件都需要同一個數(shù)據(jù)怎么辦,這個時候就只能將父組件當(dāng)做容器進行傳遞數(shù)據(jù)了,可能這個數(shù)據(jù)就只有那兩個組件需要,大家可以試想一下當(dāng)我們的應(yīng)用有幾個頁面需要一些公有的數(shù)據(jù)層級非常深的時候,傳遞數(shù)據(jù)可以說是非常的復(fù)雜,這時候我們肯定會考慮一個管理數(shù)據(jù)的容器了?這就是為什么要使用vuex了!
每一個 Vuex 應(yīng)用的核心就是 store(倉庫)烤礁。“store”基本上就是一個容器肥照,它包含著你的應(yīng)用中大部分的狀態(tài) (state)脚仔。Vuex 和單純的全局對象有以下兩點不同:
Vuex 的狀態(tài)存儲是響應(yīng)式的。當(dāng) Vue 組件從 store 中讀取狀態(tài)的時候舆绎,若 store 中的狀態(tài)發(fā)生變化鲤脏,那么相應(yīng)的組件也會相應(yīng)地得到高效更新。
你不能直接改變 store 中的狀態(tài)吕朵。改變 store 中的狀態(tài)的唯一途徑就是顯式地提交 (commit) mutation猎醇。這樣使得我們可以方便地跟蹤每一個狀態(tài)的變化,從而讓我們能夠?qū)崿F(xiàn)一些工具幫助我們更好地了解我們的應(yīng)用努溃。
一個簡單的基于vuex的簡單計數(shù)器小例子
<body>
<div id="app">
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
// new 一個VuexStore對象
const store = new Vuex.Store({
state: {
count: 0
},
// 要想改變state必須通過mutations方法改
mutations: {
increment: state => state.count++,
decrement: state => state.count--
}
})
new Vue({
el: '#app',
computed: {
count() {
// 通過計算屬性綁定store到對應(yīng)組件上
// 需要哪一個組件綁定哪一個組件
return store.state.count
}
},
methods: {
// 要想調(diào)用mutations必須使用store.commit對應(yīng)的mutation函數(shù)
increment() {
store.commit('increment')
},
decrement() {
store.commit('decrement')
}
}
})
</script>
</body>
注釋已經(jīng)比較詳細了
state
單一狀態(tài)樹,意思就是管理應(yīng)用所有狀態(tài)的對象只有一個,每一個Vue實例也只能包含一個store實例
你會發(fā)現(xiàn)獲取Store的數(shù)據(jù)是通過計算屬性獲取的,因為store的更新會帶動使用了store中對應(yīng)數(shù)據(jù)的組件的更新,所以每一次store中的數(shù)據(jù)更新,計算屬性如果檢測到需要的數(shù)據(jù)更新都將會重新求值重而重新渲染界面
-
但是我們還有一種更簡單的注入store數(shù)據(jù)的方法,直接通過
store
選項綁定到vue實例上,并且會分發(fā)給所以此實例的子組件,相當(dāng)于將store
綁定到了這一組件樹上了
image.png mapState輔助函數(shù),可以快速的幫我們綁定store數(shù)據(jù)到組件上
computed:Vuex.mapState({
// key為組件內(nèi)訪問的名稱,后面是一個函數(shù)接收store中的state我們可以直接返回
// 我們需要的數(shù)據(jù),或者通過state的數(shù)據(jù)計算出我們需要的數(shù)據(jù)
mcount: state => state.count
}),
<div id="app">
<p>{{ mcount }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
上面的綁定方式提供了一種簡寫方式
computed:Vuex.mapState({
// key為組件內(nèi)訪問的名稱,后面是一個函數(shù)接收store中的state我們可以直接返回
// 我們需要的數(shù)據(jù),或者通過state的數(shù)據(jù)計算出我們需要的數(shù)據(jù)
// mcount: state => state.count
// 上面的寫法可以簡寫為
// 傳入字符串count就等于 --> state => state.count
mcount:'count'
}),
data() {
return {
tcount:88
}
},
computed: Vuex.mapState({
// key為組件內(nèi)訪問的名稱,后面是一個函數(shù)接收store中的state我們可以直接返回
// 我們需要的數(shù)據(jù),或者通過state的數(shù)據(jù)計算出我們需要的數(shù)據(jù)
// mcount: state => {
// // 這里的this是window對象,大概是因為箭頭函數(shù)的緣故
// console.log(this);
// return state.count
// }
// 上面的寫法可以簡寫為
// 傳入字符串count就等于 --> state => state.count
// mcount:'count'
// 也可以使用函數(shù)的寫法,可以獲取當(dāng)前作用域下的this
mcount(state) {
return state.count + this.tcount
}
}),
如果我們傳遞鍵值對是相同的,類似于state的key是什么我們就以什么key綁定到組件上,還可以進行進一步簡寫
<div id="app">
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
computed: Vuex.mapState(['count']),
大家會發(fā)現(xiàn)上面的這種寫法其實已經(jīng)可以說是替換掉了所有的computed了,難道vuex使用就有這種缺點嗎?不不不,vue官方提供了一種解決方案幫助我們將mapState返回的對象融入到computed對象中(為什么會占用整個computed,因為mapState返回的就是一個對象),那么大家肯定有一種想法了,看下面的例子
computed: {
state:Vuex.mapState(['count'])
},
不知道大家有沒有這種想法,但是很遺憾就是這個方法不行,所以我們只能老老實實用vue官方的方法(低版本瀏覽器就不要想了)
computed: {
ctcount(){
// 22次平方,Python數(shù)學(xué)運算符
return this.tcount ** 22;
},
// 混入mapState
// 這是展開運算符,但是是展開對象的,而不是數(shù)組
...Vuex.mapState(['count'])
},
<div id="app">
<p>混合成功: {{ctcount}}</p>
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
關(guān)于state部分介紹到此
Getter
想像一種場景,我們組件需要的數(shù)據(jù)是通過state中的數(shù)據(jù)計算出來的,這時候簡單的計算還好說如果是復(fù)雜的過濾呢?每一次使用這個數(shù)據(jù)或者很多個組件需要過濾后的數(shù)據(jù)那么就必須重復(fù)寫許多的過濾方法,或者定義一個公共的方法
computed: {
doneTodosCount () {
// 如果其他組件需要要么抽離出去一個公共函數(shù),要么直接copy代碼
return this.$store.state.todos.filter(todo => todo.done).length
}
}
其實無論那種解決的辦法都不是特別的理想,所以vuex允許我們在store中定義getter屬性(可以認為是store的計算屬性),就像計算屬性一樣硫嘶,getter 的返回值會根據(jù)它的依賴被緩存起來,且只有當(dāng)它的依賴值發(fā)生了改變才會被重新計算
const store = new Vuex.Store({
state: {
count: 0
},
// 要想改變state必須通過mutations方法改
mutations: {
increment: state => state.count++,
decrement: state => state.count--
},
getters:{
// 每一個getter接收state為第一個參數(shù)
compuCount(state){
// 這樣我們就計算除了一個不知道的值
return state.count ** 3 / 87 * 4 % 2;
}
}
})
那么我們是把getter定義出來了,但是我們需要怎么訪問這個getter呢?
不可能是這樣吧...Vuex.mapState(['compuCount'])
,這樣就直接報錯了,那么vuex提供了三種方式訪問Store的getter
- 通過屬性訪問
<body>
<div id="app">
<p>{{compuCount}}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
// new 一個VuexStore對象
const store = new Vuex.Store({
state: {
count: 2
},
// 要想改變state必須通過mutations方法改
mutations: {
increment: state => state.count++,
decrement: state => state.count--
},
getters:{
testGetter(state){
return state.count ** 2;
},
// 每一個getter接收state為第一個參數(shù)
// getter還可以接收其他的getter作為第二個參數(shù)
compuCount(state,otherGetters){
// 第二個參數(shù)包含了所有的getters
console.log(otherGetters);
// 這樣我們就計算除了一個不知道的值
return state.count ** 10 / 87 * 4 % 5;
}
}
})
new Vue({
el: '#app',
// 綁定store
store,
data() {
return {
}
},
computed: {
compuCount(){
return this.$store.getters.compuCount;
}
},
methods: {
// 要想調(diào)用mutations必須使用store.commit對應(yīng)的mutation函數(shù)
increment() {
store.commit('increment')
},
decrement() {
store.commit('decrement')
}
}
})
</script>
</body>
注意梧税,getter 在通過屬性訪問時是作為 Vue 的響應(yīng)式系統(tǒng)的一部分緩存其中的沦疾。
- 通過方法訪問
你可以讓你定義的getter返回一個函數(shù),通過傳入不同的參數(shù)返回不同的結(jié)果
<body>
<div id="app">
<p>{{powerCount}}</p>
<button @click="add">+</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state) => { state.count++ },
minus: (state) => { state.count-- }
},
getters: {
powerCount: (state) => (num) => {
console.log(state.count,num);
return state.count ** num;
}
}
})
new Vue({
// 必須綁定store,不然mapState報一些奇怪的錯誤
store,
computed: {
...Vuex.mapState(['count']),
powerCount(){
return store.getters.powerCount(this.count);
}
},
methods: {
add() {
store.commit('add');
},
minus() {
store.commit('minus')
}
},
}).$mount("#app");
</script>
</body>
注意,getter 在通過方法訪問時第队,每次都會去進行調(diào)用哮塞,而不會緩存結(jié)果。
- mapGetters輔助函數(shù)
mapGetters 輔助函數(shù)僅僅是將 store 中的 getter 映射到局部計算屬性,跟mapState的使用方法完全一致
computed: {
...Vuex.mapState(['count']),
// 如果你不想使用getter的name綁定到store上
// 可以{yourName:'powerCount'},傳入一個對象
// 你定義什么名字,就綁定什么名字到實例上
// 實測無法綁定返回函數(shù)的getter
...Vuex.mapGetters(['pCount'])
},
Mutation
mutation一句話就是改變state的唯一途徑(后面的異步action也是通過最后調(diào)用mutation改變state的)
跟redux一樣,觸發(fā)mutation我們可以給mutation傳參,叫做payload(載荷)
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add">+10</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state, num, num2) => {
// 10 undefined
console.log(num, num2);
num = num || 1;
return state.count += num;
},
minus: (state) => { state.count-- }
},
getters: {
powerCount: (state) => (num) => {
console.log(state.count, num);
return state.count ** num;
},
pCount(state) {
return state.count ** state.count;
}
}
})
new Vue({
// 必須綁定store,不然mapState報一些奇怪的錯誤
store,
computed: {
...Vuex.mapState(['count']),
// 如果你不想使用getter的name綁定到store上
// 可以{yourName:'powerCount'},傳入一個對象
// 你定義什么名字,就綁定什么名字到實例上
// 實測無法綁定返回函數(shù)的getter
...Vuex.mapGetters(['pCount'])
},
methods: {
add() {
// 最多只能接收一個payload
// 所以大多數(shù)情況下payload為對象
store.commit('add', 10, 10);
},
minus() {
store.commit('minus')
}
},
}).$mount("#app");
</script>
</body>
mutation需要遵守Vue的響應(yīng)規(guī)則
- 最好提前在你的 store 中初始化好所有所需屬性凳谦。
- 當(dāng)需要在對象上添加新屬性時忆畅,你應(yīng)該
使用
Vue.set(obj, 'newProp', 123)
, 或者-
以新對象替換老對象。例如尸执,利用 stage-3 的對象展開運算符我們可以這樣寫:
state.obj = { ...state.obj, newProp: 123 }
-
使用常亮替代mutation類型
首先在對應(yīng)的目錄新建一個mutation-types的js文件,用于導(dǎo)出所有mutation函數(shù)名
image.png
然后在index.html中就可以這樣寫
// 使用es6語法導(dǎo)入mutation-types
import { ADD, MINUS } from "../lib/mutation-types.js";
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
[ADD]: (state, num, num2) => {
// 10 undefined
console.log(num, num2);
num = num || 1;
return state.count += num;
},
[MINUS]: (state) => { state.count-- }
},
getters: {
powerCount: (state) => (num) => {
console.log(state.count, num);
return state.count ** num;
},
pCount(state) {
return state.count ** state.count;
}
}
})
我這樣寫在瀏覽器中會出現(xiàn)錯誤,也不知道什么情況(猜測大概是因為import需要在一個單獨的js文件中寫吧)mutation必須是同步函數(shù),不支持異步操作
提交 mutation 的另一種方式是直接使用包含 type 屬性的對象:
store.commit({
type: 'increment',
amount: 10
})
你可以在組件中使用 this.$store.commit('xxx') 提交 mutation邻眷,或者使用 mapMutations 輔助函數(shù)將組件中的 methods 映射為 store.commit 調(diào)用(需要在根節(jié)點注入 store)眠屎。
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add">+</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state) => {
return state.count ++;
},
minus: (state) => { state.count-- }
}
})
new Vue({
// 必須綁定store,不然mapState報一些奇怪的錯誤
store,
computed: {
...Vuex.mapState(['count'])
},
methods: {
...Vuex.mapMutations([
// 將add -> mutation映射到this.add
'add','minus'
])
},
}).$mount("#app");
</script>
</body>
不僅僅可以直接映射到方法,甚至還可以映射一個帶參數(shù)方法
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add(10)">+</button>
<button @click="minus">-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state,num) => {
return state.count += num;
},
minus: (state) => { state.count-- }
}
})
new Vue({
// 必須綁定store,不然mapState報一些奇怪的錯誤
store,
computed: {
...Vuex.mapState(['count'])
},
methods: {
...Vuex.mapMutations([
// 將add -> mutation映射到this.add
'add','minus'
])
},
}).$mount("#app");
</script>
</body>
跟其他兩個map函數(shù)一樣,同樣可以通過一個獨享設(shè)置別名
// this.madd = this.$store.commit('add')
mapMutations({madd:'add'})
其實mapMutations的映射原理,就是通過對應(yīng)的實例綁定store選項,然后將對應(yīng)的函數(shù)映射到$store的commit()方法上了
Action
action就是專門用來解決無法異步更新state的,但是大家要明白其實action最后依然調(diào)用的是mutation并不是直接更新state的,Action 函數(shù)接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調(diào)用
context.commit
提交一個 mutation肆饶,或者通過context.state
和context.getters
來獲取 state 和 getters
<body>
<div id="app">
<p>{{count}}</p>
<button @click="add(10)">+</button>
<button @click="minus">-</button>
<button @click="addAsync(5)">async-</button>
</div>
<script src="../lib/vue.js"></script>
<script src="../lib/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
add: (state,num) => {
return state.count += num;
},
minus: (state) => { state.count-- }
},
actions:{
addAsync(context){
setTimeout(()=>{
context.commit('add',10);
},1000)
}
}
})
new Vue({
// 必須綁定store,不然mapState報一些奇怪的錯誤
store,
computed: {
...Vuex.mapState(['count'])
},
methods: {
...Vuex.mapMutations([
// 將add -> mutation映射到this.add
'add','minus'
]),
addAsync(num){
store.dispatch("addAsync",num);
}
},
}).$mount("#app");
</script>
</body>
action必須使用store的dispatch觸發(fā)(太像redux了)
Actions 支持同樣的載荷方式和對象方式進行分發(fā):
// 以載荷形式分發(fā)
store.dispatch('incrementAsync', {
amount: 10
})
// 以對象形式分發(fā)
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在組件中分發(fā)action
你在組件中使用 this.$store.dispatch('xxx') 分發(fā) action改衩,或者使用 mapActions 輔助函數(shù)將組件的 methods 映射為 store.dispatch 調(diào)用(需要先在根節(jié)點注入 store):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 將 `this.increment()` 映射為 `this.$store.dispatch('increment')`
// `mapActions` 也支持載荷:
'incrementBy' // 將 `this.incrementBy(amount)` 映射為 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 將 `this.add()` 映射為 `this.$store.dispatch('increment')`
})
}
}
一個組合異步action小例子
// 假設(shè) getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
module
由于使用單一狀態(tài)樹,應(yīng)用的所有狀態(tài)會集中到一個比較大的對象驯镊。當(dāng)應(yīng)用變得非常復(fù)雜時葫督,store 對象就有可能變得相當(dāng)臃腫。
為了解決以上問題板惑,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 的狀態(tài)
store.state.b // -> moduleB 的狀態(tài)
模塊的局部狀態(tài)
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// 這里的 `state` 對象是模塊的局部狀態(tài)
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}