从vuex源码分析module与namespaced
使用vue已經(jīng)有半年有余, 在各種正式非正式項目中用過, 開始專注于業(yè)務(wù)比較多, 用到現(xiàn)在也遇見不少因為理解不深導(dǎo)致的問題. 有問題就有找原因的勇氣, 所以帶著問題搞一波.
帶著問題看源碼
所以來整理了一下使用過程中不注意或者不規(guī)范, 或者簡化寫法的奇技淫巧, 會結(jié)合文檔的說明和實際的問題來看看源碼, 問題:
- module在vuex里實際的數(shù)據(jù)結(jié)構(gòu)
- namespaced在vuex里實際的數(shù)據(jù)結(jié)構(gòu)
- mapState, mapActions等helper的正確用法(配合module/namespaced), 或者是否存在更多騷用法
- mutation中賦值/觸發(fā)state變化原理
源碼分析
看的源碼版本為vuex2.3.1
我們使用vuex可能是類似:
import Vue from 'vue' import Vuex from 'vuex' import plugins from './plugins' Vue.use(Vuex) export default new Vuex.Store({state: {todo: ["todo1", "todo2"]},mutations: {mutationName(state, payload) {state.xxx = payload.xxx}},actions: {actionName({ commit, dispatch }, payload) {commit(mutationName, payload)}},modules: {catagories: {state: {},mutations: {}}},plugins })使用vuex的方法為使用Vue.use來installvuex, 并new一個Store實例, 我們來看一下vuex核心對象.
Store對象分析
line6: 本地vue變量, 在install時會被賦值, 之后會通過vue是否為undefined來判斷是否install
Store對象構(gòu)建
line10~14: 判斷vuex是否被正確使用
line16~26: 獲取options, state可以和vue的component的data一樣為函數(shù)return一個對象, 會在這段代碼中被parse
line28~36: store對象內(nèi)部變量初始化
line39~46: 綁定commit和dispatch方法到自身
line54: 裝載動作
line58: 裝載響應(yīng)動作
line61: 調(diào)用插件
store內(nèi)部變量初始化
this._committing = false是否合法更新state的標(biāo)識, 對象有方法_withCommit是唯一可以改動_committing的方法, 只有對象內(nèi)部使用_withCommit更新狀態(tài)才是合法的, 在strict模式下非法更新state會拋出異常.
this._modules = new ModuleCollection(options)modules的cache, 直接把store的參數(shù)全部扔給了ModuleCollection新建一個modules對象.
點擊跳轉(zhuǎn)ModuleCollection對象來看分析.
this._modulesNamespaceMap = Object.create(null) this._subscribers = [] this._watcherVM = new Vue()其余的變量是新建了空的變量, 之后會在install模塊的時候賦值.
綁定dispatch和commit方法
在line39~46, 對dispatch和commit方法進(jìn)行綁定, 使dispatch方法可以調(diào)用在Store對象上注冊過的._actions
和._mutations的方法.
dispatch方法在line108, 先兼容了參數(shù)的寫法, 取到參數(shù), 然后判斷Store對象的_.actions屬性是否注冊過, 如果注冊過多個, 將會依次調(diào)用. 也就是如果type重復(fù)了也是會調(diào)用多次的, 這個地方如果出錯debug會非常困難. 暫時沒有理解vuex此處設(shè)計的意圖.
commit方法稍微多一點, 大體思路是一樣的, 只是直接執(zhí)行沒有返回值, dispatch會返回執(zhí)行結(jié)果. 另外在line95進(jìn)行了subscriber的操作, 我們暫且不知道subscriber的作用. 稍后再看.
install模塊
首先來看參數(shù):
function installModule (store, rootState, path, module, hot) // 調(diào)用 installModule(this, state, [], this._modules.root)line255 根據(jù)path獲得namespace, 做法是讀取path的每個模塊, 如果namespaced為true則拼接, 例如path為['catagories', 'price', 'detail'], 其中price的namespaced為false, 其余為true, 那么獲得的namespace為catagories/detail/.
line258~260 把namespaced為true的module注冊到_modulesNamespaceMap.
line271的makeLocalContext函數(shù)整理了namespace和type的關(guān)系. 在之后的三個module.forEachXxx中, 都調(diào)用了registerXxx, 最后的參數(shù)都是makeLocalContext的返回值. 我們來分析一下makeLocalContext的作用:
被注冊到全局的mutation/actiongetter實際的type類似于namespace1/namespace2/type的形式, 而我們在namespaced為true的module中調(diào)用的type只是:type. 所以在namespace[true]的action中調(diào)用的所有dispatch, commit, getter, state 都會被加上 path.join("/") + "/" 的type來調(diào)用到正確的方法.
根據(jù)注冊的type, 我還得到了一個偏門結(jié)論: 可以通過設(shè)置type為namespace1/namespace2/type來調(diào)用其他namespace的type(待測試), 因為他們是這樣被注冊的.
install child module
通過比較, install child module的時候是改了第三第四個參數(shù): path => path.concat(key), module => module.getChild(key).
主要區(qū)別只是在line264~268, 與ModuleCollection的遞歸注冊子module行為類似, 遞歸的path參數(shù)流程上只是多了一步把當(dāng)前l(fā)oop產(chǎn)生的對象掛到父節(jié)點上. 做法也是一樣的, 把module名字(path)作為key, 套在父級state上. 也就是結(jié)構(gòu)為:
state: {...currentState,moduleName: {...subState},module2Name: {...anotherSubState} }在之前注冊Mutation的時候vuex也是通過這個方法來試mutation獲得嵌套過的state作為arguments[0]的.
Store對象總結(jié)
store對象把傳入的options放入了各個變量進(jìn)行儲存, 并提供了commit, dispatch等方法來調(diào)用和處理他們:
._modules
這里存放raw的modules, 未經(jīng)處理的, 以module名字作為key的方式遞歸子module.
.state
這里也是以module名字作為key的方式遞歸儲存?zhèn)魅氲膕tate
entrys
這里的entry指._actions, ._mutations, ._getters. 他們的儲存方式并沒有遞歸儲存key, 而是平級的, 用/來分割namespace來分辨type, 并在注冊時把當(dāng)前的entry綁定對應(yīng)的state(通過getNestedState方法).
問題: 如果在不同module注冊了相同type的mutation, 會發(fā)生什么?
回答: 會依次在自己的state中執(zhí)行, 不會影響對方state, 但是會造成錯誤執(zhí)行. (待測試). 所以應(yīng)該在大的項目中盡量使用namespaced[true]的方式, 而不是命名的方式.(但是也是可以利用/來串namespace的, 所以自己type命名避免/)
._modulesNamespaceMap
根據(jù)namespace為key來存放子module
初始化Store VM
這里會新建一個Vue實例并賦值給Store對象的._vm屬性, 把整個vuex的狀態(tài)放進(jìn)去. 并判斷嚴(yán)格模式, 如果為嚴(yán)格模式會在非法改變狀態(tài)的時候拋出異常.
這樣整個構(gòu)建動作已經(jīng)完成了, 那么這個._vm在什么時候用的, 請看下面的章節(jié).
Store對象的屬性&方法
line64 state的getter方法, 會獲取._vm的vue實例的state. 所以我們在vue代碼中this.$store.state.xxx獲取到的東西就是這個vue的實例的數(shù)據(jù).
line68 當(dāng)直接set Store的state時報錯, 只能通過設(shè)置._vm來進(jìn)行.
剩余的方法的是vuex的進(jìn)階用法, 是可以在使用時對vuex狀態(tài)進(jìn)行操作的方法, 詳見文檔
ModuleCollection對象
我們來看下ModuleCollection的構(gòu)造方法.
register根module
調(diào)用了register方法, 把參數(shù)的path設(shè)為根目錄, runtime設(shè)為false.
register方法一開始(l30)就判斷了除state外的屬性的值是否為函數(shù), 若不是則拋出異常.
line33 把module參數(shù)(還是初始的options, 就是{state:{...}, mutations:{...}, actions: {...}}這個)和runtime = false 來構(gòu)建了Module對象(稍后我們看Module對象的構(gòu)造)
line35 把ModuleCollection的root私有變量設(shè)為了剛才使用初始o(jì)ptions新建的Module對象.
line42 如果初始o(jì)ptions有modules這個屬性, 就開始遞歸注冊modules.
遞歸register子module
上面是register的第一個參數(shù)path為空, 也就是root節(jié)點的時候的流程, 在最后一部分(line42)根據(jù)是否當(dāng)前注冊的module含有modules屬性來遞歸注冊, 這部分我們來看一下register的path參數(shù)的行為會把數(shù)據(jù)存成什么結(jié)構(gòu). 以概覽部分的例子的參數(shù)為例(modules含有一個key為catagories)來走一遍代碼流程. (開始~)
被作為子module傳入register方法的參數(shù)應(yīng)該為: path(['catagories']), rawModule(state: {},mutations: {}), runtime(false).
注意到的是, 如果catagories有同級module, 被傳入的path也是一個元素的數(shù)組, 也就是path的意思應(yīng)該類似于從跟到當(dāng)前module的層級, 對于兄弟節(jié)點是無感的.
這里的runtime尚未明白用途, 可能是在別處調(diào)用的. 注冊流程應(yīng)該runtime都為false.
一路看下來, 也是new了一個Module對象, 但是沒有走到line35把new出的對象放到root變量里, 而是在line37~38去尋找當(dāng)前module的父節(jié)點并把自己作為child, append到父節(jié)點上.
這里又腦補了一下數(shù)據(jù)結(jié)構(gòu): path.slice(0, -1)是獲取被pop()一下的path, path[path.lengt - 1]是獲取當(dāng)前path的最后一個元素, 也就是當(dāng)前正在被register的module的key. 所以之前對于path的數(shù)據(jù)結(jié)構(gòu)判斷是正確的.
這里的appendChild和getChild很明顯是Module對象的方法了, 我們再繼續(xù)看Module對象的結(jié)構(gòu).
Module對象
最后來看Module對象的構(gòu)造~
接受2個參數(shù), 一個rawModule, 一個runtime, 第一個參數(shù)是剛才相對于key為catagories的value, 也就是類似{state: xxx, mutations: xxx, actions: xxx}的options.
Module的構(gòu)造函數(shù)只是把參數(shù)拆分, 放入了自己的私有變量, 其中state也接受函數(shù), 并執(zhí)行函數(shù)parse成對象存入私有變量. 其他變量都是原封不動儲存的, 所以vuex給他起名為 rawModule 吧. 剩下那些方法都是顧名思義的, 語法上也簡單, 沒什么好看的.
總結(jié)
那么這樣Store對象的._modules屬性的數(shù)據(jù)結(jié)構(gòu)已經(jīng)很清楚了. 類似于(腦內(nèi)):
{// (ModuleCOllection實例)root: {// (Module實例)_rawModule: {state: {...}, mutations: {...}, // ...(全是options直接傳入)},state: {}, // 進(jìn)行過parse的state, 如果是function會調(diào)用并賦值_children: {catagories: {// (Module實例)},anotherModule: {// (Module實例), 遞歸}}} }總結(jié)一點, 就是這里貯存的數(shù)據(jù)都是"raw"的.
helpers
所有的helper都用了兩個wrap方法, 先來看下這兩個方法的作用.
normalizeNamespace
因為helper是都接受兩種傳參方式:mapState(namespace, map) / mapState(map) , 如果第一個參數(shù)為map時這個函數(shù)把namespace設(shè)為空字符串 , 并且檢查namespace的最后一個字符是不是/, 如果不是的話加上.
normalizeMap
我們map的內(nèi)容也接受兩種語法:
["state1","state2" ]或者是
{state1: state => state.state1,state2: state => state.state2 }這個wrap函數(shù)會把兩種形式都normalize為含有key和val屬性的數(shù)組, 便于統(tǒng)一處理. 也就是上面?zhèn)€兩個形式會轉(zhuǎn)化為:
// Array like [{key: "state1",val: "state1" }, {key: "state2",val: "state2" }] // Object like [{key: "state1",val: state => state.state1 }, {key: "state2",val: state => state.state2 }]mapState
這里做了2個處理:
- 如果namespace不為空, 把state和getter的環(huán)境切換到相對于namespace的環(huán)境(就是之前的makeLocalContext的返回值)
- 如果val為函數(shù)則執(zhí)行, 否則返回state的val為鍵的屬性. 兩者的執(zhí)行環(huán)境皆為處理過namespace的local環(huán)境.
mapActions
mapAction的val語法只接受字符串的, 所以先把val前借namespace, 變?yōu)? namespace/val, 這樣能符合在Store里注冊的entry名.
然后檢查了一下namespace是否被注冊過, 也就是防碰撞, 然后把val作為type, 并把剩余參數(shù)帶著dispatch Store里的action.
參考:
- 美團vuex源碼分析
總結(jié)
以上是生活随笔為你收集整理的从vuex源码分析module与namespaced的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 外网如何访问 Service?- 每天5
- 下一篇: 为什么你应该让你的孩子尽早学习编程