完全搞懂vue之vuex原理篇

你真的懂vuex嗎?先拋出幾個問題?

  1. 命名空間的原理?javascript

  2. 輔助函數的原理?vue

  3. 插件用法是否瞭解?java

  4. 爲何每一個組件都能訪問到$store這個實例屬性?面試

  5. 爲何訪問this.$store.getters['a/xx']而不是this.$store.getters.a.xx?vuex

  6. state數據修改後,視圖中的getters數據爲什麼也會動態改變數組

  7. actions裏是否能夠直接操做state?(strict爲ture即嚴格模式下不行,那在嚴格模式下又是如何作到只能使用commit修改?)緩存

  8. actions裏如何訪問別的modules中的state?bash

ok,答案都在下面噢👇app

註冊插件

這個就不用多說了,註冊vue插件的經常使用方法經過vue.use()進行,傳入的能夠是對象或者函數,對象的話必須包含install方法,內部會調用install方法,並將vue做爲參數傳入異步

install方法

function install (_Vue) {
  //檢查是否已經註冊過,註冊過的話全局變量Vue會被賦值爲Vue構造函數
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  applyMixin(Vue);
}複製代碼

applyMixin

全局混入生命週期,使得每一個組件能訪問到$store

function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0]);
  //判斷版本號
  if (version >= 2) {
    //使用mixin混入beforeCreate生命週期
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    var _init = Vue.prototype._init;
    Vue.prototype._init = function (options) {
      if ( options === void 0 ) options = {};

      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit;
      _init.call(this, options);
    };
  }
  
  //使每一個組件都能訪問到$store
  function vuexInit () {
    var options = this.$options;
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store;
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store;
    }
  }
}複製代碼

store構造函數

class Store{
    constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
    const {
      plugins = [],
      strict = false
    } = options

    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    //用來記錄提交狀態,主要用來檢查是否正在進行commit操做 
    this._committing = false
    //存儲actions
    this._actions = Object.create(null)
    //存儲mutations
    this._mutations = Object.create(null)
    //存儲getters
    this._wrappedGetters = Object.create(null)
    //存儲modules
    this._modules = new ModuleCollection(options)
    //存儲module和其namespace的對應關係。
    this._modulesNamespaceMap = Object.create(null)
    //訂閱監聽
    this._subscribers = []
    //監聽器
    this._watcherVM = new Vue()

    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }
    this.strict = strict
    installModule(this, state, [], this._modules.root)
    resetStoreVM(this, state)
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }
}複製代碼

1. 主要片斷1

const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}複製代碼

上面這段代碼是啥意思呢?

主要解決的問題是調用commit的時候this的指向問題,好比下面這段代碼

actions:{
	addSalaryAction({commit},payload){
		setTimeout(()=>{
			commit('addSalary',payload)
		},1000)
	}
}複製代碼

調用commit的時候,只是執行commit這個方法,this指向的並非store實例,固然解決方案有不少,源碼作了科裏化,這樣執行commit的時候都會返回一個用store實例調用的結果

commit
=>
function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}複製代碼

經典!!!nice!!!

2. 插件管理

沒錯store內部也是有插件能夠定製的

用法以下:

import MyPlugin=()=>store=>{
  store.subscribe(xx),
  store.watch(xx)
}
const store = new Vuex.Store({
  plugins: [MyPlugin()]
}複製代碼

源碼很簡單:

//查看是否傳入plugins
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
//遍歷plugins傳入store實例做爲參數
plugins.forEach(function (plugin) { return plugin(this$1); });複製代碼


3.resetStoreVM

function resetStoreVM (store, state, hot) {
  var oldVm = store._vm;//在store中定義的vue實例

  // 建立getters
  store.getters = {};
  // 重置緩存
  store._makeLocalGettersCache = Object.create(null);
  var wrappedGetters = store._wrappedGetters;
  var computed = {};
  // 爲getters設置代理 4.2會着重講到
  forEachValue(wrappedGetters, function (fn, key) {
    computed[key] = partial(fn, store);
    Object.defineProperty(store.getters, key, {
      get: function () { return store._vm[key]; },
      enumerable: true // 可枚舉
    });
  });

  
  var silent = Vue.config.silent;
  Vue.config.silent = true;
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed: computed
  });
  Vue.config.silent = silent;

  // 嚴格模式只能經過commit修改state
  if (store.strict) {
    enableStrictMode(store);
  }
  //是否有舊的實例
  if (oldVm) {
    if (hot) {
      store._withCommit(function () {
        oldVm._data.$$state = null;
      });
    }
    Vue.nextTick(function () { return oldVm.$destroy(); });
  }
}複製代碼

