VUEX源碼學習筆記(第5~6章 共6章)

第五章 輔助函數

在第一章咱們曾經說過:html

VUEX採用的是典型的IIFE(當即執行函數表達式)模式,當代碼被加載(經過 <script>Vue.use())後,VUEX會返回一個對象,這個對象包含了 Store類、 install方法、 mapState輔助函數、 mapMutations輔助函數、 mapGetters輔助函數、 mapActions輔助函數、 createNamespacedHelpers輔助函數以及當前的版本號 version

本章就將詳細講解mapStatemapMutationsmapGettersmapActionscreateNamespacedHelpers這5個輔助和函數。vue

5.1 主要輔助函數

5.1.1 mapState

若是你在使用VUEX過程當中使用過mapState輔助函數將state映射爲計算屬性你應該會爲它所支持的多樣化的映射形式感到驚訝。咱們不妨先來看看官方文檔對它的介紹:git

圖片描述

若是你深刻思考過你可能會有疑問:VUEX的mapState是如何實現這麼多種映射的呢?若是你如今還不明白,那麼跟隨咱們來一塊兒看看吧!github

mapState輔助函數定義在VUEX源碼中的790 ~ 815 行,主要是對多種映射方式以及帶命名空間的模塊提供了支持,咱們來看看它的源碼:vuex

var mapState = normalizeNamespace(function (namespace, states) {
  var res = {};
  normalizeMap(states).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedState () {
      var state = this.$store.state;
      var getters = this.$store.getters;
      if (namespace) {
        var 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
});

能夠看到,mapState函數其實是以函數表達式的形式的形式定義的,它的實際函數是normalizeNamespace函數,這個函數會對mapState函數的輸入參數進行歸一化/規範化處理,其最主要的功能是實現了支持帶命名空間的模塊,咱們來看一下它的實現:數組

function normalizeNamespace (fn) {
  return function (namespace, map) {
    if (typeof namespace !== 'string') {
      map = namespace;
      namespace = '';
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/';
    }
    return fn(namespace, map)
  }
}

能夠看到mapState其實是中間的那段函數:promise

return function (namespace, map) {
  if (typeof namespace !== 'string') {
    map = namespace;
    namespace = '';
  } else if (namespace.charAt(namespace.length - 1) !== '/') {
    namespace += '/';
  }
  return fn(namespace, map)
}

它實際接收namespace, map能夠接收兩個參數,也能夠只接受一個map參數。app

  1. 當用戶只提供了一個map參數時。這種狀況因爲類型不是string,會直接將其做爲map,並將namespace置爲空字符串。這種狀況其實就和官方文檔的使用方式相匹配。咱們能夠看看官方文檔的示例就是隻提供了一個map參數,並無提供namespace參數。
  2. 當用戶提供了namespace, map兩個參數時,但namespace不是字符串類型。實際上這種狀況會和狀況1作同樣的處理,傳進去的第一個參數做爲實際的map,第二個參數會被忽略。
  3. 當用戶提供了namespace, map兩個參數時,且namespace是字符串類型。這種狀況下會根據字符串最末一位字符串是不是反斜線(/)來區別對待。最終程序內部會將namespace統一處理成最後一位是反斜線(/)的字符串。

由以上分析咱們能夠知道,上述官方文檔在此處的示例其實並不完善,該實例並無指出能夠經過提供模塊名稱做爲mapState的第一個參數來映射帶命名空間的模塊的state函數

咱們舉個例子看一下:學習

const moduleA = {
  namespaced: true,//帶命名空間
  state: { count1: 1, age1: 20 }
}
const store = new Vuex.Store({
  state() {
    return {
        count: 0, age: 0
    }
  },
  modules: {
    a: moduleA
  }
})
var vm = new Vue({
  el: '#example',
  store,
  computed: Vuex.mapState('a', {// 映射時提供模塊名做爲第一個參數
     count1: state => state.count1,
     age1: state => state.age1,
  })
})
console.log(vm)

其輸出以下:

圖片描述

傳遞模塊名稱後,咱們只能映射帶命名空間的該模塊的state,若是該模塊不帶命名空間(即沒有設置namespace屬性)、或者對於其它名字的模塊,咱們是不能映射他們的state的。

傳遞了模塊名稱,但該模塊不帶命名空間,嘗試對其進行映射:

const moduleA = {
  // namespaced: true,
  state: { count1: 1, age1: 20 }
}
const store = new Vuex.Store({
  state() {
    return {
        count: 0, age: 0
    }
  },
  modules: {
    a: moduleA
  }
})
var vm = new Vue({
  el: '#example',
  store,
  computed: Vuex.mapState('a', {
     count1: state => state.count1,
     age1: state => state.age1,
  })
})
console.log(vm)

傳遞了模塊名稱,但嘗試映射其它模塊的state:

const moduleA = {
  namespaced: true,
  state: { count1: 1, age1: 20 }
}
const store = new Vuex.Store({
  state() {
    return {
        count: 0, age: 0
    }
  },
  modules: {
    a: moduleA
  }
})
var vm = new Vue({
  el: '#example',
  store,
  computed: Vuex.mapState('a', {
     count1: state => state.count,
     age1: state => state.age,
  })
})
console.log(vm)

這兩種狀況下的輸出結果都會是undefined:

圖片描述

講完了mapState的參數,咱們接着回過頭來看看mapState的實現。這裏重複粘貼一下前面有關mapState定義的代碼:

function normalizeNamespace (fn) {
  return function (namespace, map) {
    if (typeof namespace !== 'string') {
      map = namespace;
      namespace = '';
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/';
    }
    return fn(namespace, map)
  }
}

咱們能夠看到,在歸一化/規範化輸入參數後,mapState函數其實是返回了另一個函數的執行結果:

return fn(namespace, map)

這個fn就是以函數表達式定義mapState函數時的normalizeNamespace 函數的參數,咱們在前面已經見到過。再次粘貼其代碼以便於分析:

function (namespace, states) {
  var res = {};
  normalizeMap(states).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedState () {
      var state = this.$store.state;
      var getters = this.$store.getters;
      if (namespace) {
        var 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
};

粗略來看,這個函數會從新定義map對象的key-value對,並做爲一個新的對象返回。咱們來進一步具體分析一下。

該函數首先調用normalizeMap函數對state參數進行歸一化/規範化。normalizeMap函數定義在VUEX源碼的899 ~ 903行,咱們來具體看看它的實現:

function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(function (key) { return ({ key: key, val: key }); })
    : Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); })
}

