vuex 2.0源碼解讀(一)

轉載請註明出處 https://segmentfault.com/a/11...javascript

vuex2.0 和 vuex1.x 相比,API改變的仍是不少的,但基本思想沒什麼改變。vuex2.0 的源碼挺短,四五百行的樣子,兩三天就能讀完。我是國慶期間斷斷續續看完的,寫一下本身的理解。這裏使用的vuex版本是 2.0.0-rc6。在看這篇文章以前,建議先看一遍官方的vuex2.0 文檔,瞭解基本概念,否則以後的內容理解起來會很費勁。html

引入 vuex 文件

要想使用 vuex 有幾種方式, 這裏不細講。vue

  • CDNjava

<script src='path/vue.js'><script> <!-- 必須先引入 vue -->
<script src='path/vuex.js'></script> <!-- 平時學習時建議使用完整版 -->
  • ES6語法 + webpackreact

import Vuex from 'vuex'
var store = new Vuex.Store({})
Vuex.mapState({})

或者webpack

import { Store, mapState } from 'vuex'
var store = new Store({})
mapState({})

Store構造函數

vuex 只暴露出了6個方法,分別是git

var index = {
Store: Store,
install: install,
mapState: mapState,
mapMutations: mapMutations,
mapGetters: mapGetters,
mapActions: mapActions
}

return index;

其中 install 方法是配合 Vue.use 方法使用的,用於在 Vue 中註冊 Vuex ,和數據流關係不大。其餘的幾種方法就是咱們經常使用的。github

先看看 Store 方法,學習 vuex 最早接觸到的就是 new Store({}) 了。那麼就先看看這個 Store 構造函數。web

var Store = function Store (options) {
  var this$1 = this; // 指向返回的store實例
  if ( options === void 0 ) options = {};

  // 使用構造函數以前,必須保證vuex已註冊,使用Vue.use(Vuex)註冊vuex
  assert(Vue, "must call Vue.use(Vuex) before creating a store instance.")
  // 須要使用的瀏覽器支持Promise
  assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser.")

  var state = options.state; if ( state === void 0 ) state = {};
  var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
  var strict = options.strict; if ( strict === void 0 ) strict = false;

  // store internal state
  // store的內部狀態(屬性)
  this._options = options
  this._committing = false
  this._actions = Object.create(null)  // 保存actions
  this._mutations = Object.create(null) // 保存mutations
  this._wrappedGetters = Object.create(null) // 保存包裝後的getters
  this._runtimeModules = Object.create(null) 
  this._subscribers = []
  this._watcherVM = new Vue()

  // bind commit and dispatch to self
  var store = this
  var ref = this;
  var dispatch = ref.dispatch; // 引用的是Store.prototype.dispatch
  var commit = ref.commit; // 引用的是Store.prototype.commit 
  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)
  }

  // strict mode
  this.strict = strict // 是否開啓嚴格模式

  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  // 初始化 root module
  // 同時也會遞歸初始化全部子module
  // 而且收集全部的getters至this._wrappedGetters
  installModule(this, state, [], options)

  // initialize the store vm, which is responsible for the reactivity
  // (also registers _wrappedGetters as computed properties)
  // 重置vm實例狀態
  // 同時在這裏把getters轉化爲computed(計算屬性)
  resetStoreVM(this, state)

  // apply plugins
  plugins.concat(devtoolPlugin).forEach(function (plugin) { return plugin(this$1); })
};

一開始會有兩個判斷條件,判斷 vuex 是否已經註冊,和當前瀏覽器是否支持 Promise, assert 方法也挺簡單,若是傳入的第一個參數爲假值,則拋出一個錯誤。vuex

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

接着往下看,接着會定義 state, plugins,strict三個變量,分別是你傳入的 options 對應的選項。以後就是定義返回的 store 實例的一些內部狀態。先不要管它們具體是什麼,這個以後會慢慢講,這裏先看看 Store 構造函數都作了些什麼。再以後就是綁定 dispatchcommit 方法到 store 實例上。接下來就是整個 vuex 的核心方法 installModule 了,以後重置 vm 實例的狀態。

簡單點說,當你使用 Store 構造函數,它實際上作了這麼幾件事,首先定義給 store 實例定義一些內部屬性,以後就是綁定 dispatchcommit 的上下文對象永遠是 store 實例上,以後 installModule 根據傳入的 options ‘充實’ 內部狀態等等。

installModule

很重要的一個方法。貼上代碼

/*
 * store 就是 store 實例
 * rootState 是使用構造函數options中定義的 state 對象
 * path 路徑
 * module 傳入的options
 */