enableStrictMode

function enableStrictMode (store) {
  //監聽state的變化,保證只能使用commit修改state
  store._vm.$watch(function () { return this._data.$$state }, function () {
    if (process.env.NODE_ENV !== 'production') {
      //經過實例屬性_commiting來判斷,只要不通過commit,_commiting就爲false,就會報錯
      assert(store._committing, "do not mutate vuex store state outside mutation handlers.");
    }
  }, { deep: true, sync: true });
}複製代碼

_withCommit

作實例屬性_commiting狀態的改變,主要用來在嚴格模式下確保只能經過commit修改state,由於只要經過commit修改_commiting屬性就會發生改變

Store.prototype._withCommit = function _withCommit (fn) {
  var committing = this._committing;
  this._committing = true;
  fn();
  this._committing = committing;
};複製代碼

4. 主要API

1.state的原理

如何經過訪問this.$store.state.xx就能訪問對應state的值?而且改變state後視圖也能對應渲染

很簡單,建立一個vue實例,state做爲data中的數據便可

class Store {
    constructor(options) {
        this._vm = new Vue({
            data() {
                return {
                    state: options.state
                }
            }
        })
    }
    get state(){
    	return this._vm.state
    }
    //直接設置state會報錯
    set state(v){
      throw new Error('')
    }
}複製代碼

2.getters?

如何經過訪問this.$store.getters.a就能返回對應方法執行後的返回值?

也就是this.$store.getters.a => return this.$store.getters.a(state)

很簡單作一層代理便可

Object.defineProperty(store.getters, key, {
  get: function () { return store._vm[key]; },
  enumerable: true // for local getters
});複製代碼

這裏最重要一點也是面試常常會問到的修改state以後getters的視圖數據如何動態渲染?

源碼中使用的方法是將其放到vue實例的computed中

var wrappedGetters = store._wrappedGetters;
//拿到存儲的全部getters
var computed = {};
//遍歷getters
forEachValue(wrappedGetters, function (fn, key) {
    //存儲到computed對象中
    computed[key] = partial(fn, store);//partical的做用是將其變成()=>{fn(store)}
    //設置getters的代理,訪問getters就是訪問computed
    Object.defineProperty(store.getters, key, {
        get: function () { return store._vm[key]; },
        enumerable: true
    });
});
...
store._vm = new Vue({
    data: {
      $$state: state
    },
    //賦值給計算屬性
    computed: computed
});複製代碼

wrappedGetters是安裝的全部getters的值,須要在installModule中看,下面會提到

3.commit

提交mutations

commit是store的實例方法,commit傳入key值和參數來執行mutations中對應的方法

幾種不一樣的用法:

  • commit({type:xx,payload:xx})

  • commit(type, payload)

咱們看下源碼:

Store.prototype.commit = function commit (_type, _payload, _options) {
  var this$1 = this;
  //對傳入的參數作統一處理,由於commit的調用方式有不少種
  var ref = unifyObjectStyle(_type, _payload, _options);
  var type = ref.type;
  var payload = ref.payload;
  var options = ref.options;

  var mutation = { type: type, payload: payload };
  var entry = this._mutations[type];
  //檢查是否有對應的mutations
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(("[vuex] unknown mutation type: " + type));
    }
    return
  }
  //改變commiting狀態
  this._withCommit(function () {
    entry.forEach(function commitIterator (handler) {
      handler(payload);
    });
  });
  //發佈全部訂閱者
  this._subscribers.forEach(function (sub) { return sub(mutation, this$1.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'
    );
  }
};複製代碼