該函數實際上意味着mapState函數的map參數同時支持數組和對象兩種形式。

  1. 若是是數組,則會遍歷數組元素,將數組元素轉成{value: value}對象。
  2. 若是是對象,則會遍歷對象key,以key-value構成{key: value}對象。

這兩種形式最終都會獲得一個新數組,而數組元素就是{key: value}形式的對象。

這也與官方文檔的描述相印證,官方文檔的既提供了mapState函數的map參數是對象的例子,也提供了參數是數組的例子。

回過頭來看,normalizeMap(states)函數執行完後會遍歷,針對每個對象元素的value作進一步的處理。它首先拿的是根實例上掛載的store模塊的state:

var state = this.$store.state;
var getters = this.$store.getters;

而若是mapState函數提供了命名空間參數(即模塊名),則會拿帶命名空間模塊的state:

if (namespace) {
  var module = getModuleByNamespace(this.$store, 'mapState', namespace);
  if (!module) {
    return
  }
  state = module.context.state;
  getters = module.context.getters;
}

這其中會調用一個從根store開始,向下查找對應命名空間模塊的方法getModuleByNamespace,它定義在VUEX源碼的917 ~ 923 行:

function getModuleByNamespace (store, helper, namespace) {
  var module = store._modulesNamespaceMap[namespace];
  if ("development" !== 'production' && !module) {
    console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace));
  }
  return module
}

由於咱們在實例化Store類的時候已經把全部模塊以namespace的爲key的形式掛載在了根store實例的_modulesNamespaceMap屬性上,因此這個查詢過程只是一個對象key的查找過程,實現起來比較簡單。

回過頭來繼續看mapState函數中「normalizeMap(states)函數執行完後會遍歷,針對每個對象元素的value作進一步的處理」的最後的執行,它會根據原始的value是不是function而進一步處理:

  1. 若是是否是function,則直接拿對應模塊的state中key對應的value。
  2. 若是是function,那麼將會執行該function,而且會將state, getters分別暴露給該function做爲第一個和第二個參數。

