Vue源碼解析(五)-vuex

Vue 組件中得到 Vuex 狀態

按官網說法:「因爲 Vuex 的狀態存儲是響應式的,從 store 實例中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態」,本文結合下面的demo進行分析:vue

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const vueStore = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment (state) {
            state.count++
        }
    }
})
let vm = new Vue({
    el: '#app',
    store: vueStore,
    template: '<div>{{count}}</div>',
    computed: {
        count(){
            return this.$store.state.count
        }
    }
})

下面主要分析爲何能夠經過this.$store直接訪問vueStore對象。先看看Vue.use方法react

Vue.use = function (plugin) {
    //插件只能註冊一次
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    
    //拼接參數,將Vue做爲第一個參數
    // additional parameters
    var args = toArray(arguments, 1);
    args.unshift(this);
    
    //調用plugin.install或plugin方法
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this
  };

再看Vuex源碼,Vuex實際上是下面這個對象git

{
  Store: Store,
  install: install,
  mapState: mapState,
  mapMutations: mapMutations,
  mapGetters: mapGetters,
  mapActions: mapActions,
  createNamespacedHelpers: createNamespacedHelpers
}

所以Vue.use(Vuex)其實想到於Vuex.install()github

let Vue; // bind on install
function install (_Vue) {
  Vue = _Vue;
  applyMixin(Vue);
}

var applyMixin = function (Vue) {
  var version = Number(Vue.version.split('.')[0]);
  //Vue2.0處理方法
  if (version >= 2) {
    //將vuexInit方法註冊到beforeCreate鉤子上,當Vue的生命週期走到callHook(vm, 'beforeCreate');時觸發vuexInit方法
    Vue.mixin({ beforeCreate: vuexInit });
  } 

  // Vuex init hook, injected into each instances init hooks list.
  function vuexInit () {
    //this就是當前正在被new的Vue對象
    var options = this.$options;
    //將options.store(本例demo中的vueStore)賦值給this.$store,所以能夠經過this.$store訪問vueStore對象
    // 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;
    }
  }
}

mapState

經過computed屬性能夠獲取到狀態值,可是每個屬性都要經過this.$store.state訪問不是很方便。vue 提供了 mapState 函數,它把state直接映射到咱們的組件中,先給出mapState的使用demovuex

let mapState = Vuex.mapState
let vm = new Vue({
  el: '#app',
  store: vueStore,
  template:
  `<div>
    <my-component></my-component>
  </div>`,
  components:{
      'my-component':{
          template: '<div>{{count}}-{{num}}</div>',
          computed: mapState({
              // {{count}}值爲this.$store.state.count
              count: state => state.count,
              // {{num}}值爲this.$store.state.num,
              num: 'num'         
          })
      }
  }
})

讓咱們看看mapState的源碼實現redux

//normalizeNamespace規範當前vuex的命名空間。默認狀況下,vuex內部的 action、mutation 和 getter 是註冊在全局命名空間的,本例也是,所以namespace=‘’
var mapState = normalizeNamespace(function (namespace, states) {
  var res = {};
  //規範states參數,將states轉換爲map格式,所以mapState支持多種寫法
  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;
  });
  //mapState其實就是提供簡潔的寫法將this.$store.state[val]賦值給coputed屬性
  return res
});

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] }); })
}
//規範當前vuex的命名空間
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)
  }
}

store響應式原理

Vue源碼解析(二)中介紹過data的響應式原理:
一、對data進行observe,針對data屬性調用Object.defineProperty設置getter和setter,同時綁定一個dep對象
二、new Watcher(vm, updateComponent, noop)監聽整個dom的變化
三、watcher初始化時調用updateComponent,updateComponent調用render函數更新dom(此時還會將該watcher對象賦值給全局對象Dep.target,進行依賴收集)
四、在watcher對象依賴收集期間,render函數訪問data中的屬性(如本例的data.message),觸發data.message的getter方法,在getter方法中會將data.message綁定的dep對象和wathcer對象創建對應關係(互相加入到對方維護的隊列屬性上)
五、後續data屬性的值變化時dep對象會通知全部依賴此data屬性的watcher對象調用updateComponent方法更新視圖
store響應式的原理也是相似的,new Vuex.Store的過程也會對state進行observesegmentfault

