在第一章咱們曾經說過:html
VUEX採用的是典型的IIFE(當即執行函數表達式)模式,當代碼被加載(經過<script>
或Vue.use()
)後,VUEX會返回一個對象,這個對象包含了Store
類、install
方法、mapState
輔助函數、mapMutations
輔助函數、mapGetters
輔助函數、mapActions
輔助函數、createNamespacedHelpers
輔助函數以及當前的版本號version
。
本章就將詳細講解mapState
、mapMutations
、mapGetters
、mapActions
、createNamespacedHelpers這5個輔助和函數。
vue
若是你在使用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
由以上分析咱們能夠知道,上述官方文檔在此處的示例其實並不完善,該實例並無指出能夠經過提供模塊名稱做爲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參數同時支持數組和對象兩種形式。
這兩種形式最終都會獲得一個新數組,而數組元素就是{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而進一步處理:
第二種狀況在前述官方文檔的例子中也有所體現:
// 爲了可以使用 `this` 獲取局部狀態,必須使用常規函數 countPlusLocalState (state) { return state.count + this.localCount }
但這個官方文檔例子並不完整,它並無體現出還會暴露出getters參數,實際上,上述例子的完整形式應該是這樣子的:
// 爲了可以使用 `this` 獲取局部狀態,必須使用常規函數 countPlusLocalState (state, getters) { return state.count + this.localCount + getters.somegetter }
與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的實現幾乎徹底同樣,惟一的差異只有兩點:
咱們來具體分析一下代碼的執行:
首先是拷貝載荷:
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是否是函數:
也就是說,官方文檔例子並不完整,它並無體現第二種狀況,實際上,官方文檔例子的完整形式還應當包括:
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
與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]
與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點:
咱們來具體分析一下代碼的執行:
首先是拷貝載荷:
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是否是函數:
也就是說,官方文檔例子並不完整,它並無體現第二種狀況,實際上,官方文檔例子的完整形式還應當包括:
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
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) }); };
isObject定義在VUEX源碼的94 ~ 96 行,主要判斷目標是不是有效對象,其實現比較簡單:
//判斷是否是object function isObject (obj) { return obj !== null && typeof obj === 'object' }
isPromise定義在VUEX源碼的98 ~ 100 行,主要判斷目標是不是promise,其實現比較簡單:
function isPromise (val) { return val && typeof val.then === 'function' }
assert定義在VUEX源碼的102 ~ 104 行,主要用來斷言,其實現比較簡單:
function assert (condition, msg) { if (!condition) { throw new Error(("[vuex] " + msg)) } }
到此這本VUEX學習筆記算是寫完了,整體而言是對我的在學習VUEX源碼過程當中的理解、想法進行的記錄和總結,這其中除了不可避免的主觀視角外,天然還會存在一些理解上的誤差甚至錯誤,但願看到這本書的人可以指正。