第二種狀況在前述官方文檔的例子中也有所體現:

// 爲了可以使用 `this` 獲取局部狀態,必須使用常規函數
countPlusLocalState (state) {
  return state.count + this.localCount
}

但這個官方文檔例子並不完整,它並無體現出還會暴露出getters參數,實際上,上述例子的完整形式應該是這樣子的:

// 爲了可以使用 `this` 獲取局部狀態,必須使用常規函數
countPlusLocalState (state, getters) {
  return state.count + this.localCount + getters.somegetter
}

5.1.2 mapMutations

與mapState能夠映射模塊的state爲計算屬性相似,mapMutations也能夠將模塊的mutations映射爲methods,咱們來看看官方文檔的介紹:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      // 將 `this.increment()` 映射爲 `this.$store.commit('increment')`
      'increment',

      // `mapMutations` 也支持載荷:
      // 將 `this.incrementBy(amount)` 映射爲 `this.$store.commit('incrementBy', amount)`
      'incrementBy' 
    ]),
    ...mapMutations({
      add: 'increment' // 將 `this.add()` 映射爲 `this.$store.commit('increment')`
    })
  }
}

一樣咱們來看看它是如何實現的,它的實現定義在VUEX源碼中的817 ~ 841 行:

var mapMutations = normalizeNamespace(function (namespace, mutations) {
  var res = {};
  normalizeMap(mutations).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedMutation () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      var commit = this.$store.commit;
      if (namespace) {
        var 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
});

和mapState的實現幾乎徹底同樣,惟一的差異只有兩點:

  1. 提交mutaion時能夠傳遞載荷,因此這裏有一步是拷貝載荷。
  2. mutation是用來提交的,因此這裏拿的是commit。

咱們來具體分析一下代碼的執行:

首先是拷貝載荷:

var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];

而後是拿commit,若是mapMutations函數提供了命名空間參數(即模塊名),則會拿帶命名空間模塊的commit:

var commit = this.$store.commit;
if (namespace) {
  var module = getModuleByNamespace(this.$store, 'mapMutations', namespace);
  if (!module) {
    return
  }
  commit = module.context.commit;
}

最後則會看對應mutation的value是否是函數:

  1. 若是不是函數,則直接執行commit,參數是value和載荷組成的數組。
  2. 若是是函數,則直接執行該函數,並將comit做爲其第一個參數,arg仍然做爲後續參數。

也就是說,官方文檔例子並不完整,它並無體現第二種狀況,實際上,官方文檔例子的完整形式還應當包括:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations('moduleName', {
       addAlias: function(commit, playload) {
           //將 `this.addAlias()` 映射爲 `this.$store.commit('increment', amount)`
           commit('increment') 
           //將 `this.addAlias(playload)` 映射爲 `this.$store.commit('increment', playload)`
           commit('increment', playload)
       }
     })
  }
}

一樣,mapMutations上述映射方式都支持傳遞一個模塊名做爲命名空間參數,這個在官方文檔也沒有體現:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations('moduleName', [
      // 將 `this.increment()` 映射爲 `this.$store.commit('increment')`
      'increment',

      // `mapMutations` 也支持載荷:
      // 將 `this.incrementBy(amount)` 映射爲 `this.$store.commit('incrementBy', amount)`
      'incrementBy' 
    ]),
    ...mapMutations('moduleName', {
      // 將 `this.add()` 映射爲 `this.$store.commit('increment')`
      add: 'increment' 
    }),
    ...mapMutations('moduleName', {
      addAlias: function(commit) {
          //將 `this.addAlias()` 映射爲 `this.$store.commit('increment')`
          commit('increment') 
      }
    })
  }
}

咱們能夠舉個例子證實一下:

const moduleA = {
  namespaced: true,
  state: { source: 'moduleA' },
  mutations: {
    increment (state, playload) {
      // 這裏的 `state` 對象是模塊的局部狀態
      state.source += playload
    }
  }
}
const store = new Vuex.Store({
  state() {
    return {
        source: 'root'
    }
  },
  mutations: {
    increment (state, playload) {
      state.source += playload
    }
  },
  modules: {
    a: moduleA
  }
})
var vm = new Vue({
  el: '#example',
  store,
  mounted() {
    console.log(this.source)
    this.localeincrement('testdata')
    console.log(this.source)
  },
    computed: Vuex.mapState([
      'source'
    ]
  ),
  methods: {
    ...Vuex.mapMutations({
      localeincrement (commit, args) {
        commit('increment', args)
      }
    })
  }
})

