前言
vuex 規定更改 state 的唯一方法是提交 mutation,主要是為了能用 devtools 追蹤狀態變化。
那麼,提交 mutation 除了最主要的更改 state,它還做了其它一些什麼事情呢,讓我們來一探究竟。
注:本次閱讀的是 vuex 的 2.0.0 版本,原始碼請戳 這裡
準備
解讀前,需瞭解一些知識:
解讀
在解讀前先來看看 mutation 的使用方式:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 變更狀態
state.count++
}
}
})
store.commit('increment')
複製程式碼
初步猜測,store 物件的初始化時將 mutation 屬性物件儲存起來,在使用 commit 提交的時候取出,並將 store 的 state 等狀態作為引數傳入,最後呼叫函式。
好像跟沒說一樣哈哈。當然,我也不知道它還做了哪些事情。開始解讀吧......
從建構函式 constructor 開始,依然過濾與 mutation 無關的程式碼:
constructor (options = {}) {
this._mutations = Object.create(null)
// bind commit and dispatch to self
const store = this
const { commit } = this
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
// init root module.
installModule(this, state, [], options)
}
複製程式碼
mutation 的註冊
mutation 的實現主要分兩步,一是初始化 install,二是實現 commit 函式。先來看看初始化的 installModule
方法吧。
function installModule (store, rootState, path, module, hot) {
const {
mutations
} = module
if (mutations) {
Object.keys(mutations).forEach(key => {
registerMutation(store, key, mutations[key], path)
})
}
}
複製程式碼
程式碼裡迴圈 options 的 mutations,將其作為引數傳入 registerMutation
方法,定位到 registerMutation
方法中(自動忽略 path):
function registerMutation (store, type, handler) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler(store.state, payload)
})
}
複製程式碼
將 mutations 儲存到了 store._mutations 陣列裡面,這裡可能會好奇一下,為什麼同一個 type 對應的是一個陣列呢,而不是一個函式。為了解釋,小小透露一下。在使用了 module 的情況下,模組中不可避免地可能出現多個相同名稱的 mutations,當使用 commit 時,多個相同名稱的 mutations 會依次觸發。
const store = new Vuex.Store({
// ...
mutations: {
addNote () {
console.log('root addNote')
}
},
modules: {
a: {
mutations: {
addNote () {
console.log('module a addNote')
}
}
}
}
})
複製程式碼
以上程式碼,如果呼叫 commit('addNote') 的話,那麼兩個 addNote 方法都會執行,所以兩個 addNote 方法都是被放到 store._mutations 陣列裡面的。試想一下,如果不是陣列,那麼只會執行其中一個 addNote 方法了。
commit 的實現
接下來看 commit 的實現,這個 commit 挺有趣的,為什麼要在建構函式裡重新賦值一遍呢。其實,這裡是 this 預設指向類的例項。但是,必須非常小心,一旦單獨使用該方法,很可能報錯。具體瞭解請戳Class 的基本語法 - this 的指向。
// bind commit and dispatch to self
const store = this
const { commit } = this
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
複製程式碼
接下來就來看看 commit 的實現,將儲存的 store._mutations 陣列取出迴圈執行。需注意的是這個 _withCommit
方法。
commit (type, payload, options) {
const mutation = { type, payload }
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
}
複製程式碼
再看 _withCommit
方法,乍看一下,這個方法好像沒什麼作用。
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
複製程式碼
裡面有一個 this._committing
變數,搜尋了一下,大致瞭解了它的作用。首先我們知道 vuex 有一個嚴格模式。
預設不開啟,所以 this._committing
變數沒什麼作用。如果手動開啟,state 初始化時在 resetStoreVM
方法裡有相應的處理。
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
複製程式碼
定位到裡面的 enableStrictMode
方法一探究竟:
function enableStrictMode (store) {
store._vm.$watch('state', () => {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}, { deep: true, sync: true })
}
複製程式碼
這裡的 store._committing 預設是 false,所以我們直接賦值 state 會 watch 到並丟擲錯誤。只有在剛剛的 _withCommit
方法裡將其設定為 true 再賦值,才不會丟擲錯誤。
總結
mutation 在註冊的時候,用一個 store._mutations 陣列將 module 模組中所有同名的方法都儲存起來,在 commit 的時候則將其所有同名的方法取出並執行。
在開啟嚴格模式的情況下進行 commit 提交,vuex 使用 _withCommit
方法來保證狀態變更是由 mutation 函式引起的,而其中是用一個 _committing
變數來判斷。測試了一下,雖然會丟擲錯誤,但還是能夠進行狀態變更的,但這樣就不能用 devtools 追蹤狀態變化了。
最後需記住,mutation 只支援同步更新狀態。