天天看點

Vuex源碼解析二之API

“ Vuex資料的存儲、讀取源碼解讀。”

資料讀取

讀取state中的資料

get state () {
  return this._vm._data.$$state
}
function resetStoreVM (store, state, hot) {
  // ...
  store._vm = new Vue({
    data: {
      $$state: state // 此處傳入的state是`root state`
    },
    computed: computed
  });
  // ...
}
           

root state

在vuex初始化安裝子產品遞歸執行

installModule

時已經完成建構,所有子子產品的state都會整合到

root state

中。

function installModule (store, rootState, path, module, hot) {
  // ...
  // set state 建構整顆state樹
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1)) // 找到父子產品state對象
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      // 設定子子產品state:子子產品名稱為key,子子產品state對象為value
      Vue.set(parentState, moduleName, module.state)
    })
  }
}
           

當通過

store.state.count

讀取資料時,傳回

this._vm._data.$$state

,也就是

store._vm._data.$$state.count

當要通路子子產品的state時,如

store.state.countA

是通路不到的,前面需要加上子子產品名稱'a':

store.state.a.countA

。通路

store.state.a.countA

就是通路

store._vm._data.$$state.a.countA

讀取getters中資料

getter

可以認為是

store

中的計算屬性,

getter

的傳回值會根據它的依賴被緩存起來,且隻有當它的依賴值發生了改變才會被重新計算。

function resetStoreVM (store, state, hot) {
  // ...
  var wrappedGetters = store._wrappedGetters;
  var computed = {};
  forEachValue(wrappedGetters, function (fn, key) {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    computed[key] = partial(fn, store);
    Object.defineProperty(store.getters, key, {
      get: function () { return store._vm[key]; },
      enumerable: true // for local getters
    });
  });
  // ...
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed: computed
  });
  //...
}
getters: {
  doubleCount (state) {
    return state.count * 2
  }
}
           

通過屬性通路:

通路 store.getters.doubleCount,也就是通路 store._vm['doubleCount'],

doubleCount 作為vue執行個體的計算屬性被通路,會執行 partial(fn, store) 得到值。

partial(fn, store) 即執行 _wrappedGetters 中對應key為 doubleCount 的getter方法。

回頭看一下_wrappedGetters中傳回rawGetter,rawGetter就是我們自定義的getter方法,可以傳入四個參數,除了全局的state和getter外,我們還可以通路到目前module下的state和getter。

function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
           

由此可見,如果想通過屬性通路有命名空間的子子產品下的getter資料,就需要這麼寫store.getters['a/sumWithRootCount']

資料存儲

commit mutation

更改Vuex的store中的狀态的唯一方法是送出mutation。每個 mutation都有一個字元串的事件類型 (type)和一個回調函數 (handler)。這個回調函數就是我們實際進行狀态更改的地方,并且它會接受 state 作為第一個參數:

當要調用這個

increment

函數時,你需要調用 store.commit 方法:

store.commit('increment')
           

下面看一下commit方法的實作

commit (_type, _payload, _options) {
  // check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)
  const mutation = { type, payload }
  const entry = this._mutations[type]
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  // ...
}
           

unifyObjectStyle

對參數進行處理,第一個參數支援傳入對象。

// commit支援對象風格的送出方式
store.commit({
  type: 'increment',
  amount: 10
})
function unifyObjectStyle (type, payload, options) {
  // 整個對象都作為載荷傳給 mutation 函數
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  if (__DEV__) {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }
  return { type, payload, options }
}
           

讀完store初始化後,我們知道_mutations中存儲了所有子產品的mutation函數,以數組的形式存儲,有命名空間的子子產品對應的key會加上子產品命名空間的字首。

const mutation = { type, payload }
const entry = this._mutations[type]
// 沒有找到該type的對應函數數組
if (!entry) {
  if (__DEV__) {
    console.error(`[vuex] unknown mutation type: ${type}`)
  }
  return
}
this._withCommit(() => {
  entry.forEach(function commitIterator (handler) {
    handler(payload)
  })
})
           

根據傳入的type,在_mutations中找到對應的函數數組

entry

,周遊擷取到每個handler并執行,這裡的handler就是我們自定義的mutation方法,該方法可以傳入兩個參數,第一個是目前子產品的state

local.state

, 第二個是payload。

// 這裡傳入的handler是我們自定義的mutation函數
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}
           

我們看到entry周遊外層包了_withCommit方法,

this._withCommit

的作用,看下面兩段代碼:

if (store.strict) {
  enableStrictMode(store)
}
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}
​
_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}
           

_withCommit 就是對 fn 包裝了一個環境,確定在 fn 中執行任何邏輯的時候 this._committing = true

如果設定為嚴格模式 store.strict = true,那麼 store._vm 會添加一個 wathcer來觀測this._data.$$state 的變化,也就是當 store.state 被修改的時候, store._committing必須為true,否則在開發階段會報警告。

