從vuex源碼分析module與namespaced

使用vue已經有半年有餘, 在各類正式非正式項目中用過, 開始專一於業務比較多, 用到如今也碰見很多由於理解不深致使的問題. 有問題就有找緣由的勇氣, 因此帶着問題搞一波.html

帶着問題看源碼

因此來整理了一下使用過程當中不注意或者不規範, 或者簡化寫法的奇技淫巧, 會結合文檔的說明和實際的問題來看看源碼, 問題:vue

  • module在vuex裏實際的數據結構
  • namespaced在vuex裏實際的數據結構
  • mapState, mapActions等helper的正確用法(配合module/namespaced), 或者是否存在更多騷用法
  • mutation中賦值/觸發state變化原理

源碼分析

看的源碼版本爲vuex2.3.1git

咱們使用vuex多是相似:github

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核心對象.vuex

Store對象分析

line6: 本地vue變量, 在install時會被賦值, 以後會經過vue是否爲undefined來判斷是否installapi

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對象.app

點擊跳轉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.

line271makeLocalContext函數整理了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的數據結構判斷是正確的.

這裏的appendChildgetChild很明顯是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爲含有keyval屬性的數組, 便於統一處理. 也就是上面個兩個形式會轉化爲:

// 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.


參考:

原文地址

相關文章
相關標籤/搜索