Vuex源碼閱讀分析

Vuex源碼閱讀分析

Vuex是專爲Vue開發的統一狀態管理工具。當咱們的項目不是很複雜時,一些交互能夠經過全局事件總線解決,可是這種觀察者模式有些弊端,開發時可能沒什麼感受,可是當項目變得複雜,維護時每每會摸不着頭腦,若是是後來加入的夥伴更會以爲很無奈。這時候能夠採用Vuex方案,它可使得咱們的項目的數據流變得更加清晰。本文將會分析Vuex的整個實現思路,當是本身讀完源碼的一個總結。vue

目錄結構

vuex目錄結構

  • module:提供對module的處理,主要是對state的處理,最後構建成一棵module treereact

  • plugins:和devtools配合的插件,提供像時空旅行這樣的調試功能。webpack

  • helpers:提供如mapActions、mapMutations這樣的apigit

  • index、index.esm:源碼的主入口,拋出Store和mapActions等api,一個用於commonjs的打包、一個用於es module的打包github

  • mixin:提供install方法,用於注入$storeweb

  • store:vuex的核心代碼ajax

  • util:一些工具函數,如deepClone、isPromise、assertvuex

源碼分析

先從一個簡單的示例入手,一步一步分析整個代碼的執行過程,下面是官方提供的簡單示例chrome

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
複製代碼

1. Vuex的註冊

Vue官方建議的插件使用方法是使用Vue.use方法,這個方法會調用插件的install方法,看看install方法都作了些什麼,從index.js中能夠看到install方法在store.js中拋出,部分代碼以下api

let Vue // bind on install

export function install (_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)
}
複製代碼

聲明瞭一個Vue變量,這個變量在install方法中會被賦值,這樣能夠給當前做用域提供Vue,這樣作的好處是不須要額外import Vue from 'vue' 不過咱們也能夠這樣寫,而後讓打包工具不要將其打包,而是指向開發者所提供的Vue,好比webpack的externals,這裏就不展開了。執行install會先判斷Vue是否已經被賦值,避免二次安裝。而後調用applyMixin方法,代碼以下

// applyMixin
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      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 () {
    const options = this.$options
    // store injection
    // 當咱們在執行new Vue的時候,須要提供store字段
    if (options.store) {
      // 若是是root,將store綁到this.$store
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 不然拿parent上的$store
      // 從而實現全部組件共用一個store實例
      this.$store = options.parent.$store
    }
  }
}
複製代碼

這裏會區分vue的版本,2.x和1.x的鉤子是不同的,若是是2.x使用beforeCreate,1.x即便用_init。當咱們在執行new Vue啓動一個Vue應用程序時,須要給上store字段,根組件從這裏拿到store,子組件從父組件拿到,這樣一層一層傳遞下去,實現全部組件都有$store屬性,這樣咱們就能夠在任何組件中經過this.$store訪問到store

接下去繼續看例子

// store.js
export default new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
  },
  actions:  {
    increment: ({ commit }) => commit('increment'),
    decrement: ({ commit }) => commit('decrement')
  },
  mutations: {
    increment (state) {
      state.count++
    },
    decrement (state) {
      state.count--
    }
  }
})
複製代碼
// app.js
new Vue({
  el: '#app',
  store, // 傳入store,在beforeCreate鉤子中會用到
  render: h => h(Counter)
})
複製代碼

2.store初始化

這裏是調用Store構造函數,傳入一個對象,包括state、actions等等,接下去看看Store構造函數都作了些什麼

