Vuex源碼 - 結合小實例解讀

做爲Vue全家桶之一的vuex,在Vue框架中起着很重要的角色。Vuex源碼篇幅雖然很少,但解讀起來仍是須要下一些功夫的。下面咱們就參照vuex官方指南文檔內容進行分析。 vue

vuex是什麼?

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。 react

解決了什麼問題?

Vuex 能夠幫助咱們管理共享狀態,並附帶了更多的概念和框架。這須要對短時間和長期效益進行權衡。
若是隻是簡單的單頁應用,最好不要使用 Vuex,使用 Vuex 多是繁瑣冗餘的,一個簡單的 store 模式就足夠了。可是,若是構建一箇中大型單頁應用,須要考慮如何更好地在組件外部管理狀態,Vuex 將會成爲最佳選擇。git

vuex使用場景.png

Vuex設計思想是什麼?

Vuex是一個專門爲Vue.js框架設計的、用於對Vue.js應用程序進行狀態管理的庫,它借鑑了Flux、redux的基本思想,將共享的數據抽離到全局,以一個單例存放,同時利用Vue.js的響應式機制來進行高效的狀態管理與更新。正是由於Vuex使用了Vue.js內部的「響應式機制」,因此Vuex是一個專門爲Vue.js設計並與之高度契合的框架(優勢是更加簡潔高效,缺點是隻能跟Vue.js搭配使用)。
vuex狀態自管理應用包含如下幾個部分:
state:驅動應用的數據源;
view:以聲明方式將 state 映射到視圖;
actions:響應在 view 上的用戶輸入致使的狀態變化。
來看一下「單向數據流」理念的簡單示意圖:
github


Vuex的完整數據流程圖

Vuex最佳實踐演示 - 一個小實例:

vuex在項目中應用的目錄,在src下建立store文件:vuex

├── store  
│   ├── modules
│       ├── todos.js  
│   ├── getter.js   
│   ├── index.js   
├── APP.vue     
└── main.js
複製代碼

index.jsredux

//index.js
import Vue from 'vue';
import Vuex from 'vuex';
import getters from './getters'
import app from './modules/todos'
//Load Vuex
Vue.use(Vuex)
//create store
export default new Vuex.Store({
  modules: { app },
  getters
})
複製代碼

getters.jswindows

//getters.js
const getters = {
  allTodos: (state) => state.app.todos,
  red: (state) => state.app.red,
  blue: (state) => state.app.blue
}
export default getters
複製代碼

todos.js數組

// todos.js
const app = {
	state: {
    todos: [{
      id: 1,
      title: 'one'
    },{
      id: 2,
      title: 'two'
    }],
    red: 0,
    blue: 0
 },
  mutations: {
    ADD_COUNT: (state) => {
      state.red++,
      state.blue++
    },
  MINUS_COUNT: (state)=> {
      state.red--,
      state.blue--
    }
  },
  actions: {
    addCounts: ({commit})=>{
      commit('ADD_COUNT')
    },
    minusCounts: ({commit})=>{
      commit('MINUS_COUNT')
    }
  }
}
export default app
複製代碼

main.js緩存

//main.js
import store from './store'

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')
複製代碼

APP.vuebash

import { mapGetters } from 'vuex'
......
export default {
  name: 'app',
  computed: mapGetters(['allTodos'])
}
複製代碼

子組件應用:

//組件應用
computed:{
		red(){
			return this.$store.state.app.red
		},
		blue(){
			return this.$store.state.app.blue
		}
	},
	methods: {
		addOne() {
			this.$store.dispatch('addCounts')
		},
		minusOne() {
			this.$store.dispatch('minusCounts')
		},
	}
複製代碼

Vuex核心源碼分析:

先看下vuex官方指南文檔,再結合上面的小實例對核心源碼進行分析。
    vuex官網指南:vuex.vuejs.org/zh/guide/
    vuex源碼:github.com/vuejs/vuex
    git上下載的源碼文件不少,可是核心執行源碼存在/src裏,源碼文件很少,目錄劃分也很清晰,每一個文件都有着各自的功能:

  • module:提供 module 對象與 module 對象樹的建立功能;
  • plugins:提供開發輔助插件,如 「時光穿梭」 功能,state 修改的日誌記錄功能等;
  • helpers.js:提供 action、mutations 以及 getters 的查找 API;
  • index.js:是源碼主入口文件,提供 store 的各 module 構建安裝;
  • mixin.js:提供了 store 在 Vue 實例上的裝載注入;
  • store.js:實現Store構造方法;
  • util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法。

Vuex源碼目錄結構

