vuex 原始碼:深入 vuex 之 module

cobish發表於2018-03-30

前言

store 將應用的狀態集中起來,但如果應用變得非常複雜時,即狀態非常的多時,store 就有可能變得相當臃腫。module 能夠幫 store 劃分了模組,每個模組都擁有自己的 state、getter、mutation、action 和 module。

那麼 module 又是怎樣進行劃分的,劃分後的模組又是如何管理自己的狀態呢?接下來就來解讀 module 的實現吧。

準備

解讀前,需要對以下知識有所瞭解:

  1. Array.prototype.reduce()
  2. Vue.set()

解讀

在 vuex 文件裡有這麼一句話:預設情況下,模組內部的 action、mutation 和 getter 是註冊在全域性名稱空間的——這樣使得多個模組能夠對同一 mutation 或 action 作出響應。

什麼意思呢?先看看以下示例:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    addNote () {
      console.log('root addNote')
    }
  },
  modules: {
    a: {
      state: {
        count: 0
      },
      mutations: {
        addNote () {
          console.log('module a addNote')
        }
      }
    }
  }
})
複製程式碼

使用了 module 之後,state 則會被模組化。比如要呼叫根模組的 state,則呼叫 store.state.count,如果要呼叫 a 模組的 state,則呼叫 store.state.a.count

但是示例中的 mutation 則是註冊在全域性下的,即呼叫 store.commit('addNote'),將會呼叫跟模組和 a 模組的 mutation。除非區分各模組 mutation 的命名,否則,在同名的情況下,只要 commit 後就會被觸發呼叫。

當然,vuex 2.0.0 後面的版本新增了名稱空間 的功能,使得 module 更加的模組化。

所以接下來要解讀的 module 中,實際上只要 state 是被模組化了, action、mutation 和 getter 還是在全域性的模組下。

modules 的註冊

installModule 裡實現了 module 的註冊,定位到 installModule 方法。

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const {
    modules
  } = module

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }
  
  // mutation 的註冊
  // action 的註冊
  // getter 的註冊

  if (modules) {
    Object.keys(modules).forEach(key => {
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}
複製程式碼

看到簡化後的程式碼,可以看出 installModule 對 module 做了兩步初始化操作。第一步是使用 Vue.set() 對當前的 module 的 state 設定了監聽;第二步則是繼續遍歷子模組,然後遞迴呼叫 installModule。

set state

所以 modules 的核心實現就在於對當前的 module 的 state 設定了監聽,將此段程式碼提取出來:

const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
  Vue.set(parentState, moduleName, state || {})
})
複製程式碼

先猜測 getNestedState 方法可以獲取到父 state。所以先取得父 state,再取得當前模組名稱,最後使用 Vue.set() 將當前的 state 設定在父 state 下。實際上該實現就是在一個 vue 例項下為 data.state 新增屬性,並能夠使得 vue 例項能夠監聽到新增屬性的改動。

getNestedState

const parentState = getNestedState(rootState, path.slice(0, -1))
複製程式碼

通過 path.slice(0, -1) 將當前模組去掉,作為引數和 rootState 根狀態傳入 getNestedState 方法中,返回了當前模組的父狀態 parentState。

來看看 getNestedState 的實現:

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}
複製程式碼

如果 length 等於 0,即只有根 state,直接返回。另一種情況,如果有巢狀的模組,那麼通過 Array.prototype.reduce() 方法一直往根 state 的屬性取 path 對應的 state 並返回。

至此,state 的模組化已經註冊完成,然後遞迴呼叫 installModule 完成所有 module 的註冊。

既然是往 rootState 裡新增屬性,那麼獲取則可以通過 store.state.a 來獲取到模組,然後再繼續獲取模組裡的 state。

get modules state

之前在解讀 mutation 和 action 的時候,一直都將 getNestedState 這個方法給省略了。在註冊 mutation 和 action 的時候,會出現以下這段程式碼:

getNestedState(store.state, path)
複製程式碼

實際上這段程式碼就是獲取當前 modules 的 state,然後作為引數回傳。

存放陣列

還記得解讀 mutation 的時候,說到為什麼會將 mutation 儲存到了 store._mutations 陣列裡面。主要目的是將所有 module 裡的 mutation 都存放在一個陣列中,以便於在 commit 的時候能觸發所有 mutation。

getter 和 action 用到陣列存放也是這樣一個原因。

但是,如果兩個 module 裡有相同的 mutation 名稱,vuex 2.0.0 裡做不到只觸發其中一個 mutation。這個在往後的版本中設定名稱空間可實現。

總結

本篇是對 module 的一個解讀。註冊 module 並沒有想象中的那麼複雜,主要分為兩個步驟。

第一步是找到當前 module 的父 state,然後在其至少繫結當前 state 的監聽,保證修改了 state 會觸發相應。

第二步則是遞迴 module,保證設定子 module 的 state,從而實現 module 的子巢狀。

相關文章