export class Store {
  constructor (options = {}) {
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      // 掛載在window上的自動安裝,也就是經過script標籤引入時不須要手動調用Vue.use(Vuex)
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      // 斷言必須使用Vue.use(Vuex),在install方法中會給Vue賦值
      // 斷言必須存在Promise
      // 斷言必須使用new操做符
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

    const {
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    // 這裏進行module收集,只處理了state
    this._modules = new ModuleCollection(options)
    // 用於保存namespaced的模塊
    this._modulesNamespaceMap = Object.create(null)
    // 用於監聽mutation
    this._subscribers = []
    // 用於響應式地監測一個 getter 方法的返回值
    this._watcherVM = new Vue()

    // 將dispatch和commit方法的this指針綁定到store上,防止被修改
    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)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state

    // 這裏是module處理的核心,包括處理根module、action、mutation、getters和遞歸註冊子module
    installModule(this, state, [], this._modules.root)

    // 使用vue實例來保存state和getter
    resetStoreVM(this, state)

    // 插件註冊
    plugins.forEach(plugin => plugin(this))

    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }
}
複製代碼

首先會判斷Vue是否是掛載在window上,若是是的話,自動調用install方法,而後進行斷言,必須先調用Vue.use(Vuex)。必須提供Promise,這裏應該是爲了讓Vuex的體積更小,讓開發者自行提供Promisepolyfill,通常咱們可使用babel-runtime或者babel-polyfill引入。最後斷言必須使用new操做符調用Store函數。

接下去是一些內部變量的初始化 _committing提交狀態的標誌,在_withCommit中,當使用mutation時,會先賦值爲true,再執行mutation,修改state後再賦值爲false,在這個過程當中,會用watch監聽state的變化時是否_committing爲true,從而保證只能經過mutation來修改state _actions用於保存全部action,裏面會先包裝一次 _actionSubscribers用於保存訂閱action的回調 _mutations用於保存全部的mutation,裏面會先包裝一次 _wrappedGetters用於保存包裝後的getter _modules用於保存一棵module樹 _modulesNamespaceMap用於保存namespaced的模塊

接下去的重點是

this._modules = new ModuleCollection(options)
複製代碼
2.1 模塊收集

接下去看看ModuleCollection函數都作了什麼,部分代碼以下

// module-collection.js
export default class ModuleCollection {
  constructor (rawRootModule) {
    // 註冊 root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  get (path) {
    // 根據path獲取module
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

  /* * 遞歸註冊module path是路徑 如 * { * modules: { * a: { * state: {} * } * } * } * a模塊的path => ['a'] * 根模塊的path => [] */
  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      // 斷言 rawModule中的getters、actions、mutations必須爲指定的類型
      assertRawModule(path, rawModule)
    }

    // 實例化一個module
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // 根module 綁定到root屬性上
      this.root = newModule
    } else {
      // 子module 添加其父module的_children屬性上
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 若是當前模塊存在子模塊(modules字段)
    // 遍歷子模塊,逐個註冊,最終造成一個樹
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

// module.js
export default class Module {
  constructor (rawModule, runtime) {
    // 初始化時runtime爲false
    this.runtime = runtime
    // Store some children item
    // 用於保存子模塊
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    // 保存原來的moudle,在Store的installModule中會處理actions、mutations等
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    // 保存state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  addChild (key, module) {
    // 將子模塊添加到_children中
    this._children[key] = module
  }
}
複製代碼

這裏調用ModuleCollection構造函數,經過path的長度判斷是否爲根module,首先進行根module的註冊,而後遞歸遍歷全部的module,子module 添加其父module的_children屬性上,最終造成一棵樹

module-collection

接着,仍是一些變量的初始化,而後

2.2 綁定commit和dispatch的this指針
// 綁定commit和dispatch的this指針
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)
}
複製代碼

這裏會將dispath和commit方法的this指針綁定爲store,好比下面這樣的騷操做,也不會影響到程序的運行

this.$store.dispatch.call(this, 'someAction', payload)
複製代碼
2.3 模塊安裝

接着是store的核心代碼