├── module  
│   ├── module-collection.js  
│   ├── module.js   
├── plugins     // 插件
│   ├── devtool.js  
│   ├── logger.js  
├── helpers.js  //輔助函數
├── index.esm.js 
├── index.js    //入口文件
├── mixin.js 
├── store.js 
└── util.js
複製代碼

初始化裝載與注入

index.js做爲入口文件,包含了全部的核心代碼引用:

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}
複製代碼

constructor - 構造方法

在實例的store文件夾的index.js裏經過new store來註冊store,下面是具體Store構造方法,能夠結合代碼註釋看:

constructor (options = {}) {
		......
    // store internal state
    /* 用來判斷嚴格模式下是不是用mutation修改state的 */
    this._committing = false
    /* 存放action */
    this._actions = Object.create(null)
    this._actionSubscribers = []
    /* 存放mutation */
    this._mutations = Object.create(null)
    /* 存放getter */
    this._wrappedGetters = Object.create(null)
    /* module收集器 */
    this._modules = new ModuleCollection(options)
    /* 根據namespace存放module */
    this._modulesNamespaceMap = Object.create(null)
    /* 存放訂閱者 */
    this._subscribers = []
    /* 用以實現Watch的Vue實例 */
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    /*將dispatch與commit調用的this綁定爲store對象自己,不然在組件內部this.dispatch時的this會指向組件的vm*/
    const store = this
    const { dispatch, commit } = this
    /* 爲dispatch與commit綁定this(Store實例自己) */
    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
    /*嚴格模式(使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤)*/
    this.strict = strict

    const state = this._modules.root.state

    /*初始化根module,這也同時遞歸註冊了全部子modle,收集全部module的getter到_wrappedGetters中去,this._modules.root表明根module才獨有保存的Module對象*/
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    /* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */
    resetStoreVM(this, state)

    // apply plugins  應用插件
    plugins.forEach(plugin => plugin(this))

    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }
複製代碼

dispatch - 分發Action

在實例的子組件應用裏會用到dispatch,dispatch的功能是觸發並傳遞一些參數(payload)給對應 type 的 action。在dispatch 中,先進行參數的適配處理,而後判斷 action type 是否存在,若存在就逐個執行。

/* 調用action的dispatch方法 */
  dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    /* actions中取出type對應的ation */
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

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

    /* 是數組則包裝Promise造成一個新的Promise,只有一個則直接返回第0個 */
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

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

commit  -  mutation事件提交

commit方法和dispatch相比雖然都是觸發type,可是對應的處理卻相對複雜。先進行參數適配,判斷觸發mutation type,利用_withCommit方法執行本次批量觸發mutation處理函數,並傳入payload參數。執行完成後,通知全部_subscribers(訂閱函數)本次操做的mutation對象以及當前的 state 狀態,若是傳入了已經移除的 silent 選項則進行提示警告。能夠看實例裏module的actions的屬性。

/* 調用mutation的commit方法 */
  commit (_type, _payload, _options) {
    // check object-style commit 校驗參數
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    /* 取出type對應的mutation的方法 */
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    /* 執行mutation中的全部方法 */
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    /* 通知全部訂閱者 */
    this._subscribers.forEach(sub => sub(mutation, this.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'
      )
    }
  }
複製代碼

state 方法

在實例module裏state有常量和變量,能夠結合源碼來看狀態的改變原理。_withCommit是一個代理方法,全部觸發mutation的進行state修改的操做都通過它,由此來統一管理監控state狀態的修改。緩存執行時的 committing 狀態將當前狀態設置爲 true 後進行本次提交操做,待操做完畢後,將 committing 狀態還原爲以前的狀態。

//保存以前的提交狀態
  _withCommit (fn) {
    /* 調用withCommit修改state的值時會將store的committing值置爲true,
    內部會有斷言檢查該值,在嚴格模式下只容許使用mutation來修改store中的值,
    而不容許直接修改store的數值 */
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
}
複製代碼

Module - 模塊管理

installModule方法 - 遍歷註冊mutation、action、getter、安裝module

在實例的module裏用到了state、mutation、action,Store的構造類除了初始化一些內部變量之外,主要執行了installModule(初始化module)以及resetStoreVM(經過VM使store「響應式」)。 installModule的做用主要是用爲module加上namespace名字空間(若是有)後,註冊mutation、action以及getter,同時遞歸安裝全部子module。最後執行plugin的植入。

/*初始化module*/
function installModule (store, rootState, path, module, hot) {
  /* 是不是根module */
  const isRoot = !path.length
  /* 獲取module的namespace */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  /* 若是有namespace則在_modulesNamespaceMap中註冊 */
  if (module.namespaced) {
    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
  }

  // set state
  if (!isRoot && !hot) {
    /* 獲取父級的state */
    const parentState = getNestedState(rootState, path.slice(0, -1))
    /* module的name */
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      /* 將子module設置成響應式的 */
      Vue.set(parentState, moduleName, module.state)
    })
  }

  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)
  })

  /* 遞歸安裝mudule */
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
複製代碼

