做爲Vue全家桶之一的vuex,在Vue框架中起着很重要的角色。Vuex源碼篇幅雖然很少,但解讀起來仍是須要下一些功夫的。下面咱們就參照vuex官方指南文檔內容進行分析。 vue
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。 react
Vuex 能夠幫助咱們管理共享狀態,並附帶了更多的概念和框架。這須要對短時間和長期效益進行權衡。
若是隻是簡單的單頁應用,最好不要使用 Vuex,使用 Vuex 多是繁瑣冗餘的,一個簡單的 store 模式就足夠了。可是,若是構建一箇中大型單頁應用,須要考慮如何更好地在組件外部管理狀態,Vuex 將會成爲最佳選擇。git
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在項目中應用的目錄,在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.vuejs.org/zh/guide/
vuex源碼:github.com/vuejs/vuex
git上下載的源碼文件不少,可是核心執行源碼存在/src裏,源碼文件很少,目錄劃分也很清晰,每一個文件都有着各自的功能:
├── 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 } 複製代碼
在實例的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,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方法和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' ) } } 複製代碼
在實例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裏用到了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用以註冊一個動態模塊,也就是在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對應的方法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.js文件進行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 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 ) } } 複製代碼
實例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等,感興趣的小夥伴們有時間能夠好好研究一下源碼,瞭解了源碼設計思想和實現邏輯後,會豁然開朗,應用起來也會遊刃有餘。