// 這裏是module處理的核心,包括處理根module、命名空間、action、mutation、getters和遞歸註冊子module
installModule(this, state, [], this._modules.root)
複製代碼
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  /* * { * // ... * modules: { * moduleA: { * namespaced: true * }, * moduleB: {} * } * } * moduleA的namespace -> 'moduleA/' * moduleB的namespace -> '' */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    // 保存namespaced模塊
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    // 非根組件設置state
    // 根據path獲取父state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 當前的module
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      // 使用Vue.set將state設置爲響應式
      Vue.set(parentState, moduleName, module.state)
    })
    console.log('end', store)
  }

  // 設置module的上下文,從而保證mutation和action的第一個參數能拿到對應的state getter等
  const local = module.context = makeLocalContext(store, namespace, path)

  // 逐一註冊mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 逐一註冊action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  // 逐一註冊getter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 逐一註冊子module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
複製代碼

首先保存namespaced模塊到store._modulesNamespaceMap,再判斷是否爲根組件且不是hot,獲得父級module的state和當前module的name,調用Vue.set(parentState, moduleName, module.state)將當前module的state掛載到父state上。接下去會設置module的上下文,由於可能存在namespaced,須要額外處理

// 設置module的上下文,綁定對應的dispatch、commit、getters、state
function makeLocalContext (store, namespace, path) {
  // namespace 如'moduleA/'
  const noNamespace = namespace === ''

  const local = {
    // 若是沒有namespace,直接使用原來的
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      // 統一格式 由於支持payload風格和對象風格
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      // 若是root: true 不會加上namespace 即在命名空間模塊裏提交根的 action
      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
        }
      }
      // 觸發action
      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      // 統一格式 由於支持payload風格和對象風格
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      // 若是root: true 不會加上namespace 即在命名空間模塊裏提交根的 mutation
      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
        }
      }
      // 觸發mutation
      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  // 這裏的getters和state須要延遲處理,須要等數據更新後才進行計算,因此使用getter函數,當訪問的時候再進行一次計算
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace) // 獲取namespace下的getters
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // 若是getter不在該命名空間下 直接return
    if (type.slice(0, splitPos) !== namespace) return

    // 去掉type上的命名空間
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    // 給getters加一層代理 這樣在module中獲取到的getters不會帶命名空間,實際返回的是store.getters[type] type是有命名空間的
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

複製代碼

這裏會判斷modulenamespace是否存在,不存在不會對dispatchcommit作處理,若是存在,給type加上namespace,若是聲明瞭{root: true}也不作處理,另外gettersstate須要延遲處理,須要等數據更新後才進行計算,因此使用Object.defineProperties的getter函數,當訪問的時候再進行計算

再回到上面的流程,接下去是逐步註冊mutation action getter 子module,先看註冊mutation

/* * 參數是store、mutation的key(namespace處理後的)、handler函數、當前module上下文 */
function registerMutation (store, type, handler, local) {
  // 首先判斷store._mutations是否存在,不然給空數組
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 將mutation包一層函數,push到數組中
  entry.push(function wrappedMutationHandler (payload) {
    // 包一層,commit執行時只須要傳入payload
    // 執行時讓this指向store,參數爲當前module上下文的state和用戶額外添加的payload
    handler.call(store, local.state, payload)
  })
}
複製代碼

mutation的註冊比較簡單,主要是包一層函數,而後保存到store._mutations裏面,在這裏也能夠知道,mutation能夠重複註冊,不會覆蓋,當用戶調用this.$store.commit(mutationType, payload)時會觸發,接下去看看commit函數

// 這裏的this已經被綁定爲store
commit (_type, _payload, _options) {
  // 統一格式,由於支持對象風格和payload風格
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  // 獲取當前type對應保存下來的mutations數組
  const entry = this._mutations[type]
  if (!entry) {
    // 提示不存在該mutation
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  // 包裹在_withCommit中執行mutation,mutation是修改state的惟一方法
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      // 執行mutation,只須要傳入payload,在上面的包裹函數中已經處理了其餘參數
      handler(payload)
    })
  })
  // 執行mutation的訂閱者
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    // 提示silent參數已經移除
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools'
    )
  }
}
複製代碼