4.dispatch

分發actions

執行異步

actions: {
    minusAgeAction({commit},payload){
      console.log(this)
      setTimeout(()=>{
        commit('minusAge',payload)
      },1000)
    },
 },複製代碼

有一個注意點是actions中的commit中的this指向問題,上面已經講過了,咱們看下核心源碼:

Store.prototype.dispatch = function dispatch (_type, _payload) {
    var this$1 = this;

  // check object-style dispatch
  var ref = unifyObjectStyle(_type, _payload);
  var type = ref.type;
  var payload = ref.payload;

  var action = { type: type, payload: payload };
  var entry = this._actions[type];
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(("[vuex] unknown action type: " + type));
    }
    return
  }

  try {
    this._actionSubscribers
      .filter(function (sub) { return sub.before; })
      .forEach(function (sub) { return sub.before(action, this$1.state); });
  } catch (e) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn("[vuex] error in before action subscribers: ");
      console.error(e);
    }
  }

  var result = entry.length > 1
    ? Promise.all(entry.map(function (handler) { return handler(payload); }))
    : entry[0](payload);

  return result.then(function (res) {
    try {
      this$1._actionSubscribers
        .filter(function (sub) { return sub.after; })
        .forEach(function (sub) { return sub.after(action, this$1.state); });
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn("[vuex] error in after action subscribers: ");
        console.error(e);
      }
    }
    return res
  })
};複製代碼

modules—vuex中是如何管理module的?

最基本的用法:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    age: 10,
    salary: 6500
  },
  getters: {
    totalSalary(state) {
      return state.salary * 12
    }
  },
  modules: {
    a: {
      namespaced: true,
      state: {
        age: 100,
        salary:12000
      },
      getters:{
        totalSalary(state){
          return state.salary*12
        }
      },
      modules:{
        b: {
          namespaced:true,
          state:{
            age:200
          }
        }
      }
    }
  }
})複製代碼

訪問a中的state=>this.$store.state.a.age

訪問a中的getters=>this.$store.getters['a/totalSalary']

訪問a中子模塊b中的state=>this.$store.state.a.b.age

訪問a中子模塊b中的getters=>this.$store.getters['a/b/totalSalary']

1.ModuleCollection

這個類的做用就是將傳入的modules格式化便於管理,好比傳入如下的modules

modules:{
    a: {
      namespaced: true,
      state: {
        age: 100,
        salary: 12000
      },
      getters: {
        totalSalary(state) {
          return state.salary * 12
        }
      },
      modules: {
        c: {
          namespaced: true,
          state: {
            age: 300,
            salary: 14000
          },
          getters: {
            totalSalary(state) {
              return state.salary * 12
            }
          },
        }
      }
    },
    b:{}
 }複製代碼

=>格式化成

root:{
  _raw:rootModule,
  _children:{
		a:{
      _raw:aModule,
      _children:{
        c:{
          _raw:bModule,
          state:cState
        }
      },
      state:aState
    },
    b:{
      _raw:bModule,
      state:bState
    }
  },
  state:xx
}複製代碼

實現起來很簡單,可是這裏有一個bug就是沒法遞歸到第二層級,就是說下面的代碼轉化的a和c在同一層級,但很明顯的是c是a的children?如何改進呢?小夥伴們能夠先試着想一下

class ModuleCollection{
    constructor(options){
        this.register([],options)
    }
    register(path,rootModule){
        let newModule={
            _rawModule:rootModule,
            _children:{},
            state:rootModule.state
        }
        if(path.length===0){
            this.root=newModule
        }else{
            this.root._children[path[path.length-1]]=rootModule
        }
        
        if(rootModule.modules){
            forEach(rootModule.modules,(key,value)=>{
                this.register(path.concat(key),value)
            })
        }
    }
}複製代碼