function installModule (store, rootState, path, module, hot) {
  var isRoot = !path.length  // 是不是root
  var state = module.state;
  var actions = module.actions;
  var mutations = module.mutations;
  var getters = module.getters;
  var modules = module.modules;

  // set state
  if (!isRoot && !hot) { 
    // 找到要註冊的 path 的上一級 state
    var parentState = getNestedState(rootState, path.slice(0, -1))
    // 定義 module 的 name
    var moduleName = path[path.length - 1]
    // store._withCommit方法以後會講
    // 這裏先理解爲 執行傳入的函數
    store._withCommit(function () {
      // 使用Vue.set方法
      // parentState[moduleName] = state
      // 而且state變成響應式的
      Vue.set(parentState, moduleName, state || {})
    })
  }
  // 以後設置 mutations, actions, getters, modules
  if (mutations) {
    Object.keys(mutations).forEach(function (key) {
      registerMutation(store, key, mutations[key], path)
    })
  }

  if (actions) {
    Object.keys(actions).forEach(function (key) {
      registerAction(store, key, actions[key], path)
    })
  }

  if (getters) {
    wrapGetters(store, getters, path)
  }

  if (modules) {
    Object.keys(modules).forEach(function (key) {
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}

這裏有個很重要的概念要理解,什麼是 path. vuex 的一個 store 實例能夠拆分紅不少個 module ,不一樣的 module 能夠理解成一個子代的 store 實例(事實上,module 確實和 store 具備同樣的結構),這是一種模塊化的概念。所以這裏的 path 能夠理解成是表示一種層級關係,當你有了一個 root state 以後,根據這個 root state 和 path 能夠找到 path 路徑對應的一個 local state, 每個 module 下的 mutations 和 actions 改變的都是這個local state,而不是 root state.

這裏在 Store 構造函數裏傳入的 path 路徑爲 [],說明註冊的是一個root state. 再看看上一段代碼的最後

if (modules) {
    Object.keys(modules).forEach(function (key) {
      installModule(store, rootState, path.concat(key), modules[key], hot)
   })
 }

若是傳入的options 中有 modules 選項,重複調用 installModule, 這裏傳入的函數的 path 參數是 path.concat(key), 因此應該很好理解了。

簡單看一下 getNestedState 方法。

/*
 * state: Object, path: Array
 * 假設path = ['a', 'b', 'c']
 * 函數返回結果是state[a][b][c]
 */
function getNestedState (state, path) {
  return path.length
    ? path.reduce(function (state, key) { return state[key]; }, state)
    : state
}

reduce 方法接受一個函數,函數的參數分別是上一次計算後的值,和當前值,reduce 方法的第二個參數 state 是初始計算值。

registerMutation

若是 mutations 選項存在,那麼就註冊這個 mutations ,看一下它的實現。

/*
 * 註冊mutations,也就是給store._mutations添加屬性
 * 這裏說一下handler
 * handler 是 mutations[key]
 * 也就是傳入 Store構造函數的 mutations 
 */
function registerMutation (store, type, handler, path) {
  if ( path === void 0 ) path = [];

  // 在_mutations中找到對應type的mutation數組
  // 若是是第一次建立,就初始化爲一個空數組
  var entry = store._mutations[type] || (store._mutations[type] = [])
  // 推入一個對原始mutations[key]包裝過的函數
  entry.push(function wrappedMutationHandler (payload) {
    // store.state表示root state, 先獲取path路徑下的local state
    // mutation應該是對path路徑下的state的修改
    // 函數接受一個payload參數
    // 初始的handler,接受一個state he payload 參數
    handler(getNestedState(store.state, path), payload)
  })
}

邏輯很簡單,全部的 mutations 都通過處理後,保存在了 store._mutations 對象裏。 _mutations 的結構爲

_mutations: {
    type1: [wrappedFunction1, wrappedFuction2, ...],
    type2: [wrappedFunction1, wrappedFuction2, ...],
    ...
}

registerAction

function registerAction (store, type, handler, path) {
  if ( path === void 0 ) path = [];

  var entry = store._actions[type] || (store._actions[type] = [])
  var dispatch = store.dispatch;
  var commit = store.commit;
  entry.push(function wrappedActionHandler (payload, cb) {
    var res = handler({
      dispatch: dispatch,
      commit: commit,
      getters: store.getters,
      state: getNestedState(store.state, path),
      rootState: store.state
    }, payload, cb)
    // 若是 res 不是 promise 對象 ,將其轉化爲promise對象
    // 這是由於store.dispatch 方法裏的 Promise.all()方法。
    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
    }
  })
}

這裏一樣是'充實' store._actions 對象,每一種 action type 都對應一個數組,數組裏存放的包裝後的 handler 函數,因爲涉及到 promise,這裏我想在下一節結合 store 的 dispatch 實例方法一塊兒講。

wrapGetters

/*
 * 包裝getters函數
 * store增長一個 _wrappedGetters 屬性
 * moduleGetters: 傳入的options.getters
 * modulePath: 傳入 installModule 函數的 path 
 */
function wrapGetters (store, moduleGetters, modulePath) {
  Object.keys(moduleGetters).forEach(function (getterKey) {
    var rawGetter = moduleGetters[getterKey] // 原始的getter
    if (store._wrappedGetters[getterKey]) { // 若是已經存在,警告
      console.error(("[vuex] duplicate getter key: " + getterKey))
      return
    }
    store._wrappedGetters[getterKey] = function wrappedGetter (store) {
        // 接受三個參數
        // local state
        //  全局的 getters
        // 全局的 state
      return rawGetter(
        getNestedState(store.state, modulePath), // local state
        store.getters, // getters
        store.state // root state
      )
    }
  })
}

注意 這裏的全部 getters 都儲存在了全局的一個 _wrappedGetters 對象中,一樣屬性名是各個 getterKey ,屬性值一樣是一個函數,執行這個函數,將會返回原始 getter 的執行結果。

install modules

if (modules) {
    Object.keys(modules).forEach(function (key) {
      installModule(store, rootState, path.concat(key), modules[key], hot)
   })
 }

若是 options 中有 modules 選項,那麼就遞歸調用 installModule 方法,注意這裏的 path 改變。

resetStoreVM

function resetStoreVM (store, state) {
  var oldVm = store._vm // 原來的_vm

  // bind store public getters
  store.getters = {} // 初始化 store 的 getters 屬性爲一個空數組。
  var wrappedGetters = store._wrappedGetters
  var computed = {} 
  Object.keys(wrappedGetters).forEach(function (key) {
    var fn = wrappedGetters[key]
    // use computed to leverage its lazy-caching mechanism
    // 將wrappedGetter中的屬性轉移到 computed 中
    computed[key] = function () { return fn(store); }
    // store.getters[key] = store._vm[key]
    Object.defineProperty(store.getters, key, {
      get: function () { return store._vm[key]; }
    })
  })
  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  // 設爲 silent 模式
  var silent = Vue.config.silent
  Vue.config.silent = true
  // 初始化一個 store._vm 實例
  store._vm = new Vue({
    data: { state: state },
    computed: computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  // 啓用嚴格模式
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation.
    store._withCommit(function () {
      oldVm.state = null
    })
    // 執行destroy 方法,通知全部的watchers 改變,並從新計算getters的值。
    Vue.nextTick(function () { return oldVm.$destroy(); })
  }
}

這個方法在 installModule 方法以後執行,來看看它都作了什麼。簡單點說,就是給 store 增長了一個 _vm 屬性,指向一個新的 vue 實例,傳入的選項包括一個 state 和 computed, computed 來自store 的 getters 屬性。同時給 store 增長了一個 getters 屬性,且 store.getters[key] = store._vm[key]

mapState

在講 mapState 以前,先說一下基礎方法 normalizeMap

/*
 * 若是map是一個數組 ['type1', 'type2', ...]
 * 轉化爲[
 *   {
 *     key: type1,
 *     val: type1
 *   },
 *   {
 *     key: type2,
 *     val: type2
 *   },
 *   ...
 * ]
 * 若是map是一個對象 {type1: fn1, type2: fn2, ...}
 * 轉化爲 [
 *   {
 *     key: type1,
 *     val: fn1
 *   },
 *   {
 *     key: type2,
 *     val: fn2
 *   },
 *   ...
 * ]
 */
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] }); })
}

