本文為轉(zhuǎn)載,原文:Vue學(xué)習(xí)筆記進(jìn)階篇——vuex核心概念
前言
本文將繼續(xù)上一篇 vuex文章 著榴,來詳細(xì)解讀一下vuex的核心概念啤挎,states, getters, mutations, actions, mudoles.
State
單一狀態(tài)樹
Vuex 使用的是單一狀態(tài)樹
饲嗽,用一個(gè)對(duì)象就包含了全部的應(yīng)用層級(jí)狀態(tài)炭玫。至此它便作為一個(gè)『唯一數(shù)據(jù)源(SSOT)』
而存在。這也意味著喝噪,每個(gè)應(yīng)用將僅僅包含一個(gè) store
實(shí)例础嫡。單一狀態(tài)樹讓我們能夠直接地定位任一特定的狀態(tài)片段,在調(diào)試的過程中也能輕易地取得整個(gè)當(dāng)前應(yīng)用狀態(tài)的快照酝惧。
單狀態(tài)樹和模塊化并不沖突 —— 在后面的章節(jié)里我們會(huì)討論如何將狀態(tài)和狀態(tài)變更事件分布到各個(gè)子模塊中榴鼎。
在 Vue 組件中獲得 Vuex 狀態(tài)
那么我們?nèi)绾卧?Vue 組件中展示狀態(tài)呢?由于 Vuex 的狀態(tài)存儲(chǔ)是響應(yīng)式的晚唇,從 store 實(shí)例中讀取狀態(tài)最簡(jiǎn)單的方法就是在計(jì)算屬性
中返回某個(gè)狀態(tài):
computed:{
count(){
return this.$store.state.count
},
}
每當(dāng) this.$store.state.count
變化的時(shí)候, 都會(huì)重新求取計(jì)算屬性巫财,并且觸發(fā)更新相關(guān)聯(lián)的 DOM。
然而哩陕,這種模式導(dǎo)致組件依賴的全局狀態(tài)單例平项。在模塊化的構(gòu)建系統(tǒng)中赫舒,在每個(gè)需要使用 state 的組件中需要頻繁地導(dǎo)入,并且在測(cè)試組件時(shí)需要模擬狀態(tài)闽瓢。
而在vue組件中獲得vuex狀態(tài)的時(shí)候接癌,我們更多的是使用mapState輔助函數(shù),因?yàn)樗褂闷饋砗芎?jiǎn)單扣讼,也很方便缺猛。
mapState輔助函數(shù)
當(dāng)一個(gè)組件需要獲取多個(gè)狀態(tài)時(shí)候,將這些狀態(tài)都聲明為計(jì)算屬性會(huì)有些重復(fù)和冗余椭符。為了解決這個(gè)問題荔燎,我們可以使用 mapState
輔助函數(shù)幫助我們生成計(jì)算屬性,讓你少按幾次鍵:
// 在單獨(dú)構(gòu)建的版本中輔助函數(shù)為 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭頭函數(shù)可使代碼更簡(jiǎn)練
count: state => state.count,
// 傳字符串參數(shù) 'count' 等同于 `state => state.count`
countAlias: 'count',
// 為了能夠使用 `this` 獲取局部狀態(tài)销钝,必須使用常規(guī)函數(shù)
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
當(dāng)映射的計(jì)算屬性的名稱與 state 的子節(jié)點(diǎn)名稱相同時(shí)有咨,我們也可以給 mapState
傳一個(gè)字符串?dāng)?shù)組。
computed: mapState([
// 映射 this.count 為 store.state.count
'count'
])
對(duì)象展開運(yùn)算符
mapState
函數(shù)返回的是一個(gè)對(duì)象蒸健。我們?nèi)绾螌⑺c局部計(jì)算屬性混合使用呢座享?通常,我們需要使用一個(gè)工具函數(shù)將多個(gè)對(duì)象合并為一個(gè)似忧,以使我們可以將最終對(duì)象傳給 computed
屬性征讲。但是自從有了對(duì)象展開運(yùn)算符(現(xiàn)處于 ECMASCript 提案 stage-3 階段),我們可以極大地簡(jiǎn)化寫法:
computed:{
count(){
return this.$store.state.count
},
...mapState(['count']),
...mapState({
countAlias: 'count',
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
寫法是多種的橡娄,具體怎么寫,要看自己的喜好好需求了癣籽。
組件仍然保有局部狀態(tài)
使用 Vuex 并不意味著你需要將所有的狀態(tài)放入 Vuex挽唉。雖然將所有的狀態(tài)放到 Vuex 會(huì)使?fàn)顟B(tài)變化更顯式和易調(diào)試,但也會(huì)使代碼變得冗長(zhǎng)和不直觀筷狼。如果有些狀態(tài)嚴(yán)格屬于單個(gè)組件瓶籽,最好還是作為組件的局部狀態(tài)。你應(yīng)該根據(jù)你的應(yīng)用開發(fā)需要進(jìn)行權(quán)衡和確定埂材。
Getters
有時(shí)候我們需要從store
中的state
中派生出一些狀態(tài)塑顺,例如對(duì)列表進(jìn)行過濾并計(jì)數(shù):
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多個(gè)組件需要用到此屬性,我們要么復(fù)制這個(gè)函數(shù)俏险,或者抽取到一個(gè)共享函數(shù)然后在多處導(dǎo)入它 —— 無論哪種方式都不是很理想严拒。
Vuex 允許我們?cè)?code>store中定義『getters』
(可以認(rèn)為是 store
的計(jì)算屬性
)。Getters
接受 state
作為其第一個(gè)參數(shù):
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
Getters
會(huì)暴露為 store.getters
對(duì)象:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getters
也可以接受其他 getters
作為第二個(gè)參數(shù):
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我們可以很容易地在任何組件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
mapGetters 輔助函數(shù)
mapGetters
輔助函數(shù)僅僅是將store
中的 getters
映射到局部計(jì)算屬性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用對(duì)象展開運(yùn)算符將 getters 混入 computed 對(duì)象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想將一個(gè) getter
屬性另取一個(gè)名字竖独,使用對(duì)象形式:
mapGetters({
// 映射 this.doneCount 為 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})
其實(shí)裤唠,你會(huì)發(fā)現(xiàn),mapGetter
的作用和mapState
是一樣的莹痢。語法也是一致的种蘸。
Mutations
更改 Vuex 的 store 中的狀態(tài)的唯一方法是提交 mutation
墓赴。Vuex 中的 mutations
非常類似于事件:每個(gè) mutation
都有一個(gè)字符串的 事件類型 (type)
和 一個(gè)回調(diào)函數(shù) (handler)
。這個(gè)回調(diào)函數(shù)就是我們實(shí)際進(jìn)行狀態(tài)更改的地方航瞭,并且它會(huì)接受 state 作為第一個(gè)參數(shù):
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 變更狀態(tài)
state.count++
}
}
})
你不能直接調(diào)用一個(gè) mutation handler诫硕。這個(gè)選項(xiàng)更像是事件注冊(cè):“當(dāng)觸發(fā)一個(gè)類型為 increment 的 mutation 時(shí),調(diào)用此函數(shù)刊侯≌掳欤”要喚醒一個(gè) mutation handler,你需要以相應(yīng)的 type 調(diào)用store.commit
方法:
store.commit('increment')
提交載荷(Payload)
你可以向 store.commit
傳入額外的參數(shù)滔吠,即 mutation 的 載荷(payload)
:
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多數(shù)情況下纲菌,載荷應(yīng)該是一個(gè)對(duì)象,這樣可以包含多個(gè)字段并且記錄的 mutation 會(huì)更易讀:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
對(duì)象風(fēng)格的提交方式
提交 mutation 的另一種方式是直接使用包含 type 屬性的對(duì)象:
store.commit({
type: 'increment',
amount: 10
})
當(dāng)使用對(duì)象風(fēng)格的提交方式疮绷,整個(gè)對(duì)象都作為載荷傳給 mutation 函數(shù)翰舌,因此 handler 保持不變:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Mutations 需遵守 Vue 的響應(yīng)規(guī)則
既然 Vuex 的 store 中的狀態(tài)是響應(yīng)式的,那么當(dāng)我們變更狀態(tài)時(shí)冬骚,監(jiān)視狀態(tài)的 Vue 組件也會(huì)自動(dòng)更新椅贱。這也意味著 Vuex 中的 mutation 也需要與使用 Vue 一樣遵守一些注意事項(xiàng):
- 最好提前在你的 store 中初始化好所有所需屬性。
- 當(dāng)需要在對(duì)象上添加新屬性時(shí)只冻,你應(yīng)該
- 使用 Vue.set(obj, 'newProp', 123), 或者 -
- 以新對(duì)象替換老對(duì)象庇麦。例如,利用 stage-3 的對(duì)象展開運(yùn)算符我們可以這樣寫:
state.obj = { ...state.obj, newProp: 123 }
使用常量替代 Mutation 事件類型
使用常量替代 mutation 事件類型在各種 Flux 實(shí)現(xiàn)中是很常見的模式喜德。這樣可以使 linter 之類的工具發(fā)揮作用山橄,同時(shí)把這些常量放在單獨(dú)的文件中可以讓你的代碼合作者對(duì)整個(gè) app 包含的 mutation 一目了然:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我們可以使用 ES2015 風(fēng)格的計(jì)算屬性命名功能來使用一個(gè)常量作為函數(shù)名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
用不用常量取決于你 —— 在需要多人協(xié)作的大型項(xiàng)目中恨狈,這會(huì)很有幫助衩婚。但如果你不喜歡礁鲁,你完全可以不這樣做胶台。
mutation 必須是同步函數(shù)
一條重要的原則就是要記住 mutation 必須是同步函數(shù)污尉。為什么姜盈?請(qǐng)參考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
現(xiàn)在想象硕蛹,我們正在 debug 一個(gè) app 并且觀察 devtool 中的 mutation 日志浪漠。每一條 mutation 被記錄秕豫,devtools 都需要捕捉到前一狀態(tài)和后一狀態(tài)的快照朴艰。然而,在上面的例子中 mutation 中的異步函數(shù)中的回調(diào)讓這不可能完成:因?yàn)楫?dāng) mutation 觸發(fā)的時(shí)候混移,回調(diào)函數(shù)還沒有被調(diào)用祠墅,devtools 不知道什么時(shí)候回調(diào)函數(shù)實(shí)際上被調(diào)用 —— 實(shí)質(zhì)上任何在回調(diào)函數(shù)中進(jìn)行的的狀態(tài)的改變都是不可追蹤的。
在組件中提交 Mutations
你可以在組件中使用 this.$store.commit('xxx')
提交 mutation
沫屡,或者使用 mapMutations
輔助函數(shù)將組件中的 methods 映射為store.commit
調(diào)用(需要在根節(jié)點(diǎn)注入 store
)饵隙。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment' // 映射 this.increment() 為 this.$store.commit('increment')
]),
...mapMutations({
add: 'increment' // 映射 this.add() 為 this.$store.commit('increment')
})
}
}
Action
Action 類似于 mutation,不同在于:
Action 提交的是 mutation沮脖,而不是直接變更狀態(tài)金矛。
Action 可以包含任意異步操作芯急。
讓我們來注冊(cè)一個(gè)簡(jiǎn)單的 action:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action
函數(shù)接受一個(gè)與store
實(shí)例具有相同方法和屬性的 context
對(duì)象,因此你可以調(diào)用 context.commit
提交一個(gè) mutation
驶俊,或者通過 context.state
和 context.getters
來獲取 state
和 getters
娶耍。當(dāng)我們?cè)谥蠼榻B到 Modules
時(shí),你就知道 context 對(duì)象為什么不是 store 實(shí)例本身了饼酿。
上面的action
注冊(cè)也可簡(jiǎn)寫為這樣
actions: {
increment ({ commit }) {
commit('increment')
}
}
分發(fā) Action
Action 通過 store.dispatch
方法觸發(fā):
store.dispatch('increment')
乍一眼看上去感覺多此一舉榕酒,我們直接分發(fā) mutation 豈不更方便?實(shí)際上并非如此故俐,還記得 mutation 必須同步執(zhí)行這個(gè)限制么想鹰?Action 就不受約束!我們可以在 action 內(nèi)部執(zhí)行異步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions 支持同樣的載荷方式
和對(duì)象方式
進(jìn)行分發(fā):
// 以載荷形式分發(fā)
store.dispatch('incrementAsync', {
amount: 10
})
// 以對(duì)象形式分發(fā)
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在組件中分發(fā) Action
你在組件中使用 this.$store.dispatch('xxx')
分發(fā)action
药版,或者使用 mapActions 輔助函數(shù)
將組件的 methods
映射為 store.dispatch
調(diào)用(需要先在根節(jié)點(diǎn)注入 store):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment' // 映射 this.increment() 為 this.$store.dispatch('increment')
]),
...mapActions({
add: 'increment' // 映射 this.add() 為 this.$store.dispatch('increment')
})
}
}
Modules
由于使用單一狀態(tài)樹辑舷,應(yīng)用的所有狀態(tài)會(huì)集中到一個(gè)比較大的對(duì)象。當(dāng)應(yīng)用變得非常復(fù)雜時(shí)槽片,store 對(duì)象就有可能變得相當(dāng)臃腫何缓。
為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)
还栓。每個(gè)模塊擁有自己的state
碌廓、mutation
、action
剩盒、getter
谷婆、甚至是嵌套子模塊——從上至下進(jìn)行同樣方式的分割:
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)
對(duì)于模塊內(nèi)部的 mutation 和 getter,接收的第一個(gè)參數(shù)
是模塊的局部狀態(tài)對(duì)象
辽聊。
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// 這里的 `state` 對(duì)象是模塊的局部狀態(tài)
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
同樣波材,對(duì)于模塊內(nèi)部的 action,局部狀態(tài)通過 context.state
暴露出來身隐, 根節(jié)點(diǎn)狀態(tài)則為 context.rootState
:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
對(duì)于模塊內(nèi)部的getter
,根節(jié)點(diǎn)狀態(tài)會(huì)作為第三個(gè)參數(shù)
暴露出來:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
項(xiàng)目結(jié)構(gòu)
Vuex 并不限制你的代碼結(jié)構(gòu)唯灵。但是贾铝,它規(guī)定了一些需要遵守的規(guī)則:
- 應(yīng)用層級(jí)的狀態(tài)應(yīng)該集中到單個(gè) store 對(duì)象中。
- 提交 mutation 是更改狀態(tài)的唯一方法埠帕,并且這個(gè)過程是同步的垢揩。
- 異步邏輯都應(yīng)該封裝到 action 里面。
只要你遵守以上規(guī)則敛瓷,如何組織代碼隨你便叁巨。如果你的 store
文件太大,只需將action
呐籽、mutation
锋勺、和 getters
分割到單獨(dú)的文件蚀瘸。
對(duì)于大型應(yīng)用,我們會(huì)希望把 Vuex 相關(guān)代碼分割到模塊中庶橱。下面是項(xiàng)目結(jié)構(gòu)示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API請(qǐng)求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我們組裝模塊并導(dǎo)出 store 的地方
├── actions.js # 根級(jí)別的 action
├── mutations.js # 根級(jí)別的 mutation
└── modules
├── cart.js # 購(gòu)物車模塊
└── products.js # 產(chǎn)品模塊
完
本文為原創(chuàng)贮勃,轉(zhuǎn)載請(qǐng)注明出處
上一節(jié):Vue學(xué)習(xí)筆記進(jìn)階篇——vuex安裝及使用
返回目錄
下一節(jié):Vue學(xué)習(xí)筆記實(shí)戰(zhàn)篇——音樂播放器 · 需求說明