改進事後:

class ModuleCollection{
    constructor(options){
        this.register([],options)
    }
    register(path,rootModule){
        let newModule={
            _rawModule:rootModule,
            _children:{},
            state:rootModule.state
        } //vuex源碼是將其變成一個module類
        
        if(path.length===0){
            this.root=newModule
        }else{
            //第一次path爲[a]
            //第二次path變成[a,c]=>因此c前面的必然是父級,但也可能存在多個層級及父親的父親(及若是是[a,c,d]的話那麼意味着a是d的父親的父親),因此須要用reduce
            //而後又回到與a平級,因而path從新變成[b]
            let parent=path.slice(0,-1).reduce((prev,curr)=>{
                return prev._children[curr]
            },this.root) //vuex源碼中這一段會封裝成一個get實例方法
            
            parent._children[path[path.length-1]]=newModule //vuex源碼這一段會做爲module類的一個實例屬性addChild,由於parent是一個module類因此能夠調用addChild方法
        }
        if(rootModule.modules){
            forEach(rootModule.modules,(key,value)=>{
                this.register(path.concat(key),value)
            })
        }
    }
}複製代碼

2.installModule

modules定義完畢,下一步就是如何在$store上取值了,好比想要調用a中的state或者getters?如何作呢,很簡單咱們須要將整個module安裝到$Store上

2.1 安裝state

在實現以前咱們先看下用法,好比有咱們註冊了這樣的module

export default new Vuex.Store({
  state: {
    age: 10,
    salary: 6500
  },
  getters: {
    totalSalary(state) {
      return state.salary * 12
    }
  },
  mutations: {
    addAge(state, payload) {
      state.age += payload
    },
    minusAge(state, payload) {
      state.age -= payload
    }
  },
  actions: {
    minusAgeAction({ commit }, payload) {
      setTimeout(() => {
        commit('minusAge', payload)
      }, 1000)
    }
  },
  modules: {
    a: {
      namespaced:true,
      state: {
        age: 100,
        salary: 10000
      },
      getters: {
        totalSalaryA(state) {
          return state.salary * 12
        }
      },
      mutations: {
        addAge(state, payload) {
          console.log(1)
          state.age += payload
        },
        minusAge(state, payload) {
          state.age -= payload
        }
      },
      modules: {
        c: {
          namespaced: true,
          state: {
            age: 300,
            salary: 14000
          },
          getters: {
            totalSalaryC(state) {
              return state.salary * 12
            }
          },
          modules:{
            d:{}
          }
        }
      }
    },
    b:{}
  }
})複製代碼

咱們但願訪問this.$store.state.a.age就能拿到a模塊下的age,原理其實和以前格式化的原理同樣,格式化是須要遞歸而後把子模塊放到父模塊的_children中,而安裝state則是將子模塊中的state都放到父級的state中

const installModule=(store,state,path,rootModule)=>{
	if(path.length>0){
    let parent=path.slice(0,-1).reduce((prev,curr)=>{
      return prev[curr].state
    },state)
    parent[path[path.length-1]]=rootModule.state
  }
  forEach(rootModule._rawModule.modules,(key,value)=>{
    installModule(store,state,path.concat(key),value)
  })
}複製代碼

若是不是很理解能夠先打印path,看每一次path的取值就會明朗不少,這樣就結束了嗎?並無那麼簡單,細心的小夥伴會發現咱們store類中的state是響應式的,是基於vue的發佈訂閱模式,就是一旦state中值發生改變會觸發試圖的更新可是咱們上述是在state中又增長了n個對象,這些對象並非響應式的(若是瞭解vue的原理這一塊不用多講),意思就是後續增長的對象改變不會觸發試圖的更新,因此咱們須要將這些後增長的也變成響應式的,很簡單,vue中有靜態方法set能夠幫您輕鬆搞定