是以_withCommit的作用就是確定嚴格模式下在執行自定義的 mutation 方法時不會報警告。

dispatch action

Action 類似于 mutation,不同在于:

  • Action 送出的是 mutation,而不是直接變更狀态。
  • Action 可以包含任意異步操作。

看一下我們例子中的

action

// root
actions: {
  incrementIfOdd ({ state, commit}) {
    if (state.count % 2 === 1) {
      commit('increment')
    }
  }
},
// moduleA
actions: {
  incrementIfOddOnRootSum ({ state, commit, rootState }) {
    if ((state.countA + rootState.count) % 2 === 1) {
      commit('increment')
    }
  }
}
           

我們通過store.dispatch送出一個action。

store.dispatch('incrementIfOdd')
           
看一下dispatch方法的實作:      
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)
  const action = { type, payload }
  const entry = this._actions[type]
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }
  // ...
  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
  return new Promise((resolve, reject) => {
    result.then(res => {
      // ...
      resolve(res)
    }, error => {
      // ...
      reject(error)
    })
  })
}
           

同mutation一樣,我們在初始化過程中在actions中存儲了所有子產品的action函數。根據傳入的type,找到對應的函數數組,周遊執行handler回調函數。我們看一下執行handler回調函數實際上是執行了

wrappedActionHandler(payload)

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 如果傳回值不是Promise對象,要resolve出去一個Promise對象
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}
           

我們觀察一下handler函數(也就是自定義的action函數)的傳參。

第一個參數傳入一個對象,包含了目前子產品

local

下的 dispatch、commit、getters、state,以及全局的 rootState 和 rootGetters,是以我們定義的 action 函數能拿到目前子產品下的 commit 方法。

{
  dispatch: local.dispatch,
  commit: local.commit,
  getters: local.getters,
  state: local.state,
  rootGetters: store.getters,
  rootState: store.state
}
           

第二個是載荷payload,不多說。

從中可看出,

action

比我們自己寫一個函數執行異步操作然後送出

mutation

的好處是在于它可以在參數中擷取到目前子產品的一些方法和狀态。

輔助函數

大部分場景下我們可以用

this.$store

去通路store這個全局執行個體對象。但在一個元件中有多個狀态要讀取或者多個狀态需要進行設定時,使用map***的輔助函數會更友善。vuex提供這樣的文法糖。接下來我們分别看一下mapState、mapGetters、mapMutations、mapActions。

mapState

用法:

mapState 輔助函數幫助我們生成計算屬性

// 在單獨建構的版本中輔助函數為 Vuex.mapState              import { mapState } from 'vuex'              export default {              // ...              computed: mapState({              // 箭頭函數可使代碼更簡練              count: state => state.count,              // 傳字元串參數 'count' 等同于 `state => state.count`              countAlias: 'count',              // 為了能夠使用 `this` 擷取局部狀态,必須使用正常函數              countPlusLocalState (state, getters) {              return state.count + this.localCount              }              })              }
           

當映射的計算屬性的名稱與 state 的子節點名稱相同時,我們也可以給 mapState 傳一個字元串數組。

