vuex 源碼:深刻 vuex 之 namespaced

前言

解讀完 module 以後,我的以爲有了 namespaced 的 module 纔算是真正的模塊,因而又補充了這一篇。vue

namespaced 把 getter、mutation 和 action 都作了真正的模塊化,使得 store 可使用特定模塊的 mutation 等。本篇就來瞧瞧 namespaced 是如何實現的?git

準備

這一次閱讀的是 vuex 的 2.1.0 版本,源碼請戳 這裏。建議下載並打開代碼跟着解讀,不然可能看得一臉懵逼。github

解讀

vuex 2.1.0 的代碼大體與 2.0.0 相似,只不過將 module 提取到一個目錄裏。裏面有兩個文件,分別是 module-collection.js 和 module.js,接下來會涉及到這兩個文件的解讀。vuex

仍是從 store 的構造函數 constructor 出發,開始尋找與 module 和 namespaced 相關的代碼。數組

constructor (options = {}) {
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)

  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  installModule(this, state, [], this._modules.root)
}
複製代碼

咱們先把 _modulesNamespaceMap 單獨拎出來,這個屬性主要是輔助函數 mapState 使用到,解讀到 mapState 時再用它。app

因此 constructor 對 module 作了兩件事,一是經過傳配置參數 options 來初始化 _modules,二是經過 installModule 來註冊 module。ide

作的事情很少,但裏面實現的代碼還真很多,你們作好心理準備吧哈哈。模塊化

ModuleCollection

首先來看看 new ModuleCollection 初始化 _modules 到底作了哪些事。定位到 module-collection.js 文件,看到它的構造函數:函數

constructor (rawRootModule) {
  // register root module (Vuex.Store options)
  this.root = new Module(rawRootModule, false)

  // register all nested modules
  if (rawRootModule.modules) {
    forEachValue(rawRootModule.modules, (rawModule, key) => {
      this.register([key], rawModule, false)
    })
  }
}
複製代碼

構造函數作了也是兩件事情,一件是註冊了一個根 module,另外一個是遍歷註冊子 module。打開 module.js 看下主要使用到的代碼:ui

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
  }

  addChild (key, module) {
    this._children[key] = module
  }

  getChild (key) {
    return this._children[key]
  }
}
複製代碼

構造函數裏添加 _children 即子模塊,而後當前模塊保存在 _rawModule。而後就是兩個會用到的方法 addChild 和 getChild,顧名思義,就是添加子模塊和獲取子模塊會用到。

再回到 ModuleCollection 構造函數的第二步,定位到 register 方法:

// 獲得對應 path 的 module
get (path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}
  
register (path, rawModule, runtime = true) {
  // path.slice(0, -1) 表示去掉最後一個
  // 取得父 module
  const parent = this.get(path.slice(0, -1))

  // new 一個新的 module
  const newModule = new Module(rawModule, runtime)

  // 添加子 module
  parent.addChild(path[path.length - 1], newModule)

  // register nested modules
  if (rawModule.modules) {
    // 遞歸註冊子 module
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}
複製代碼

代碼註釋都添加上了,能夠看到該 register 跟以前解讀 module 時遞歸 set state 有點相似。這裏遞歸完後會生成一個 module 實例,若該實例有子 module,那麼存放在它的 _children 屬性中,以此類推。

installModule

store 初始化 _modules 屬性後,接下來就是註冊 module。定位到 installModule 方法:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (namespace) {
    store._modulesNamespaceMap[namespace] = 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, module.state)
    })
  }

  const local = module.context = makeLocalContext(store, namespace)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, path)
  })

  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local, path)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local, path)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
複製代碼

代碼雖然有點長,但有些是之前解讀過的(註冊 state),有些是大同小異的(註冊 mutation、action 和 getter)。大體瞄了一眼,其中根據 _modules 生成 namespace,而後分別註冊 state、mutation、action 和 getter,最後遞歸註冊子模塊。

由於註冊 state 和遞歸子模塊以前解決過,因此再也不重複。接下就是看看 getNamespace 和 registerAction(其它大同小異) 是如何實現的。

getNamespace

定位到 ModuleCollection 的 getNamespace:

getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}
複製代碼

根據參數 path 來拼接 namespace。好比如下:

// options
{
  modules: {
    a: {
      modules: {
        b: {
          // ...
        }
      }
    }
  }
}
複製代碼

path 數組對應的 namespace 分別爲:

// []
/

// [a]
/a/

// [a, b]
/a/b/
複製代碼

registerAction

獲取到 namespace 以後,接下來就是註冊 action。

const local = module.context = makeLocalContext(store, namespace)

module.forEachAction((action, key) => {
  const namespacedType = namespace + key
  registerAction(store, namespacedType, action, local, path)
})
複製代碼

第二步的 registerAction 以前已經解讀過,只不過是將子 module 裏的 action 用 namespacedType 做爲 key 表示,用來區分 store._actions 的 key。因此這段代碼主要解讀第一步的 makeLocalContext。

function makeLocalContext (store, namespace) {
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (!store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    }
  }

  return local
}
複製代碼

代碼簡化後仍是很多的。其中 unifyObjectStyle 方法只是爲了支持傳參有多種格式,這裏再也不詳細解讀。

這裏主要是爲了 action 傳回來的參數後再 dispatch action 時簡化第一個參數的路徑。看如下代碼可能會比較容易理解:

modules: {
  foo: {
    namespaced: true,

    actions: {
      // 在這個模塊中, dispatch 和 commit 也被局部化了
      // 他們能夠接受 `root` 屬性以訪問根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
      }
    }
  }
}
複製代碼

helper

vuex 的輔助函數有帶命名空間的綁定函數,以下:

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo',
    'bar'
  ])
}
複製代碼

mapActions 方面,主要在原來的實現上包裝了一層 normalizeNamespace。打開 helper.js 文件,找到 normalizeNamespace 方法:

function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}
複製代碼

主要是拼接 path 給 store.__actions 調用。

可是 state 不像 action 是存放在數組裏的,因此須要用到 _modulesNamespaceMap 來取得當前的 module,才能取到裏面的 state。

_modulesNamespaceMap 具體實現代碼很少,就不詳細解讀了。

總結

終於將 namespace 解讀完了,感受比以前的解讀要困難一些,涉及到的代碼量也多了很多,因此有些代碼也沒能詳細解讀。

module 添加了 namespace,將整個 module 都提取了出來,遞歸初始化一個 _modules,方便後面模塊的查找與使用。

namespace 做爲路徑,並做爲數組的 key 去訪問到子模塊的 action 等。從而能夠單獨訪問到子模塊內的 action 等。相比於以前只能訪問子模塊內 state 而不能訪問 action 等的 2.0.0 版本,2.1.0 版本添加了 namespace 就更加模塊化了。

相關文章
相關標籤/搜索