parent[path[path.length-1]]=rootModule.state
=>
Vue.set(parent,path[path.length-1],rootModule.state)複製代碼

2.2 安裝getters

getters的用法比較特殊

1.是不能註冊相同名稱的方法

2.若是沒有註冊命名空間,想獲取a模塊中的getters中的方法,用法爲this.$store.getters.屬性名

3.若是註冊了命名空間,想獲取a模塊中的getters中的方法,用法爲this.$store.getters['a/xx']

咱們先看前兩個如何實現?

const installModule=(store,state,path,rootModule)=>{
    let getters=rootModule._rawModule.getters
    //當前的module是否存在getters
    if(getters){
        //若是存在把getters都掛載到store.getters上也就是Store類的實例上
        forEach(getters,(key,value)=>{
            Object.defineProperty(store.getters,key,{
                get:()=>{
                    return value(rootModule.state)
                }
            })
        })
    }
    //遞歸module
    forEach(rootModule._rawModule.modules,(key,value)=>{
    	installModule(store,state,path.concat(key),value)
  	})
}複製代碼

2.3 安裝mutations

mutations的用法和getters又不一樣

1.能夠註冊相同名稱的方法,相同方法會存入到數組中按順序執行

2.若是沒有註冊命名空間,想獲取a模塊中的mutations中的方法,用法爲this.$store.commit('屬性名')

3.若是註冊了命名空間,想獲取a模塊中的mutations中的方法,用法爲this.$store.commit['a/xx']

咱們先看前兩個如何實現?

const installModule=(store,state,path,rootModule)=>{
    let mutations=rootModule._rawModule.mutations
    //當前的module是否存在mutations
    if(mutations){
        //若是存在,看當前的屬性在mutations中是否已經存在,不存在的話先賦值爲空數組,而後在將方法放入,出現同名也不要緊,調用時依次執行
        forEach(mutations,(key,value)=>{
                if (mutations) {
        	    forEach(mutations, (key, value) => {
            		store.mutations[key]=store.mutations[key]||[]
            		store.mutations[key].push(
                	payload => {
                            value(rootModule.state, payload)
                	})
        	    })
    	        }
        })
    }
    //遞歸module
    forEach(rootModule._rawModule.modules,(key,value)=>{
    	installModule(store,state,path.concat(key),value)
    })
}複製代碼

2.4 安裝actions

actions和mutations原理幾乎如出一轍,主要區別以下

1.調用時接受的參數不是state而是一個通過處理的對象能夠解構出state,commit等屬性

2.調用時沒有命名空間是this.$store.dispatch(xx),有命名空間是this.$store.dispatch('a/xx')

咱們先看前兩個如何實現?

const installModule=(store,state,path,rootModule)=>{
    let actions=rootModule._rawModule.actions
    //當前的module是否存在actions
    if(actions){
        //若是存在,看當前的屬性在actions中是否已經存在,不存在的話先賦值爲空數組,而後在將方法放入,出現同名也不要緊,調用時依次執行
        forEach(actions,(key,value)=>{
            store.actions[key]=store.actions[key]||[]
            store.actions[key].push(
                payload => {
                    value(store, payload)
                }
            )
        })
    }
    //遞歸module
    forEach(rootModule._rawModule.modules,(key,value)=>{
    	installModule(store,state,path.concat(key),value)
  	})
}複製代碼

3.命名空間

也就是所謂的namespace,爲何須要命名空間?

1.不一樣模塊中有相同命名的mutations、actions時,不一樣模塊對同一 mutation 或 action 做出響應

2.輔助函數(下面會講到)

在上面安裝模塊的時候咱們講過mutations和actions中若是名稱相同並不矛盾,會將其都存入到數組中依次執行,那這樣帶來的後果就是我只想執行a模塊中的mutation可是沒想到先執行了最外面定義的mutation,而後在執行a模塊中的,因此咱們須要引入命名空間加以區分(有人說那我起名字不同不就好了嗎?相信我項目大或者多人合做的話這一點真的很難保證)