首先對參數進行統一處理,由於是支持對象風格和載荷風格的,而後拿到當前type對應的mutation數組,使用_withCommit包裹逐一執行,這樣咱們執行this.$store.commit的時候會調用對應的mutation,並且第一個參數是state,而後再執行mutation的訂閱函數

接下去看action的註冊

/* * 參數是store、type(namespace處理後的)、handler函數、module上下文 */
function registerAction (store, type, handler, local) {
  // 獲取_actions數組,不存在即賦值爲空數組
  const entry = store._actions[type] || (store._actions[type] = [])
  // push到數組中
  entry.push(function wrappedActionHandler (payload, cb) {
    // 包一層,執行時須要傳入payload和cb
    // 執行action
    let 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,這樣就支持promise的鏈式調用
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      // 使用devtool處理一次error
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}
複製代碼

mutation很相似,使用函數包一層而後push到store._actions中,有些不一樣的是執行時參數比較多,這也是爲何咱們在寫action時能夠解構拿到commit等的緣由,而後再將返回值promisify,這樣能夠支持鏈式調用,但實際上用的時候最好仍是本身返回promise,由於一般action是異步的,比較多見是發起ajax請求,進行鏈式調用也是想當異步完成後再執行,具體根據業務需求來。接下去再看看dispatch函數的實現

// this已經綁定爲store
dispatch (_type, _payload) {
  // 統一格式
  const { type, payload } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  // 獲取actions數組
  const entry = this._actions[type]
  // 提示不存在action
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }
  // 執行action的訂閱者
  this._actionSubscribers.forEach(sub => sub(action, this.state))
  // 若是action大於1,須要用Promise.all包裹
  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}
複製代碼

這裏和commit也是很相似的,對參數統一處理,拿到action數組,若是長度大於一,用Promise.all包裹,不過直接執行,而後返回執行結果。

接下去是getters的註冊和子module的註冊

/* * 參數是store、type(namesapce處理後的)、getter函數、module上下文 */
function registerGetter (store, type, rawGetter, local) {
  // 不容許重複定義getters
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 包一層,保存到_wrappedGetters中
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // 執行時傳入store,執行對應的getter函數
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
複製代碼

首先對getters進行判斷,和mutation是不一樣的,這裏是不容許重複定義的,而後包裹一層函數,這樣在調用時只須要給上store參數,而用戶的函數裏會包含local.state local.getters store.state store.getters

// 遞歸註冊子module
installModule(store, rootState, path.concat(key), child, hot)
複製代碼
使用vue實例保存state和getter

接着再繼續執行resetStoreVM(this, state),將stategetters存放到一個vue實例中,

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
複製代碼
function resetStoreVM (store, state, hot) {
  // 保存舊vm
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 循環全部getters,經過Object.defineProperty方法爲getters對象創建屬性,這樣就能夠經過this.$store.getters.xxx訪問
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // getter保存在computed中,執行時只須要給上store參數,這個在registerGetter時已經作處理
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 使用一個vue實例來保存state和getter
  // silent設置爲true,取消全部日誌警告等
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // 恢復用戶的silent設置
  Vue.config.silent = silent

  // enable strict mode for new vm
  // strict模式
  if (store.strict) {
    enableStrictMode(store)
  }
  // 若存在oldVm,解除對state的引用,等dom更新後把舊的vue實例銷燬
  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
複製代碼

這裏會從新設置一個新的vue實例,用來保存stategettergetters保存在計算屬性中,會給getters加一層代理,這樣能夠經過this.$store.getters.xxx訪問到,並且在執行getters時只傳入了store參數,這個在上面的registerGetter已經作了處理,也是爲何咱們的getters能夠拿到state getters rootState rootGetters的緣由。而後根據用戶設置開啓strict模式,若是存在oldVm,解除對state的引用,等dom更新後把舊的vue實例銷燬

function enableStrictMode (store) {
  store._vm.$watch(
    function () {
      return this._data.$$state
    },
    () => {
      if (process.env.NODE_ENV !== 'production') {
        // 不容許在mutation以外修改state
        assert(
          store._committing,
          `Do not mutate vuex store state outside mutation handlers.`
        )
      }
    },
    { deep: true, sync: true }
  )
}
複製代碼

使用$watch來觀察state的變化,若是此時的store._committing不會true,即是在mutation以外修改state,報錯。

再次回到構造函數,接下來是各種插件的註冊

2.4 插件註冊
// apply plugins
plugins.forEach(plugin => plugin(this))

if (Vue.config.devtools) {
  devtoolPlugin(this)
}
複製代碼

到這裏store的初始化工做已經完成。大概長這個樣子

store

看到這裏,相信已經對store的一些實現細節有所瞭解,另外store上還存在一些api,可是用到的比較少,能夠簡單看看都有些啥

2.5 其餘api
  • watch (getter, cb, options)

用於監聽一個getter值的變化

watch (getter, cb, options) {
  if (process.env.NODE_ENV !== 'production') {
    assert(
      typeof getter === 'function',
      `store.watch only accepts a function.`
    )
  }
  return this._watcherVM.$watch(
    () => getter(this.state, this.getters),
    cb,
    options
  )
}
複製代碼

首先判斷getter必須是函數類型,使用$watch方法來監控getter的變化,傳入stategetters做爲參數,當值變化時會執行cb回調。調用此方法返回的函數可中止偵聽。

  • replaceState(state)

用於修改state,主要用於devtool插件的時空穿梭功能,代碼也至關簡單,直接修改_vm.$$state

replaceState (state) {
  this._withCommit(() => {
    this._vm._data.$$state = state
  })
}
複製代碼
  • registerModule (path, rawModule, options = {})

用於動態註冊module

registerModule (path, rawModule, options = {}) {
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(
      path.length > 0,
      'cannot register the root module by using registerModule.'
    )
  }

  this._modules.register(path, rawModule)
  installModule(
    this,
    this.state,
    path,
    this._modules.get(path),
    options.preserveState
  )
  // reset store to update getters...
  resetStoreVM(this, this.state)
  }
複製代碼

首先統一path的格式爲Array,接着是斷言,path只接受StringArray類型,且不能註冊根module,而後調用store._modules.register方法收集module,也就是上面的module-collection裏面的方法。再調用installModule進行模塊的安裝,最後調用resetStoreVM更新_vm

  • unregisterModule (path)

根據path註銷module

unregisterModule (path) {
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
  }

  this._modules.unregister(path)
  this._withCommit(() => {
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1])
  })
  resetStore(this)
}

