我看Vuex(二)

煎蛋_發表於2018-02-22

書接上回,上回書說了 vuex 的安裝、以及 store 建構函式,下面我們來講後面部分

images 1

收集 modules

vuex 允許我們自定義多個模組,防止應用的所有狀態會集中到一個比較大的物件,導致 store 就變的臃腫了。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
複製程式碼

這是如何實現的呢?

default

在store的建構函式裡面有這樣一段程式碼:this._modules = new ModuleCollection(options),他就是用來收集使用者自定義的 modules,該函式位於 module/module-collection.js

現在假設我們的 modules 做如下設定:

store = {
  modules: {
    moduleA: {
      state: {},

      modules: {
        moduleC: {
          state: {},
          actions: {}
        }
      }

    },

    modulesB: {
      state: {},
      mutations: {
        // xxx
      }
    }

  }
}
複製程式碼

模組關係圖如下:

image

來到 ModuleCollection 的建構函式,很簡單,呼叫其 register 方法,並傳入三個引數

register(path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }

    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // 整個傳入store 裡的 option 為一個大的 module ,
      // 再對option 裡的 modules 屬性進行遞迴註冊為 module
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // 這裡的 concat 並不改變原來的陣列,所以如果是同級的 module ,那麼他還是有著相同的父級
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
複製程式碼

現在我們一步一步來分析。 先講一下 register 裡的對傳入模組結構的斷言,呼叫 assertRawModule(path, rawModule) 以確保 getters/mutations/actions 依次符合 函式/函式/(物件或者函式) 的結構,這部分程式碼並不難,但是作者的這種編碼習慣非常值得學習,詳見 assertRawModule

images 2

回到正題

第一次 呼叫 this.register([], rawRootModule, false)

此時傳入 register 函式的 path空陣列,rawModule 為最外層的 store 物件,即可以理解為 根module,runtime 為 false

image

接著呼叫 new Module(rawModule, runtime) 例項化這個 根module

constructor (rawModule, runtime) {
    this.runtime = runtime
    // 儲存子 modules
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    // 儲存原始的這個傳進來的 module
    this._rawModule = rawModule
    const rawState = rawModule.state  // 獲取模組的 state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} // state 最終是一個物件
  }
複製程式碼

回到 register 函式,如果此時的 path 為 空陣列,那麼就將此模組設定為 整個狀態樹的 根模組,即this.root= 根 module,這裡我們的 path=[],所以它是 根 module,不走 else

來到後面,判斷該模組是否有 modules屬性,即有子模組,有則繼續迴圈註冊子模組,我們這裡有 moduleAmoduleB ,所以繼續註冊

// util.js
function forEachValue (obj, fn) {
    Object.keys(obj).forEach(key => fn(obj[key], key))
  }
複製程式碼

第二次呼叫 this.register(path.concat(key), rawChildModule, runtime)

此時傳入 register 函式的 pathpath.concat(key),即 path =['moduleA'] ,rawModule 為 moduleA 物件,runtime 為 false

注:path.concat(key) 並不改變原始的 path,它返回一個新的陣列,所以 根module的path棧 還是空,這一點很重要

image

繼續重複第一步的步驟,不同的是,例項化完 moduleA 後,由於此時的 path =['moduleA'],所以它走 else

else {
      const parent = this.get(path.slice(0, -1)) 
      parent.addChild(path[path.length - 1], newModule)
    }
複製程式碼

path.slice(0, -1) 返回 path 陣列以外的其他元素,不改變原始陣列,所以等價於 this.get([])