normalizeMap 函數接受一個對象或者數組,最後都轉化成一個數組形式,數組元素是包含 key 和 value 兩個屬性的對象。

再來看看 mapState 方法。

/*
 * states: Object | Array
 * 返回一個對象
 * 對象的屬性名對應於傳入的 states 的屬性名或者數組元素
 * 屬性值都是一個函數
 * 執行這個函數的返回值根據 val 的不一樣而不一樣
 */
function mapState (states) {
  var res = {}
  normalizeMap(states).forEach(function (ref) {
    var key = ref.key; 
    var val = ref.val; 

    res[key] = function mappedState () {
      return typeof val === 'function' // 若是是函數,返回函數執行後的結果
        ? val.call(this, this.$store.state, this.$store.getters)
        : this.$store.state[val] // 若是不是函數,而是一個字符串,直接在state中讀取。
    }
  })
  return res 
}

mapState 函數執行的結果是返回一個對象,屬性名對應於傳入的 states 對象或者數組元素。屬性值是一個函數,執行這個函數將返回相應的 state .

mapMutations

/*
 * mutations: Array
 * 返回一個對象
 * 屬性名爲 mutation 類型
 * 屬性值爲一個函數
 * 執行這個函數後將觸發指定的 mutation 
 */
function mapMutations (mutations) {
  var res = {}
  normalizeMap(mutations).forEach(function (ref) {
    var key = ref.key; // mutation type
    var val = ref.val; // mutation type

    res[key] = function mappedMutation () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ]; // 一個數組緩存傳入的參數

      // val做爲commit函數的第一個參數type, 剩下的參數依次是payload 和 options
      return this.$store.commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
}

注意這裏傳入的 mutations 只能是一個數組,數組元素的 mutation type . 函數的做用的也很簡單,傳入一個 mutations 數組,返回一個對象,屬性名是 mutation 的類型,屬性值是一個函數,執行這個函數,將調用 commit 來觸發對應的 mutation 從而改變state。另外注意這裏的 this 指向的 store 的 _vmmapState 是在 Vue 實例中調用的。

mapActions

function mapActions (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 ];

      return this.$store.dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
}