複製代碼

registerModule同樣,首先統一path的格式爲Array,接着是斷言,path只接受StringArray類型,接着調用store._modules.unregister方法註銷module,而後在store._withCommit中將該modulestate經過Vue.delete移除,最後調用resetStore方法,須要再看看resetStore的實現

function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}
複製代碼

這裏是將_actions _mutations _wrappedGetters _modulesNamespaceMap都清空,而後調用installModuleresetStoreVM從新進行所有模塊安裝和_vm的設置

  • _withCommit (fn)

用於執行mutation

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}
複製代碼

在執行mutation的時候,會將_committing設置爲true,執行完畢後重置,在開啓strict模式時,會監聽state的變化,當變化時_committing不爲true時會給出警告

3. 輔助函數

爲了不每次都須要經過this.$store來調用api,vuex提供了mapState mapMutations mapGetters mapActions createNamespacedHelpers 等api,接着看看各api的具體實現,存放在src/helpers.js

3.1 一些工具函數

下面這些工具函數是輔助函數內部會用到的,能夠先看看功能和實現,主要作的工做是數據格式的統1、和經過namespace獲取module

/** * 統一數據格式 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ] * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ] * @param {Array|Object} map * @return {Object} */
function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

/** * 返回一個函數,接受namespace和map參數,判斷是否存在namespace,統一進行namespace處理 * @param {Function} fn * @return {Function} */
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