輸出結果:

root
test.html:139 roottestdata

另一個例子:

const moduleA = {
  namespaced: true,
  state: { source: 'moduleA' },
  mutations: {
    increment (state, playload) {
      // 這裏的 `state` 對象是模塊的局部狀態
      state.source += playload
    }
  }
}
const store = new Vuex.Store({
  state() {
    return {
        source: 'root'
    }
  },
  mutations: {
    increment (state, playload) {
      state.source += playload
    }
  },
  modules: {
    a: moduleA
  }
})
var vm = new Vue({
  el: '#example',
  store,
  mounted() {
    console.log(this.source)
    this.localeincrement('testdata')
    console.log(this.source)
  },
    computed: Vuex.mapState('a', [
      'source'
    ]
  ),
  methods: {
    ...Vuex.mapMutations('a', {
      localeincrement (commit, args) {
        commit('increment', args)
      }
    })
  }
})

輸出結果:

moduleA
test.html:139 moduleAtestdata

5.1.3 mapGetters

與mapState能夠映射模塊的state爲計算屬性相似,mapGetters也能夠將模塊的getters映射爲計算屬性,咱們來看看官方文檔的介紹:

圖片描述

mapGetters輔助函數定義在VUEX源碼中的843 ~ 864 行,咱們來看看它的源碼:

var mapGetters = normalizeNamespace(function (namespace, getters) {
  var res = {};
  normalizeMap(getters).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    val = namespace + val;
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if ("development" !== 'production' && !(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
});

和mapState的實現幾乎徹底同樣,惟一的差異只有1點:就是最後不會出現value爲函數的狀況。直接拿的是對應模塊上的getters:

return this.$store.getters[val]

5.1.4 mapActions

與mapMutations能夠映射模塊的mutation爲methods相似,mapActions也能夠將模塊的actions映射爲methods,咱們來看看官方文檔的介紹:

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      // 將 `this.increment()` 映射爲 `this.$store.dispatch('increment')`
      'increment',

      // `mapActions` 也支持載荷:
      // 將 `this.incrementBy(amount)` 映射爲 `this.$store.dispatch('incrementBy', amount)`
      'incrementBy' 
    ]),
    ...mapActions({
      // 將 `this.add()` 映射爲 `this.$store.dispatch('increment')`
      add: 'increment' 
    })
  }
}

一樣咱們來看看它是如何實現的,它的實現定義在VUEX源碼中的866 ~ 890 行:

var mapActions = normalizeNamespace(function (namespace, actions) {
  var res = {};
  normalizeMap(actions).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedAction () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      var dispatch = this.$store.dispatch;
      if (namespace) {
        var 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
});

和mapMutations的實現幾乎徹底同樣,惟一的差異只有1點:

  1. action是用來分派的,因此這裏拿的是dispatch。

咱們來具體分析一下代碼的執行:

首先是拷貝載荷:

var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];

而後是拿dispatch,若是mapActions函數提供了命名空間參數(即模塊名),則會拿帶命名空間模塊的dispatch:

var dispatch = this.$store.dispatch;
if (namespace) {
  var module = getModuleByNamespace(this.$store, 'mapActions', namespace);
  if (!module) {
    return
  }
  dispatch = module.context.dispatch;
}

最後則會看對應action的value是否是函數:

  1. 若是不是函數,則直接執行dispatch,參數是value和載荷組成的數組。
  2. 若是是函數,則直接執行該函數,並將dispatch做爲其第一個參數,arg仍然做爲後續參數。

也就是說,官方文檔例子並不完整,它並無體現第二種狀況,實際上,官方文檔例子的完整形式還應當包括:

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions ('moduleName', {
       addAlias: function(dispatch, playload) {
           //將 `this.addAlias()` 映射爲 `this.$store.dispatch('increment', amount)`
           dispatch('increment') 
           //將 `this.addAlias(playload)` 映射爲 `this.$store.dispatch('increment', playload)`
           dispatch('increment', playload)
       }
     })
  }
}