var Store = function Store (options) {
    var state = options.state
    //爲了實現state的響應性new一個vue對象
    // initialize the store vm, which is responsible for the reactivity
    resetStoreVM(this, state);
}
function resetStoreVM (store, state, hot) {
  //new一個vue對象對data(值爲store.state)進行監聽
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed: computed
  });
}

後續實現和上面的data響應式相同promise

mutation

new Vuex.Store的過程當中會將mutation註冊到store._mutations上架構

function registerMutation (store, type, handler, local) {
  var entry = store._mutations[type] || (store._mutations[type] = []);
  //封裝mutation方法並push到store._mutations[type]上
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload);
  });
}

當執行commit方法時就會執行store._mutations上對應的方法app

Store.prototype.commit = function commit (type, payload) {
    var entry = this._mutations[type];
    entry.forEach(function commitIterator (handler) {
        handler(payload);
    });
}

actions

const vueStore = new Vuex.Store({
    state: {
        count: 1,
    },
    mutations: {
        increment (state,payload) {
            state.count+=payload
        }
    },
    actions: {
        increment (context,payload) {
            setTimeout(function () {
                context.commit('increment',payload)
            },1000)
        }
    }
})
vueStore.dispatch('increment',10)

和mutation同樣,new Vuex.Store也會將action註冊到store._actions上,而後經過dispatch調用

function registerAction (store, type, handler, local) {
  var entry = store._actions[type] || (store._actions[type] = []);
  //包裝action方法,傳入store對象的commit方法和state等等
  entry.push(function wrappedActionHandler (payload, cb) {
    var res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb);
    //action的返回不是promise會返回Promise.resolve(res)
    if (!isPromise(res)) {
        res = Promise.resolve(res);
    }
    return res
  });
}

Store.prototype.dispatch = function dispatch (_type, _payload) {
  var entry = this._actions[type];
  return entry.length > 1
    ? Promise.all(entry.map(function (handler) { return handler(payload); }))
    : entry[0](payload)
};

看到action和mutation的源碼實現,你不由要問了,這不是基本同樣的嗎,那幹嗎還要畫蛇添足?
vuex官網的解釋:在 mutation 中混合異步調用會致使你的程序很難調試。例如,當你能調用了兩個包含異步回調的 mutation 來改變狀態,你怎麼知道何時回調和哪一個先回調呢?這就是爲何咱們要區分這兩個概念。在 Vuex 中,mutation 都是同步事務。
知乎上有個問題「vuex中爲何把把異步操做封裝在action,把同步操做放在mutations?「,vue的做者尤雨溪的解釋:事實上在 vuex 裏面 actions 只是一個架構性的概念,並非必須的,說到底只是一個函數,你在裏面想幹嗎均可以,只要最後觸發 mutation 就行。異步競態怎麼處理那是用戶本身的事情。vuex 真正限制你的只有 mutation 必須是同步的這一點(在 redux 裏面就好像 reducer 必須同步返回下一個狀態同樣)。
同步的意義在於這樣每個 mutation 執行完成後均可以對應到一個新的狀態(和 reducer 同樣),這樣 devtools 就能夠打個 snapshot 存下來,而後就能夠隨便 time-travel 了。
我我的的理解這是vuex的使用規範問題,mutation中使用異步也不會有大問題,可是按規範開發能讓項目結構更清晰,調試更方便,下圖是用vue devtool調試的vuex官方例子(https://github.com/vuejs/vuex...),mutation的觸發時間線一目瞭然

clipboard.png

相關文章
相關標籤/搜索