咱們先看下命名空間的API:

好比咱們想要調用a模塊中mutations的addNum方法,咱們通常這樣調用this.$store.commit('a/addNum'),咱們以前調用是this.$store.commit('addNum')

因此咱們只須要在安裝mutations時把對應屬性前加一個模塊名的前綴便可。整理步驟以下

  • 先判斷該模塊是否有命名空間,若是有的話須要進行拼接返回,好比a模塊有命名空間返回a/,a模塊下的c模塊也有則返回a/c/

  • 將返回的命名空間放在mutations或者actions屬性名的前面加以區分

咱們看下vuex內部源碼

ModuleCollection.prototype.getNamespace = function getNamespace (path) {
  var module = this.root;
  return path.reduce(function (namespace, key) {
    module = module.getChild(key);
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
};
//getNamespace是實例方法用來返回拼接後的命名空間
...
function installModule (store, rootState, path, module, hot) {
  var isRoot = !path.length;
  var namespace = store._modules.getNamespace(path);

  if (module.namespaced) {
    //若是有命名空間,則存放到_modulesNamespaceMap對象中
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/'))));
    }
    store._modulesNamespaceMap[namespace] = module;
  }

  if (!isRoot && !hot) {
    var parentState = getNestedState(rootState, path.slice(0, -1));
    var moduleName = path[path.length - 1];
    store._withCommit(function () {
      if (process.env.NODE_ENV !== 'production') {
        if (moduleName in parentState) {
          console.warn(
            ("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"")
          );
        }
      }
      Vue.set(parentState, moduleName, module.state);
    });
  }

  var local = module.context = makeLocalContext(store, namespace, path);

  module.forEachMutation(function (mutation, key) {
    var namespacedType = namespace + key; //將命名空間拼接到屬性名前面
    registerMutation(store, namespacedType, mutation, local);
  });
  
  module.forEachAction(function (action, key) {
    var type = action.root ? key : namespace + key;//這個暫時還沒用到過
    var handler = action.handler || action;
    registerAction(store, type, handler, local);
  });

  module.forEachGetter(function (getter, key) {
    var namespacedType = namespace + key;
    registerGetter(store, namespacedType, getter, local);
  });

  module.forEachChild(function (child, key) {
    installModule(store, rootState, path.concat(key), child, hot);
  });
}複製代碼

ok這樣基本就實現了,可是actions彷佛還有問題,咱們舉個例子

actions: {
    minusAgeAction({ commit }, payload) {
      setTimeout(() => {
        commit('minusAge', payload) //並無使用命名空間,如何精確調用b模塊mutation?
      }, 1000)
    }
},複製代碼

假設上面是a模塊的actions,咱們調用時執行的commit並無使用命名空間調用,因此勢必又會去調用最外層的mutation?因此根源是解構出的commit有問題,咱們看下vuex源碼這塊是怎麼寫的?

function installModule (store, rootState, path, module, hot) {
  var isRoot = !path.length;
  var namespace = store._modules.getNamespace(path);
  ...

  var local = module.context = makeLocalContext(store, namespace, path);
  //建立上下文對象代替store實例
  ...

  module.forEachAction(function (action, key) {
    var type = action.root ? key : namespace + key;
    var handler = action.handler || action;
    registerAction(store, type, handler, local);
  });


  module.forEachChild(function (child, key) {
    installModule(store, rootState, path.concat(key), child, hot);
  });
}複製代碼

registerAction

function registerAction (store, type, handler, local) {
  var entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler (payload) {
    //傳入上下文對象
    var res = handler.call(store, {
      dispatch: local.dispatch,//只能調用當前模塊的actions
      commit: local.commit,//只能調用當前模塊的mutations
      getters: local.getters,//只包含當前模塊的getters
      state: local.state,//只包含當前的state
      rootGetters: store.getters,//根上的getters,可用來調取別的模塊中的getters
      rootState: store.state//根上的state,可用來調取別的模塊中的state
    }, payload);
    if (!isPromise(res)) {
      res = Promise.resolve(res);
    }
    if (store._devtoolHook) {
      return res.catch(function (err) {
        store._devtoolHook.emit('vuex:error', err);
        throw err
      })
    } else {
      return res
    }
  });
}複製代碼

建立上下文對象的方法

function makeLocalContext (store, namespace, path) {
  var noNamespace = namespace === '';

  var local = {
    dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) {
      var args = unifyObjectStyle(_type, _payload, _options);
      var payload = args.payload;
      var options = args.options;
      var type = args.type;

      if (!options || !options.root) {
        type = namespace + type;
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(("[vuex] unknown local action type: " + (args.type) + ", global type: " + type));
          return
        }
      }

      return store.dispatch(type, payload)
    },

    //調用a模塊中的addAge,commit('addAge',payload)就至關於調用store.commit('a/addAge',payload)
    commit: noNamespace ? store.commit : function (_type, _payload, _options) {
      //判斷是否傳入的對象
      //另一種調用方式, 傳入的是一個對象 commit({type:xx,payload:xx})
      var args = unifyObjectStyle(_type, _payload, _options);
      var payload = args.payload;
      var options = args.options;
      var type = args.type;

      if (!options || !options.root) {
        type = namespace + type;
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type));
          return
        }
      }
      //調用實例中的commit
      store.commit(type, payload, options);
    }
  };

  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? function () { return store.getters; }
        : function () { return makeLocalGetters(store, namespace); }
    },
    state: {
      get: function () { return getNestedState(store.state, path); }
    }
  });

  return local
}複製代碼