// 作用:獲取當前的模組的父模組
// 傳入的 path 是從根模組到父模組的一條鏈路
get(path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
複製程式碼

this.root為前面的 根 module,而 path 是空,所以 parent=根 module ,然後執行 parent.addChild(path[path.length - 1], newModule),此時獲取 path 棧頂元素("moduleA")作為 key ,和 例項 moduleA 作為 value ,加入到 父模組(根 module)的子元素物件中

由於 moduleA 還有子模組,所以繼續遞迴 子模組

第三次呼叫 this.register(path.concat(key), rawChildModule, runtime)

此時傳入 register 函式的 pathpath.concat(key),即 path =['moduleA'] ,rawModule 為 moduleA 物件,runtime 為 false

image

繼續上面步驟 來到 this.get,這是傳入的引數是 ['moduleA'],即 moduleC 的父模組 moduleA。由於 根module 儲存了 moduleA,所以通過這種類似於鏈的方式來獲取 父模組,同理將 moduleC 加入 moduleA 的子模組物件中 至此,第一條鏈就講完了,

返回到 根moduleforEachValue 迴圈中,這裡我們講到,他的 path 還是空,這就體現了 使用 concat 方法的好處與機智。 所以與處理 moduleA 的過程一模一樣

第四次呼叫 this.register(path.concat(key), rawChildModule, runtime)

此時傳入 register 函式的 pathpath.concat(key),即 path =['moduleB'] ,rawModule 為 moduleB 物件,runtime 為 false

image

終於將 this._modules = new ModuleCollection(options) 的過程分析完畢了 最終的 this._modules.root(不包括方法) 如下圖所示

image

總的看下來挺佩服作者的思維以及處理方式的

3

看著挺長的了,其實就是多了幾個迴圈過程的講解,所以要不要再翻篇呢?呢?呢?????

4
5

回到 store.js 的建構函式

const state = this._modules.root.state  // 將 "根模組物件的 state" (即最外層store的state物件)賦予 state ,
複製程式碼

installModule(this, state, [], this._modules.root)

初始化根模組,並且遞迴註冊子模組,並且收集所有模組的 getters

function installModule(store, rootState, path, module, hot) {
// hot 當動態改變 modules 或者熱更新的時候為 true.
  const isRoot = !path.length // 判斷是否是根模組
  const namespace = store._modules.getNamespace(path)  // 獲取s使用的名稱空間

  // register in namespace map
  if (module.namespaced) { 
    // 如果名稱空間存在,就在store 物件中建立 namespace 到模組的對映
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) { // 如果不是根模組以及 hot = false,這裡我們是根模組,所以我們先放一放,跳到下一步
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 跳到下面先看看makeLocalContext,
  // 又跳!!!放心,,不用跳多遠,也就是下一個 issue,,,摳鼻.gif
  const local = module.context = makeLocalContext(store, namespace, path)  
  
  // 註冊各個模組的 mutaations 方法到 store._mutations 中,每個type對應一個陣列
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 註冊各個模組的 actions 到store._actions
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  // 註冊各個模組的 getters 到store._wrappedGetters
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
 
  
  module.forEachChild((child, key) => { // 遞迴子模組
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
複製程式碼

上面出現的有關函式由於排版以及篇幅原因,我放到了 我看Vue(三) 中。

總結下,installModule 都幹了些神馬:

  1. 獲取名稱空間
  2. 獲取本模組的 context 屬性,裡面包含了本模組的 dispatch/commit/getter/state 等屬性或方法
  3. 將 各個模組按照名稱空間將 mutations/getters/actions 加入到全域性的 _mutations /_wrappedGetters/_actions

接下來我們簡單講講 在元件裡面呼叫 dispatch、commit 的過程

dispatch/commit

commit(_type, _payload, _options) {
    // check object-style commit
    // 這裡就是獲取正確的 type / payload /options
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = {
      type,
      payload
    }

    // 獲取觸發的type  對應的 mutation
    const entry = this._mutations[type]
    if (!entry) {  // 如果不存在,給出警告
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    this._withCommit(() => {  // 由於entry是一個陣列,所以逐個執行,並傳入負載
      entry.forEach(function commitIterator(handler) {
        handler(payload)
      })
    })

    // 觸發 訂閱函式
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

dispatch(_type, _payload) {  // 基本和commit 一樣
// xxx
  }

複製程式碼

看完上面的程式碼,我有些疑問,這個 store.statestore.getter 是哪來的?

images 3

resetStoreVM(this, state)

function resetStoreVM(store, state, hot) {
  const oldVm = store._vm // 之前的 vue 例項

  // bind store public getters
  store.getters = {}  // 終於找到你 
  const wrappedGetters = store._wrappedGetters  // 前面說過的各個模組的 getters 集合
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)    // 收集各個 getter,等會傳入 computed ,以此做到響應式
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],  // 因為利用了計算屬性,所以各個 getter 就變成了 vue 例項的屬性
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent //// 是否取消Vue 所有的日誌與警告
  Vue.config.silent = true

  // 重新 new 
  store._vm = new Vue({
    data: {
      $$state: state  // 這裡雖然是 $$state,但是利用store.state時獲取的就是它
    },
    computed
  })

  /* get state() {
        return this._vm._data.$$state
  }*/

  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
     // 解除舊vm的state的引用,以及銷燬舊的Vue物件
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
   
    Vue.nextTick(() => oldVm.$destroy())
  }
}
複製程式碼

解決了 store.getter了,那麼 store.state是如何來的呢? 還記不記得第一次讓你跳的地方,沒錯就是 installModule

 if (!isRoot && !hot) { // 如果不是根模組以及 hot = false,這裡我們是根模組,所以我們先放一放,跳到下一步
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
複製程式碼

這裡我們就來看 Vue.set(parentState, moduleName, module.state)。 它的作用是在父模組的state 屬性上新增上本模組的state,還是按照一開始我們那種依賴關係來看:

image

這樣我們就不難理解 getNestState 裡面為什麼可以如此獲取 state

好了,vuex 核心大致的內容就是這些,後面在 我看Vuex(三) 中我會解釋下其他一些函式的作用

原文地址: 我看Vuex(二)

如有不當,歡迎指出交流,謝謝 ^_^

images 2

相關文章