vuex 原始碼:深入 vuex 之 mutation

cobish發表於2018-03-19

前言

vuex 規定更改 state 的唯一方法是提交 mutation,主要是為了能用 devtools 追蹤狀態變化。

那麼,提交 mutation 除了最主要的更改 state,它還做了其它一些什麼事情呢,讓我們來一探究竟。

注:本次閱讀的是 vuex 的 2.0.0 版本,原始碼請戳 這裡

準備

解讀前,需瞭解一些知識:

  1. Vuex Module
  2. Vuex 嚴格模式
  3. Class 的基本語法 - this 的指向

解讀

在解讀前先來看看 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 只支援同步更新狀態。

相關文章