computed: mapState([              // 映射 this.count 為 store.state.count              'count'              ])
           

mapState傳回一個對象,傳入的參數必須是一個對象或者一個數組。

/**              * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.              * @param {Function} fn              * @return {Function}              */              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)              }              }              /**              * Normalize the map              * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]              * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]              * @param {Array|Object} map              * @return {Object}              */              function normalizeMap (map) {              if (!isValidMap(map)) {              return []              }              return Array.isArray(map)              ? map.map(key => ({ key, val: key }))              : Object.keys(map).map(key => ({ key, val: map[key] }))              }              export const mapState = normalizeNamespace((namespace, states) => {              const res = {}              if (__DEV__ && !isValidMap(states)) {              console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')              }              normalizeMap(states).forEach(({ key, val }) => {              res[key] = function mappedState () {              let state = this.$store.state              let getters = this.$store.getters              if (namespace) {              const module = getModuleByNamespace(this.$store, 'mapState', namespace)              if (!module) {              return              }              state = module.context.state              getters = module.context.getters              }              return typeof val === 'function'              ? val.call(this, state, getters)              : state[val]              }              // mark vuex getter for devtools              res[key].vuex = true              })              return res              })
           

執行normalizeNamespace方法傳回的函數,normalizeNamespace的作用是對傳參做一次處理,如果未傳命名空間,那麼第一個參數指派為空,将傳入的第一個參數指派到map。将map作為states參數傳入。

normalizeMap先格式化map。

分數組和對象兩種情況

normalizeMap([1, 2, 3])               // 傳回值 [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]              normalizeMap({a: 1, b: 2, c: 3})              // 傳回值 [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]              mapState([              // 映射 this.count 為 store.state.count              'count'              ])              // 傳入的`['count']`數組,會被轉換為`[{key: 'count', val: 'count'}]`              mapState({              count: state => state.count,              countAlias: 'count'              })              // 傳回值              // [              //   {              //     key: 'count',              //     val: state => state.count              //   },              //   {              //     key: 'countAlias',              //     val: 'count'              //   }              // ]
           

周遊這個對象數組,建立res對象,res對象的屬性名為周遊的數組中對象的key,屬性值是mappedState函數,mappedState函數内部可以擷取到store.state和store.getters,判斷對象的val是函數則執行函數傳入state和getters,否則如果是字元串則直接擷取到state[val]

當使用mapState來綁定帶命名空間的子產品時,寫起來可能比較繁瑣:

​computed: {
  ...mapState({
    countA: state => state.a.countA,
    pointX: state => state.a.pointX,
    pointY: state => state.a.pointY,
  })
}
           
mapState支援傳入命名空間,可以這樣寫:      
computed: {              ...mapState('a', {              countA: state => state.countA,              pointX: state => state.pointX,              pointY: state => state.pointY,              })              }                
如果存在命名空間,那麼state和getters就會變成目前命名空間子產品下的state和getters。      
if (namespace) {              const module = getModuleByNamespace(this.$store, 'mapState', namespace)              if (!module) {              return              }              state = module.context.state              getters = module.context.getters              }
           

通過getModuleByNamespace函數拿到目前module,因為store初始化時_modulesNamespaceMap中存儲了命名空間映射到子產品的映射表。

/**
 * Search a special module from store by namespace. if module not exist, print error message.
 * @param {Object} store
 * @param {String} helper
 * @param {String} namespace
 * @return {Object}
 */
function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}
// 初始化安裝子產品時,存儲了映射表
function installModule (store, rootState, path, module, hot) {
  // ...
  const namespace = store._modules.getNamespace(path)
  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }
  // ...
}
           

了解了mapState後,再了解以下的輔助函數就比較簡單了。原理都類似。

mapGetters

用法:

mapGetters 輔助函數僅僅是将 store 中的 getter 映射到局部計算屬性:

import { mapGetters } from 'vuex'
export default {
  // ...
  computed: {
  // 使用對象展開運算符将 getter 混入 computed 對象中
    ...mapGetters([
      'doubleCount'
      // ...
    ])
  }
}
           

如果想将一個getter屬性另取一個名字,使用對象形式:

...mapGetters({
  // 把 `this.doneCount` 映射為 `this.$store.getters.doubleCount`
  root_doubleCount: 'doubleCount',
  a_doubleCount: 'a/doubleCount',
  b_doubleCount: 'b/doubleCount',
  a_sumWithRootCount: 'a/sumWithRootCount',
  b_sumWithRootCount: 'b/sumWithRootCount',
})
           

mapGetters也支援傳入命名空間:

...mapGetters('a', [
  doubleCount,
  sumWithRootCount
])
           

看一下mapGetters函數的實作,同樣傳回一個對象,傳入參數也必須是對象或者數組。

/**
 * Reduce the code which written in Vue.js for getting the getters
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} getters
 * @return {Object}
 */
export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  if (__DEV__ && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (__DEV__ && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
           

周遊normalizeMap後的對象數組,建立res對象,res對象的屬性名為周遊的數組中對象元素的key,屬性值是mappedGetter函數,如果有命名空間,val就拼接上namespace字首,直接取

this.$store.getters[val]

傳回就行。

mapMutations

用法:

使用 mapMutations 輔助函數将元件中的 methods 映射為 store.commit 調用(前提需要在根節點注入 store)。

mapMutations也支援傳入對象或者數組,如果需要換個名字就用對象的形式。

import { mapMutations } from 'vuex'
export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射為 `this.$store.commit('increment')`
      // `mapMutations` 也支援載荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射為 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射為 `this.$store.commit('increment')`
    })
  }
}
​
​
/**
 * Reduce the code which written in Vue.js for committing the mutation
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept anthor params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
 * @return {Object}
 */
export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  if (__DEV__ && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})
           

commit.apply(this.$store, [val].concat(args))

可以看出還支援傳入額外的參數args。

mapMutations同樣支援namespace,如果存在命名空間,那麼commit方法會變成目前命名空間子產品(子子產品)下的commit方法。

mapActions

用法和mapMutations一樣:

使用 mapActions 輔助函數将元件的 methods 映射為 store.dispatch 調用(需要先在根節點注入 store)