unifyObjectStyle

function unifyObjectStyle (type, payload, options) {
  //判斷傳入的第一個參數是不是對象,對參數從新作調整後返回
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)
  return { type, payload, options }
}複製代碼

4.subscribe

這個API可能不少人不經常使用,主要使用場景就是監聽commit操做,好比我須要將哪些state值動態本地存儲,即只要調用commit就執行本地存儲將新的值更新。固然還有不少使用場景

Store.prototype.commit = function commit (_type, _payload, _options) {
  ...
  this._subscribers.forEach(function (sub) { return sub(mutation, this$1.state); });
  ...
};複製代碼

從上面代碼能夠看出只要執行commit操做便會執行實例屬性_subscribers,沒錯這就是發佈訂閱的模式。先收集全部訂閱內容,執行commit就遍歷執行

store實例方法subscribe (發佈)

Store.prototype.subscribe = function subscribe (fn) {
  return genericSubscribe(fn, this._subscribers)
};複製代碼

genericSubscribe(訂閱)

function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn);
  }
  return function () {
    var i = subs.indexOf(fn);
    if (i > -1) {
      subs.splice(i, 1);
    }
  }
}複製代碼

demo

this.$store.subscribe(()=>{
	console.log('subscribe')
})複製代碼

每執行一次commit便會打印subscribe

固然還有action的監聽,能夠本身研究下


5.輔助函數

方便調用的語法糖

5.1 mapState

原理其實很簡單

好比在a模塊調用age,想辦法將其指向this.$store.state.a.age

用法:

import {vuex} from "vuex"
export default:{
	computed:{
		...mapState('a',['age'])
                //...mapState('a',{age:(state,getters)=>state.age) 另一種調用方法
	}
}複製代碼

從上面調用方法可知,mapState是一個函數,返回的是一個對象,對象的結構以下:

{
	age(){
		return this.$store.state.a.age
	}
}
=>
computed:{
	age(){
		return this.$store.state.a.age
	}
}複製代碼

因而咱們調用this.age就可得到當前模塊的state中的age值,好的咱們看下源碼