mapActions 函數和 mapMutations 函數幾乎一模一樣。惟一的區別即便這裏應該使用 dispatch 方法來觸發 action.

mapGetters

/*
 * getters: Array
 */
function mapGetters (getters) {
  var res = {}
  normalizeMap(getters).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val; 

    res[key] = function mappedGetter () {
      // 若是在getters中不存在,報錯
      if (!(val in this.$store.getters)) {
        console.error(("[vuex] unknown getter: " + val))
      }
      // 根據 val 在 getters 對象裏找對應的屬性值
      return this.$store.getters[val]
    }
  })
  return res
}

這裏 getters 一樣接受一個數組,一樣返回一個對象。

以上講了四種 map*** 方法,這四種方法能夠都返回一個對象,所以能夠 ES6 新特性 ... 解構符。如

{
    ...mapState(options)
}

關於 ... 解構符號, 舉個小例子就明白了

var obj1 = {
    a: 1,
    b: 2,
    c: 3
}
var obj2 = {
    ...obj1,
    d: 4
}
// obj2 = { a: 1, b: 2, c: 3, d: 4 }
// 一樣能夠用於數組
var arr1 = ['a', 'b', 'c']
var arr2 = [...arr1, 'd']
// arr2 = ['a', 'b', 'c', 'd']

install

install 方法與 vuex 數據流關係不大,主要是用於在 Vue 中註冊 Vuex,這裏爲了保持篇幅的完整性,簡單介紹一下。

function install (_Vue) {
  if (Vue) { 
  // 報錯,已經使用了 Vue.use(Vuex)方法註冊了
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

// auto install in dist mode
// 在瀏覽器環境寫,會自動調用 install 方法
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

沒什麼難度,那就再看一下 applyMixin 方法

function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0])
  // 檢查使用的 Vue 版本,初始化時的生命週期鉤子函數是 init 仍是 beforeCreate
  if (version >= 2) {
    var usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    // 保存以前的 Vue.prototype._init
    var _init = Vue.prototype._init

    // 從新設置Vue.prototype._init
    Vue.prototype._init = function (options) {
      if ( options === void 0 ) options = {};
      //  初始化時先初始化vuexInit
      // options.init: Array  表示一組要執行的鉤子函數
      //  options.init鉤子函數以前加上了 vueInit
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }
  
  /*
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    var options = this.$options
    // store injection
    // 若是本身有store選項,用本身的
    // 不然查找父組件的
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

註釋寫的很清楚了,那麼再看看什麼有是 vuexInit 函數, vuexInit 函數是 vuex 的生命週期鉤子函數。函數傳遞了兩個信息,(1)子組件能夠有本身單獨的store,可是通常不這麼作 (2) 若是子組件沒有本身的 store ,就會查找父組件的。這也印證了 根組件的 store 會注入到全部的後代組件。

小結

以上講解了 Vuex 暴露出的 6 種方法,也是 Vuex 裏的用的最多的幾種方法,以後還會解讀一下其餘一些方法,好比 store 的一些實例方法。

另外本文的 github 的地址爲: learnVuex2.0

轉載請註明原連接

全文完

相關文章
相關標籤/搜索