import { mapActions } from 'vuex'
export default {
  // ...
  methods: {
    ...mapActions([
      'incrementIfOdd', // 将 `this.incrementIfOdd()` 映射為 `this.$store.dispatch('incrementIfOdd')`
    ]),
    ...mapActions({
      addIfOdd: 'incrementIfOdd' // 将 `this.addIfOdd()` 映射為 `this.$store.dispatch('incrementIfOdd')`
    })
  }
}
           

mapActions實作和mapMutations一模一樣,隻是把commit變成了dispatch,支援namespace,也支援傳入額外的參數args。

/**
 * Reduce the code which written in Vue.js for dispatch the action
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function.
 * @return {Object}
 */
export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  if (__DEV__ && !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})
           

createNamespacedHelpers

可以通過使用 createNamespacedHelpers 建立基于某個命名空間輔助函數。它傳回一個對象,對象裡有新的綁定在給定命名空間值上的元件綁定輔助函數:

/**
 * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
 * @param {String} namespace
 * @return {Object}
 */
export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})
           

它的用法:

import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('a')
export default {
  computed: {
    // 在 `a` 中查找
    ...mapState({
      countA: state => state.countA,
      pointX: state => state.pointX,
      pointY: state => state.pointY
    })
  },
  methods: {
    // 在 `a` 中查找
    ...mapActions([
      'incrementIfOddOnRootSum'
    ])
  }
}
           

子產品動态注冊

store.registerModule

import Vuex from 'vuex'
const store = new Vuex.Store({ /* 選項 */ })
// 注冊子產品 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注冊嵌套子產品 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})
           

注冊後,通過 store.state.myModule 和 store.state.nested.myModule 通路子產品的狀态。

registerModule的實作:

registerModule (path, rawModule, options = {}) {
  if (typeof path === 'string') path = [path]
  if (__DEV__) {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(path.length > 0, 'cannot register the root module by using registerModule.')
  }
  this._modules.register(path, rawModule)
  installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  // reset store to update getters...
  resetStoreVM(this, this.state)
}
           

第一個參數必傳,不能傳空,傳數組或者字元串。

this._modules.register

執行

ModuleCollection

register

方法,往子產品樹

_modules

中加入一個新的子子產品。然後,通過

installModule

安裝子產品,通過

resetStoreVM

重新生成一個vue執行個體(也會銷毀原vue執行個體),更新

state

getters

export default class ModuleCollection {
  // ...
  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) { // 根子產品
      this.root = newModule
    } else {
      // 父module和子module建立父子關系
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }
    // register nested modules 遞歸注冊嵌套的module
    if (rawModule.modules) {
      // 比方這裡有個名為a的子子產品,path就是['a'], 那麼path.length不為0,執行建立父子關系的邏輯
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
      // 注意這裡用了concat方法,該方法不會改變原path,而僅僅會傳回被連接配接數組的一個副本。
    }
  }
  // ...
}
           

store.unregisterModule(moduleName) 動态解除安裝子產品

import Vuex from 'vuex'
const store = new Vuex.Store({ /* 選項 */ })
// 解除安裝子產品 `myModule`
store.unregisterModule('myModule')
// 解除安裝嵌套子產品 `nested/myModule`
store.unregisterModule(['nested', 'myModule'])
​
​
unregisterModule (path) {
  if (typeof path === 'string') path = [path]
  if (__DEV__) {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
  }
  this._modules.unregister(path)
  this._withCommit(() => {
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1])
  })
  resetStore(this)
}
           

第一步,先将在_modules子產品樹中擴充的子子產品通過

unregister

移除。

export default class ModuleCollection {
  // ...
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  unregister (path) {
    // 擷取目前子產品的父子產品
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    const child = parent.getChild(key)
    if (!child) {
      if (__DEV__) {
        console.warn(
          `[vuex] trying to unregister module '${key}', which is ` +
          `not registered`
        )
      }
      return
    }
    if (!child.runtime) {
      return
    }
    parent.removeChild(key)
  }
  // ...
}
export default class Module {
  // ...
  removeChild (key) {
    delete this._children[key]
  }
  // ...
}
           

邏輯比較簡單,先通過

this.get()

擷取到目前子產品的父子產品,父子產品通過removeChild移除對應key的子子產品。

第二步,

Vue.delete(parentState, path[path.length - 1])

把root state中對應的state也移除。

第三步,

resetStore(this)

function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}
           

resetStore

store

下的對應存儲的

_actions

_mutations

_wrappedGetters

 和 

_modulesNamespaceMap

 都清空,然後重新執行 installModule安裝所有子產品以及

resetStoreVM

重置

store._vm

以上就是動态解除安裝子產品的步驟。

總結

我們回顧了一下store初始化的過程,并且分析了vuex中進行資料讀取和存儲的方法,還分析vuex提供的輔助函數和子產品動态注冊和解除安裝。

mapXXX

的設計讓我們在使用api的時候更加友善,設計思路非常值得我們學習。

繼續閱讀