快速跳轉:javascript
首先,vuex是什麼?vue
根據官方的解釋,java
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
vue的整個狀態管理主要包括如下幾部分:react
看一下官網提供的單向數據流示意:
git
可是,若是咱們要在兄弟組件之間傳值,或者要多個組件共享一個狀態,這個單向數據流就會遭到破壞。在不使用vuex的時候,須要在父組件中定義好state,再用props傳遞給子組件,若是子組件要修改父組件的狀態,就須要使用$emit
事件。稍微進階一點,可使用busEvent, 就是new一個Vue實例出來,經過在target component上$on
註冊上一個事件,在source component上$emit觸發這個事件,引發target component的狀態改變。可是,一旦應用變得大型,這兩種方式就變得很是脆弱。因此,vuex就誕生啦~github
仍是看一下官網的vuex示意圖:
vuex
Vuex實現了一個單項數據流,在全局擁有一個state存放數據,全部修改state的操做必須經過mutation來進行執行,同時mutation只能同步修改state,若是要異步修改就須要調用action, 而action內部也是要使用mutation進行修改state的操做。最後,根據state的變化,渲染到視圖上。api
Vuex運行依賴Vue內部雙向綁定機制,須要new Vue的實例來實現,所以,vuex只能和vue搭配使用。數組
更多vuex的詳細介紹請參閱Vuex官方教程。promise
首先,看一下store是如何install到vue上的,如下是vuex的install代碼:
/*暴露給外部的插件install方法,供Vue.use調用安裝插件*/ export function install (_Vue) { if (Vue) { /*避免重複安裝(Vue.use內部也會檢測一次是否重複安裝同一個插件)*/ if (process.env.NODE_ENV !== 'production') { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } /*保存Vue,同時用於檢測是否重複安裝*/ Vue = _Vue applyMixin(Vue) }
這段代碼主要作了兩件事:
接下來看一下applyMixin
的代碼:
export default function (Vue) { /*獲取Vue版本,鑑別Vue1.0仍是Vue2.0*/ const version = Number(Vue.version.split('.')[0]) if (version >= 2) { /*經過mixin將vuexInit混淆到Vue實例的beforeCreate鉤子中*/ Vue.mixin({ beforeCreate: vuexInit }) } else { // override init and inject vuex init procedure // for 1.x backwards compatibility. /*將vuexInit放入_init中調用*/ 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. */ /*Vuex的init鉤子,會存入每個Vue實例等鉤子列表*/ function vuexInit () { const options = this.$options // store injection if (options.store) { /*存在store其實表明的就是Root節點,直接執行store(function時)或者使用store(非function)*/ this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 從父組件裏獲取this.$store this.$store = options.parent.$store } } }
上面的代碼解釋一下,就是先去檢查vue的版本,若是是2.0就在beforeCreate的時候調用VueInit這個方法,若是是1.0就放入Vue的_init中;
而後再看一下vuexInit這個方法,若是options存在store的話就證實是Root節點,直接執行store(由於咱們注入store的時候,都會在new Vue的時候放進去,好比:
new Vue({ el: '#app', store })
因此若是options裏有store,就直接執行便可;若是沒有,就從父組件中獲取$store,這樣就保證了全部組件都公用了全局的同一份store。
經過以上步驟,就完成了this.$store的注入,在工程的任何地方均可以應用,並且指向的都是同一個store。
回想一下,咱們在定義Store的時候,一般是這樣寫的 (來自vuex官方教程舉例):
const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { moduleA, moduleB } })
所以,初始化store實例的時候就註冊了modules裏的state/mutations/getters/actions。來看一下Store的代碼(由於store構造類的源碼不少,會一部分一部分講):
/*Store構造類*/ export class Store { constructor (options = {}) { // 首先會檢查一下瀏覽器環境內有沒有vue if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue) } const { plugins = [], // 默認嚴格模式開啓 strict = false } = options /*從option中取出state,若是state是function則執行,最終獲得一個對象*/ let { state = {} } = options if (typeof state === 'function') { state = state() } // store internal state /* 用來判斷嚴格模式下是不是用mutation修改state的 */ this._committing = false // 如下是初始化actions/mutations/wrapperGetters/modules等 /* 存放action */ this._actions = Object.create(null) /* 存放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實例自己) */ // dispatch對應actions, commit對應mutations (dispatch裏會有promise實現) 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 installModule(this, state, [], this._modules.root) // initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) resetStoreVM(this, state) // apply plugins plugins.forEach(plugin => plugin(this)) /* devtool插件 */ if (Vue.config.devtools) { devtoolPlugin(this) } }
以上代碼是Vue構造類的第一部分,能夠看到除了初始化actions/mutations/modules...等等,還調用了installModule和resetStoreVM這兩個方法。
看一下這兩個方法分別是什麼:
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length // path是[]表示是根節點 /* 獲取module的namespace */ const namespace = store._modules.getNamespace(path) // register in namespace map /* 若是有namespace則在_modulesNamespaceMap中註冊 */ if (module.namespaced) { 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] // 這邊也用_withCommit是由於state只能在_committing === true的時候進行修改 store._withCommit(() => { // Vue.set內部會用defineReactive將module.state設置成響應式的 // defineReactive的代碼詳見第一節 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 namespacedType = namespace + key registerAction(store, namespacedType, action, 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) }) }
installModule的做用主要是爲module加上namespace名字空間(若是有)後,註冊mutation、action以及getter,同時遞歸安裝全部子module。
/* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */ function resetStoreVM (store, state, hot) { /* 存放以前的vm對象 */ const oldVm = store._vm // bind store public getters 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) => { // use computed to leverage its lazy-caching mechanism computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // use a Vue instance to store the state tree // suppress warnings just in case the user has added // some funky global mixins 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.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()) } }
在這段代碼中, 實際上是利用了vue.js能夠對數據進行雙向綁定的特色(具體參見Vue的雙向綁定原理),new了一個vue的實例,而且綁定了state和computed, 這樣就能夠實現data和視圖的同步更新。
繼續看下Vuex.store裏還有什麼:
get state () { return this._vm._data.$$state } set state (v) { // state不容許經過set進行修改,必須經過commit if (process.env.NODE_ENV !== 'production') { assert(false, `Use store.replaceState() to explicit replace store state.`) } }
get沒什麼可說的,就是返回這個state裏的值,可是set這邊除了一個斷言就沒有了,意思是state裏的數據並不能直接修改,必須用mutation。
再看一下store裏提供的比較重要的兩個api —— commit 和dispatch。
commit (_type, _payload, _options) { // unifyObjectStyle主要是統一參數的格式 // 由於mutation 支持兩種寫法 (一種是列舉參數,一種是把參數放在{}裏,具體參見教程) 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' ) } } _withCommit (fn) { // _committing是一個標誌位,在strict模式下保證只能經過mutation來修改store的數據 const committing = this._committing this._committing = true fn() this._committing = committing }
mutation用的這個commit方法,要注意的就是這個_withCommit,在_withCommit裏_committing是true, 所以,_committing是一個標誌位,若是在strice mode(default true)下,必定要用commit方法才能修改。看一下這個斷言:
function enableStrictMode (store) { store._vm.$watch(function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { // function assert (condition, msg) { // if (!condition) throw new Error(`[vuex] ${msg}`) // } assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`) } }, { deep: true, sync: true }) }
調用的vue的watch方法,在有變更的時候作一個檢驗,若是_committing不是true, 就扔出error。
繼續看給actions調用的dispatch方法:
/* 調用action的dispatch方法 */ dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_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 } /* 是數組則包裝Promise造成一個新的Promise,只有一個則直接返回第0個 */ return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) }
dispatch
這個方法比較簡單,使用了Promise.all, 因此能夠執行異步操做。
可是咱們在實際操做mutation和action的時候,是能夠獲取到state和調用commit的,這是怎麼作到的呢?看一下registerMutation
和registerAction
兩個方法。
/* 遍歷註冊mutation */ function registerMutation (store, type, handler, local) { /* 全部的mutation會被push進一個數組中,這樣相同的mutation就能夠調用不一樣module中的同名的mutation了 */ const entry = store._mutations[type] || (store._mutations[type] = []) entry.push(function wrappedMutationHandler (payload) { handler.call(store, local.state, payload) }) } /* 遍歷註冊action */ function registerAction (store, type, handler, local) { /* 取出type對應的action */ const entry = store._actions[type] || (store._actions[type] = []) entry.push(function wrappedActionHandler (payload, cb) { 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) /* 判斷是不是Promise */ if (!isPromise(res)) { /* 不是Promise對象的時候轉化稱Promise對象 */ res = Promise.resolve(res) } if (store._devtoolHook) { /* 存在devtool捕獲的時候觸發vuex的error給devtool */ return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) }
能夠看到,在registerMutation
裏,push進mutationHandler的時候,會塞進local.state, 而在registerAction
裏,push進actionHandler的時候,第一個參數會包裝成dispatch/commit/getters/state...的對象,這樣在action裏就能夠去調用這些屬性和方法。
因而,咱們就能夠這樣使用mutation和action:
// vuex裏定義mutation: increment (state, payload) { state.count += payload.amount } // vuex裏定義action: (使用解構) actionB ({ dispatch, commit }) { return dispatch('actionA').then(() => { commit('someOtherMutation') }) }
最後一口氣把store裏的代碼都看完吧:
/* 觀察一個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) } /* 重置state */ replaceState (state) { this._withCommit(() => { this._vm._data.$$state = state }) }
/* 註冊一個動態module,當業務進行異步加載的時候,能夠經過該接口進行註冊動態module */ registerModule (path, rawModule) { /* 轉化稱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)) // reset store to update getters... /* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */ resetStoreVM(this, this.state) } /* 註銷一個動態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) }
先看一下registerModule
這個api的調用方式:
// 註冊myModule store.registerModule('myModule', { // ... }) // 註冊嵌套模塊 `nested/myModule` store.registerModule(['nested', 'myModule'], { // ... })
因此,在調用registerModule
的時候,由於能夠傳string和array,因此在一開始要統一成array;以後再調用installModule
和resetStoreVM
這兩個Vuex.store裏最重要的方法,初始化module並重設store。
而unregisterModule
的做用就是註銷掉一個動態模塊。調用的方式就是unregisterModule(path: string | Array<string>)
,在內部首先和registerModule
同樣,把string統一成array, 若是不是這兩種數據結構的參數,就拋錯出來~以後在moudles裏unregister這個path, 經過commit的方式把該module裏的state註銷掉,同時重置store。
store的代碼基本就講完啦,能夠看到,vuex的代碼其實不少都是高度依賴vue自身支持雙向綁定的特性,好比store構造函數裏的resetStoreVM()
, 就是new了一個Vue的實例,運用vue內部的響應式註冊了state和computed;再好比,store提供的api-watch
也是使用的vm.$watch。所以,vuex只能和vue搭配使用,不能作其餘框架的狀態管理。