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