/** * 根據namespace獲取module * @param {Object} store * @param {String} helper * @param {String} namespace * @return {Object} */
function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (process.env.NODE_ENV !== 'production' && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}
複製代碼
3.2 mapState

爲組件建立計算屬性以返回 store 中的狀態

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
  // 返回一個對象,值都是函數
  res[key] = function mappedState () {
    let state = this.$store.state
    let getters = this.$store.getters
    if (namespace) {
      // 若是存在namespace,拿該namespace下的module
      const module = getModuleByNamespace(this.$store, 'mapState', namespace)
      if (!module) {
        return
      }
      // 拿到當前module的state和getters
      state = module.context.state
      getters = module.context.getters
    }
    // Object類型的val是函數,傳遞過去的參數是state和getters
    return typeof val === 'function'
      ? val.call(this, state, getters)
      : state[val]
  }
  // mark vuex getter for devtools
  res[key].vuex = true
  })
  return res
})
複製代碼

mapStatenormalizeNamespace的返回值,從上面的代碼能夠看到normalizeNamespace是進行參數處理,若是存在namespace便加上命名空間,對傳入的states進行normalizeMap處理,也就是數據格式的統一,而後遍歷,對參數裏的全部state都包裹一層函數,最後返回一個對象

大概是這麼回事吧

export default {
  // ...
  computed: {
    ...mapState(['stateA'])
  }
  // ...
}
複製代碼

等價於

export default {
  // ...
  computed: {
    stateA () {
      return this.$store.stateA
    }
  }
  // ...
}
複製代碼
3.4 mapGetters

store 中的 getter 映射到局部計算屬性中

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    // this namespace has been mutate by normalizeNamespace
    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]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
複製代碼

一樣的處理方式,遍歷getters,只是這裏須要加上命名空間,這是由於在註冊時_wrapGetters中的getters是有加上命名空間的

3.4 mapMutations

建立組件方法提交 mutation

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    // 返回一個對象,值是函數
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      // 執行mutation,
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})
複製代碼

和上面都是同樣的處理方式,這裏在判斷是否存在namespace後,commit是不同的,上面能夠知道每一個module都是保存了上下文的,這裏若是存在namespace就須要使用那個另外處理的commit等信息,另外須要注意的是,這裏不須要加上namespace,這是由於在module.context.commit中會進行處理,忘記的能夠往上翻,看makeLocalContextcommit的處理

3.5 mapAction

建立組件方法分發 action

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const 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基本同樣的處理方式

4. 插件

Vuex中能夠傳入plguins選項來安裝各類插件,這些插件都是函數,接受store做爲參數,Vuex中內置了devtoollogger兩個插件,

// 插件註冊,全部插件都是一個函數,接受store做爲參數
plugins.forEach(plugin => plugin(this))

// 若是開啓devtools,註冊devtool
if (Vue.config.devtools) {
  devtoolPlugin(this)
}
複製代碼
// devtools.js
const devtoolHook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook

  // 觸發vuex:init
  devtoolHook.emit('vuex:init', store)

  // 時空穿梭功能
  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  // 訂閱mutation,當觸發mutation時觸發vuex:mutation方法,傳入mutation和state
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}
複製代碼

總結

到這裏基本vuex的流程源碼已經分析完畢,分享下本身看源碼的思路或者過程,在看以前先把官網的文檔再仔細過一遍,而後帶着問題來看源碼,這樣效率會比較高,利用chrome在關鍵點打開debugger,一步一步執行,看源碼的執行過程,數據狀態的變換。並且能夠暫時屏蔽一些沒有反作用的代碼,好比assert,這些函數通常都不會影響流程的理解,這樣也能夠儘可能減小源碼行數。剩下的就是耐心了,前先後後斷斷續續看了不少次,總算也把這份分享寫完了,因爲水平關係,一些地方可能理解錯誤或者不到位,歡迎指出。

原文地址

相關文章
相關標籤/搜索