var mapState = normalizeNamespace(function (namespace, states) {
  var res = {};
  //傳入的states必須是數組或對象
  if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object');
  }
  //遍歷先規範化states,而後作遍歷
  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) {
        // 獲取實例上_modulesNamespaceMap屬性爲namespace的module
        var module = getModuleByNamespace(this.$store, 'mapState', namespace);
        if (!module) {
          return
        }
        //module中的context是由makeLocalContext建立的上下文對象,只包含當前模塊
        state = module.context.state;
        getters = module.context.getters;
      }
      //判斷val是不是函數,這裏涉及到mapState的不一樣用法,核心的語法糖
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  return res
});複製代碼

5.2 mapGetters

原理和mapState幾乎如出一轍

好比在a模塊調用totalSalary,想辦法將其指向this.$store.getters['a/totalSalary']

var mapGetters = normalizeNamespace(function (namespace, getters) {
  var res = {};
  if (process.env.NODE_ENV !== 'production' && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object');
  }
  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 (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
        console.error(("[vuex] unknown getter: " + val));
        return
      }
      return this.$store.getters[val] //這個在上面命名空間中的getters已經講過了,核心語法糖
    };
    //調試用的
    res[key].vuex = true;
  });
  return res
});複製代碼

5.3 mapMutations

用法:

...mapMutations('a',['addAge'])
//...mapMutations('a',{
// addAge:(commit,payload)=>{
// commit('addAge',payload)
// }
//第二種調用方法
})複製代碼

var mapMutations = normalizeNamespace(function (namespace, mutations) {
  var res = {};
  if (process.env.NODE_ENV !== 'production' && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object');
  }
  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 ];

      // Get the commit method from store
      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
});複製代碼

5.4 mapActions

同mapMutations,只要把函數內部的commit換成dispatch就好了

5.5 輔助函數中涉及的幾個方法

normalizeMap

規範化傳入的參數

function normalizeMap (map) {
  if (!isValidMap(map)) {
    return []
  } //不是數組或者對象直接返回空數組
  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] }); })
  //對傳入的參數作處理,作統一輸出
}複製代碼

normalizeNamespace

標準化命名空間,是一個科裏化函數

function normalizeNamespace (fn) {
  return function (namespace, map) {
    // 規範傳入的參數
    // 咱們通常這樣調用 有模塊:...mapState('a',['age'])或者 沒模塊:...mapState(['age'])
    if (typeof namespace !== 'string') { //沒傳入命名空間的話,傳入的第一個值賦值給map,命名空間爲空
      map = namespace;
      namespace = '';
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/';
      //傳入命名空間的話,看命名空間最後是否有/,沒有添加/
    }
    return fn(namespace, map)
  }
}複製代碼

對傳入的命名空間添加/是由於每一個聲明命名空間的模塊,內部的getters等的屬性名都通過了處理,以命名空間+key的形式存儲。

最後

最後附上一些心得,其實源碼並不難讀懂,核心代碼和邏輯就那麼點,咱們徹底能夠實現一個極其簡易版的vuex

源碼之因此看上去很繁雜是由於

  • 做者將不少方法抽離出來,方法單獨命名,因此常常會發現函數裏又套了n個函數,其實只是做者將重複的一些方法單獨抽離出來公用而已

  • 多個API以及API的不一樣使用方法,好比上面提到的mapState這些輔助函數,能夠有多種使用方式,還有好比commit你能夠傳{xx,payload}也能夠傳{type:xx,payload:xx},做者要作許多相似這樣的處理,兼容多個用戶的習慣

  • 龐大的錯誤處理,什麼throw new Error這些東西其實也佔了很大比重,好的源碼設計錯誤處理是必不可少的,這是爲了防止使用者作一些違背做者設計初衷的行爲,若是錯誤提示設計的好是能起到錦上添花的做用的,一旦出了錯誤用戶能一目瞭然哪出了bug。

聽說點讚的人都會拿到高薪噢!!!

參考博客-掘金

參考博客-知乎

相關文章
相關標籤/搜索