前言
解讀完 module 之後,個人覺得有了 namespaced
的 module 才算是真正的模組,於是又補充了這一篇。
namespaced 把 getter、mutation 和 action 都做了真正的模組化,使得 store 可以使用特定模組的 mutation 等。本篇就來瞧瞧 namespaced 是如何實現的?
準備
這一次閱讀的是 vuex 的 2.1.0
版本,原始碼請戳 這裡。建議下載並開啟程式碼跟著解讀,否則可能看得一臉懵逼。
解讀
vuex 2.1.0 的程式碼大致與 2.0.0 類似,只不過將 module 提取到一個目錄裡。裡面有兩個檔案,分別是 module-collection.js 和 module.js,接下來會涉及到這兩個檔案的解讀。
還是從 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 時再用它。
所以 constructor 對 module 做了兩件事,一是通過傳配置引數 options 來初始化 _modules,二是通過 installModule 來註冊 module。
做的事情不多,但裡面實現的程式碼還真不少,大家做好心理準備吧哈哈。
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
看下主要使用到的程式碼:
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 就更加模組化了。