registerModule - 註冊模塊

registerModule用以註冊一個動態模塊,也就是在store建立之後再註冊模塊的時候用該接口。

/* 註冊一個動態module,當業務進行異步加載的時候,能夠經過該接口進行註冊動態module */
  registerModule (path, rawModule, options = {}) {
    /* 轉化成Array */
    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)
    /*初始化module*/
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // reset store to update getters...
    /* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */
    resetStoreVM(this, this.state)
  }
複製代碼

registerModule - 註銷模塊

一樣,與registerModule對應的方法unregisterModule,動態註銷模塊。實現方法是先從state中刪除模塊,而後用resetStore來重製store

/* 註銷一個動態module */
  unregisterModule (path) {
    /* 轉化成Array */
    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(() => {
      /* 獲取父級的state */
      const parentState = getNestedState(this.state, path.slice(0, -1))
      /* 從父級中刪除 */
      Vue.delete(parentState, path[path.length - 1])
    })
    /* 重製store */
    resetStore(this)
  }
複製代碼

getters - 獲取數據狀態

能夠看到實例將getters作成單獨的一個getters.js文件進行getter狀態管理。

resetStoreVM - 綁定getter

執行完各 module 的 install 後,執行 resetStoreVM 方法,進行 store 組件的初始化,resetStoreVM首先會遍歷wrappedGetters,使用Object.defineProperty方法爲每個getter綁定上get方法:

/* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */
function resetStoreVM (store, state, hot) {
  /* 存放以前的vm對象 */
  const oldVm = store._vm

  // bind store public getters(computed)
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  /* 經過Object.defineProperty爲每個getter方法設置get方法,好比獲取this.$store.getters.test的時候獲取的是store._vm.test,也就是Vue對象的computed屬性 */
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  const silent = Vue.config.silent
  /* Vue.config.silent暫時設置爲true的目的是在new一個Vue實例的過程當中不會報出一切警告 */
  Vue.config.silent = true
  /* 這裏new了一個Vue對象,運用Vue內部的響應式實現註冊state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // 恢復Vue的模式
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* 使用嚴格模式,保證修改store只能經過mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除舊vm的state的引用,以及銷燬舊的Vue對象 */
    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())
  }
}
複製代碼

註冊getter

//註冊getter
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
複製代碼

輔助函數 - mapState, mapMutations, mapGetters, mapActions

實例APP.vue裏用到了mapGetters,經過computed計算屬性獲取state狀態。當一個組件須要獲取多個狀態、多個getter、多個mutations方法、多個action分發事件時候,這些輔助函數幫助咱們生成計算屬性,這四個輔助函數具體實現方法在helpers.js裏:

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) {
        const 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
})
......
複製代碼

插件

Vuex源碼裏提供了兩個插件源碼:devtool.js和logger.js。若是已經安裝了Vue.js devtools,則會在windows對象上暴露一個VUE_DEVTOOLS_GLOBAL_HOOK。devtoolHook用在初始化的時候會觸發「vuex:init」事件通知插件,而後經過on方法監聽「vuex:travel-to-state」事件來重置state。最後經過Store的subscribe方法來添加一個訂閱者,在觸發commit方法修改mutation數據之後,該訂閱者會被通知,從而觸發「vuex:mutation」事件:

/* 從window對象的__VUE_DEVTOOLS_GLOBAL_HOOK__中獲取devtool插件 */
const target = typeof window !== 'undefined'
  ? window
  : typeof global !== 'undefined'
    ? global
    : {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__

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

  /* devtool插件實例存儲在store的_devtoolHook上 */
  store._devtoolHook = devtoolHook

  // 觸發Vuex組件初始化的hook,並將store的引用地址傳給devtool插件,使插件獲取store的實例 
  devtoolHook.emit('vuex:init', store)

  // 提供「時空穿梭」功能,即state操做的前進和倒退
  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  // 訂閱store的變化, mutation被執行時,觸發hook,並提供被觸發的mutation函數和當前的state狀態
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}
複製代碼

總結

Vuex是一個很是優秀的庫,代碼簡潔,邏輯清晰。源碼中還有一些工具函數相似hotUpdate、watch 以及 subscribe等,感興趣的小夥伴們有時間能夠好好研究一下源碼,瞭解了源碼設計思想和實現邏輯後,會豁然開朗,應用起來也會遊刃有餘。

相關文章
相關標籤/搜索