Vuex 筆記
一個簡單的狀態管理
單一數據源:
const sourceOfTruth = {}
const vmA = new Vue({
data: sourceOfTruth
})
const vmB = new Vue({
data: sourceOfTruth
})
每當 sourceOfTruth
發生變化, vmA
和 vmB
都會自動更新它們的視圖. 子組件可以通過 this.$root.$data
訪問數據. 現在我們有了單一的數據源, 但是調試會很困難. 因為無論何時數據源發生變化都會改變程序, 但是沒有任何跡象表明變化發生.
store pattern
為了解決上述問題, 我們可以引入 store pattern
:
var store = {
debug: true,
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
this.debug && console.log('setMessageAction triggered with', newValue)
this.state.message = newValue
},
clearMessageAction () {
this.debug && console.log('clearMessageAction triggered')
this.state.message = 'action B triggered'
}
}
所有的數據改變都發生 store
內. 這種集中的狀態管理模式使我們很容易記錄變化發生, 如何發生.
除了單一的數據源外, 每個 vue
實例或組件也可以有其私有狀態:
var vmA = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})
var vmB = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})
使用Vuex
// 如果 Vuex 不是全局的, 那么確保調用 Vue.use(Vuex) 使 Vuex 生效.
const store = new Vuex.Store({
// 數據源
state: {
count: 0
},
// 數據操作
mutations: {
increment (state) {
state.count++
}
}
})
// 觸發數據變化操作
store.commit('increment')
console.log(store.state.count) // -> 1
State
store
自動注入到子組件中
通常我們通過計算屬性來訪問 store
中的數據, 這樣就能感知到數據發生變化.
根組件的 store
屬性會注入到其所有的子組件中. (通過 Vue.use(Vuex)
生效)
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
// 子組件通過 this.$store 訪問父組件的 store
return this.$store.state.count
}
}
}
new Vue({
// 父對象中的 store 自動注入到子組件
store,
componets: {
Counter
}
})
mapState
如果 store
中有許多數據需要訪問, 每個數據都需要定義一個計算屬性會非常麻煩. Vuex
提供了 mapState
來簡化計算屬性的定義.
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// es6 箭頭函數更加簡潔
count: state => state.count,
// 字符串 'count' 等同於 `state => state.count`
countAlias: 'count',
// 為了訪問組件的 `this`, 必須使用普通的函數
// 箭頭函數會綁定 `this` 到 `mapState` 的參數這個對象
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
如果計算屬性和 store
中數據是一一對應的, 可以使用更簡單的字符串數組:
computed: mapState([
// map this.count to store.state.count
'count'
])
es6 的擴展操作符
使用 mapState
返回一個對象, 如果組件還有私有的計算屬性, 通常我們可以使用 _.extend({localComputed}, mapState(...))
這種方式合並對象已得到最終的 computed
. 使用 es6 的擴展操作符可以簡化:
computed: {
localComputed(){ /* ... */},
// 通過擴展操作符擴展 computed 對象
...mapState({
// ...
})
}
Getters
通常計算屬性是基於一段 store
數據的代碼, 比如過濾一個列表並計數:
computed: {
doneTodoCount() {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果我們需要復用這段代碼, 基本就是重構提取出一個函數, 但是這樣還不是很理想.
Vuex
在 store
中提供了 getters
:
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)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
})
// 通過 `store.getters` 訪問
store.getters.doneTodosCount
上面的計算屬性就可以改成:
computed: {
doneTodoCount() {
return this.$store.getters.doneTodoCount
}
}
mapGetters
同 state
的 mapState
類似, getters
也有 mapGetters
來簡化計算屬性的定義
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// mix the getters into computed with object spread operator
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
使用對象可以自定義對應關系
mapGetters({
// map this.doneCount to store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})
Mutations
Vuex
中的 state
只能通過 mutations
來改變. mutations
很像事件, 都有一個類型和處理函數. 處理函數是真正改變 state
的地方, 並以 state
作為第一個參數.
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 改變 state
state.count++
}
}
})
就是事件一樣, 我們不能直接調用處理函數, 而是要通過 store.commit(type)
來觸發 mutation
處理函數.
store.commit('increment')
帶 playload commit
我們可以將處理函數的參數放到第二個參數 playload
中:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {amount: 10})
對象風格 commit
store.commit({
type: 'increment',
playload: { amount: 10 }
})
靜默模式
默認情況下, 每一次 commit
都會發送到插件 (比如: devtools
) 中. 可能你會希望某些 commit
不被記錄. 這時候可以傳遞第三個參數以設置為靜默模式:
store.commit('increment', {
amount: 1
}, { silent: true })
// 對象風格 commit
store.commit({
type: 'increment',
amount: 1
}, { silent: true })
Mutations 要遵守 Vue 的響應式規則
即:
- 提前初始化所有的狀態值
- 添加新的屬性到對象時, 你應該:
- 使用
Vue.set(obj, 'newProp', 123)
或 - 直接替換新的對象:
state.obj = {...state.obj, newProp: 123}
- 使用
使用常量為 Mutations 命名
使用常量為 Mutations 命名是各種 Flux
實現常用的模式. 將所有常量放到一個文件中, 我們能看到整個程序有什么情況數據會發生變化.
// 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: {
// es6 特性 computed property name
// 屬性名稱運行時確定
[SOME_MUTATION] (state) {
// mutate state
}
}
})
Mutations 必須是同步的
異步 mutations
調用違反了所有的狀態變化必須在 store
中進行的規定. 比如:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
當上例中狀態變化時, someMutation
已經結束了. 這時候如果有其他狀態變化的操作發生, devtools
記錄下來的狀態變化就是錯誤的.
mapMutations
我們可以通過 this.$store.commit('xxx')
在組件中調用 mutations
, 一般我們將這些調用分裝到 methods
中, 同時 Vuex
也提供了 mapMutations
函數簡化 methods
定義:
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment' // 映射 this.increment() 到 this.$store.commit('increment')
]),
...mapMutations({
add: 'increment' // map this.add() to this.$store.commit('increment')
})
}
}
Actions
異步的 mutations
使程序的狀態變化難以追蹤. 為了解決異步操作, Vuex
引入了 actions
.
actions
跟 mutations
非常像, 它們的不同之處在於:
actions
不改變state
, 而是commit mutations
actions
可以包含任意的異步操作
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
actions
接收一個 context
對象作為參數, context
可以訪問 commit
, getters
, state
, 但是它不是 store
對象.
通常, 我們會使用 es6 的參數結構語法來簡化代碼:
actions: {
increment({commit}) {
commit('increment')
}
}
Dispatching Actions
actions
通過 store.dispatch
來觸發:
store.dispatch('increment')
dispatch
也支持 commit
中的 playload
參數以及對象風格的調用方式.
// dispatch with a payload
store.dispatch('incrementAsync', {
amount: 10
})
// dispatch with an object
store.dispatch({
type: 'incrementAsync',
amount: 10
})
mapActions
類似 mapMutations
Actions 組合
actions
通常是異步的, 我們怎么來組合多個 actions
來執行復雜的操作?
首先我們需要知道的是 store.dispatch
返回 actions
中處理函數的返回值, 因此我們可以返回一個 Promise
:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
},
actionB ({ dispatch, commit }) {
// 組合
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
使用 async/await
語法, 可以簡化為:
// 假設 getData() 和 getOtherData() 返回 Promises
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for actionA to finish
commit('gotOtherData', await getOtherData())
}
}
Modules
當我們的程序足夠大時, store
也會變得非常大, 其中的 state
, getters
, mutations
, actions
也會非常大.
因此 Vuex
允許我們將 store
分成幾個 modules
, 每個 modules
都有自己的 state
, getters
, mutations
, actions
甚至它自己的 modules
.
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's state
store.state.b // -> moduleB's state
Modules 當前狀態
在 modules
中, getters
和 mutations
的第一個參數都是 modules
的 state
, 同樣 actions
的 context.state
也是 modules
的 state
, 根節點的狀態可以通過 context.rootState
訪問到. getters
的可以通過第三個參數訪問 $rootState
:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
命名空間
modules
的 state
放到根節點的對應的屬性中, 而 actions
, mutations
和 getters
沒有命名空間. 所以多個 modules
可以對同一個 commit
或 dispatch
做響應. 因此必須自己通過前綴或后綴來避免命名沖突.
動態 Modules 注冊
store.registerModule('myModule', {
// ...
})