一樣,mapActions上述映射方式都支持傳遞一個模塊名做爲命名空間參數,這個在官方文檔也沒有體現:

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions('moduleName', [
      // 將 `this.increment()` 映射爲 `this.$store.dispatch('increment')`
      'increment', 

      // `mapActions` 也支持載荷:
      // 將 `this.incrementBy(amount)` 映射爲 `this.$store.dispatch('incrementBy', amount)`
      'incrementBy' 
    ]),
    ...mapActions('moduleName', {
      // 將 `this.add()` 映射爲 `this.$store.dispatch('increment')`
      add: 'increment' 
    }),
    ...mapActions('moduleName', {
      addAlias: function (dispatch) {
        // 將 `this.addAlias()` 映射爲 `this.$store.dispatch('increment')`
        dispatch('increment') 
      }
    })
  }
}

咱們能夠舉個例子證實一下:

const moduleA = {
  namespaced: true,
  state: { source: 'moduleA' },
  mutations: {
    increment (state, playload) {
      // 這裏的 `state` 對象是模塊的局部狀態
      state.source += playload
    }
  },
  actions: {
    increment (context) {
      context.commit('increment', 'testdata')
    }
  }
}
const store = new Vuex.Store({
  state() {
    return {
        source: 'root'
    }
  },
  mutations: {
    increment (state, playload) {
      state.source += playload
    }
  },
  actions: {
    increment (context) {
      context.commit('increment', 'testdata')
    }
  },
  modules: {
    a: moduleA
  }
})
var vm = new Vue({
  el: '#example',
  store,
  mounted() {
    console.log(this.source)
    this.localeincrement()
    console.log(this.source)
  },
    computed: Vuex.mapState([
      'source'
    ]
  ),
  methods: {
    ...Vuex.mapActions( {
      localeincrement (dispatch) {
        dispatch('increment')
      }
    })
  }
})

輸出結果:

root
roottestdata

另一個例子:

const moduleA = {
  namespaced: true,
  state: { source: 'moduleA' },
  mutations: {
    increment (state, playload) {
      // 這裏的 `state` 對象是模塊的局部狀態
      state.source += playload
    }
  },
  actions: {
    increment (context) {
      context.commit('increment', 'testdata')
    }
  }
}
const store = new Vuex.Store({
  state() {
    return {
        source: 'root'
    }
  },
  mutations: {
    increment (state, playload) {
      state.source += playload
    }
  },
  actions: {
    increment (context) {
      context.commit('increment', 'testdata')
    }
  },
  modules: {
    a: moduleA
  }
})
var vm = new Vue({
  el: '#example',
  store,
  mounted() {
    console.log(this.source)
    this.localeincrement()
    console.log(this.source)
  },
    computed: Vuex.mapState('a', [
      'source'
    ]
  ),
  methods: {
    ...Vuex.mapActions('a', {
      localeincrement (dispatch) {
        dispatch('increment')
      }
    })
  }
})

輸出結果:

moduleA
moduleAtestdata

5.1.5 createNamespacedHelpers

createNamespacedHelpers主要是根據傳遞的命名空間產生對應模塊的局部化mapState、mapGetters、mapMutations、mapActions映射函數,它定義在VUEX源碼的892 ~ 897行:

var createNamespacedHelpers = function (namespace) { return ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
}); };

5.2 其它輔助函數

5.2.1 isObject

isObject定義在VUEX源碼的94 ~ 96 行,主要判斷目標是不是有效對象,其實現比較簡單:

//判斷是否是object
function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

5.2.2 isPromise

isPromise定義在VUEX源碼的98 ~ 100 行,主要判斷目標是不是promise,其實現比較簡單:

function isPromise (val) {
  return val && typeof val.then === 'function'
}

5.2.3 assert

assert定義在VUEX源碼的102 ~ 104 行,主要用來斷言,其實現比較簡單:

function assert (condition, msg) {
  if (!condition) { throw new Error(("[vuex] " + msg)) }
}

第六章 總結

到此這本VUEX學習筆記算是寫完了,整體而言是對我的在學習VUEX源碼過程當中的理解、想法進行的記錄和總結,這其中除了不可避免的主觀視角外,天然還會存在一些理解上的誤差甚至錯誤,但願看到這本書的人可以指正。

更多內容可查看本人博客以及github

相關文章
相關標籤/搜索