由於對Vue.js很感興趣,並且平時工做的技術棧也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js源碼,並作了總結與輸出。javascript
文章的原地址:https://github.com/answershuto/learnVue。html
在學習過程當中,爲Vue加上了中文的註釋https://github.com/answershuto/learnVue/tree/master/vue-src以及Vuex的註釋https://github.com/answershuto/learnVue/tree/master/vuex-src,但願能夠對其餘想學習源碼的小夥伴有所幫助。vue
可能會有理解存在誤差的地方,歡迎提issue指出,共同窗習,共同進步。java
咱們在使用Vue.js開發複雜的應用時,常常會遇到多個組件共享同一個狀態,亦或是多個組件會去更新同一個狀態,在應用代碼量較少的時候,咱們能夠組件間通訊去維護修改數據,或者是經過事件總線來進行數據的傳遞以及修改。可是當應用逐漸龐大之後,代碼就會變得難以維護,從父組件開始經過prop傳遞多層嵌套的數據因爲層級過深而顯得異常脆弱,而事件總線也會由於組件的增多、代碼量的增大而顯得交互錯綜複雜,難以捋清其中的傳遞關係。react
那麼爲何咱們不能將數據層與組件層抽離開來呢?把數據層放到全局造成一個單一的Store,組件層變得更薄,專門用來進行數據的展現及操做。全部數據的變動都須要通過全局的Store來進行,造成一個單向數據流,使數據變化變得「可預測」。git
Vuex是一個專門爲Vue.js框架設計的、用於對Vue.js應用程序進行狀態管理的庫,它借鑑了Flux、redux的基本思想,將共享的數據抽離到全局,以一個單例存放,同時利用Vue.js的響應式機制來進行高效的狀態管理與更新。正是由於Vuex使用了Vue.js內部的「響應式機制」,因此Vuex是一個專門爲Vue.js設計並與之高度契合的框架(優勢是更加簡潔高效,缺點是隻能跟Vue.js搭配使用)。具體使用方法及API能夠參考Vuex的官網。github
先來看一下這張Vuex的數據流程圖,熟悉Vuex使用的同窗應該已經有所瞭解。web
Vuex實現了一個單向數據流,在全局擁有一個State存放數據,全部修改State的操做必須經過Mutation進行,Mutation的同時提供了訂閱者模式供外部插件調用獲取State數據的更新。全部異步接口須要走Action,常見於調用後端接口異步獲取更新數據,而Action也是沒法直接修改State的,仍是須要經過Mutation來修改State的數據。最後,根據State的變化,渲染到視圖上。Vuex運行依賴Vue內部數據雙向綁定機制,須要new一個Vue對象來實現「響應式化」,因此Vuex是一個專門爲Vue.js設計的狀態管理庫。vuex
使用過Vuex的朋友必定知道,Vuex的安裝十分簡單,只須要提供一個store,而後執行下面兩句代碼即完成的Vuex的引入。chrome
Vue.use(Vuex); /*將store放入Vue建立時的option中*/ new Vue({ el: '#app', store });
那麼問題來了,Vuex是怎樣把store注入到Vue實例中去的呢?
Vue.js提供了Vue.use方法用來給Vue.js安裝插件,內部經過調用插件的install方法(當插件是一個對象的時候)來進行插件的安裝。
咱們來看一下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 /*將vuexInit混淆進Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/ applyMixin(Vue) }
這段install代碼作了兩件事情,一件是防止Vuex被重複安裝,另外一件是執行applyMixin,目的是執行vuexInit方法初始化Vuex。Vuex針對Vue1.0與2.0分別進行了不一樣的處理,若是是Vue1.0,Vuex會將vuexInit方法放入Vue的_init方法中,而對於Vue2.0,則會將vuexinit混淆進Vue的beforeCreacte鉤子中。來看一下vuexInit的代碼。
/*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) { /*子組件直接從父組件中獲取$store,這樣就保證了全部組件都公用了全局的同一份store*/ this.$store = options.parent.$store } }
vuexInit會嘗試從options中獲取store,若是當前組件是根組件(Root節點),則options中會存在store,直接獲取賦值給$store便可。若是當前組件非根組件,則經過options中的parent獲取父組件的$store引用。這樣一來,全部的組件都獲取到了同一分內存地址的Store實例,因而咱們能夠在每個組件中經過this.$store愉快地訪問全局的Store實例了。
那麼,什麼是Store實例?
咱們傳入到根組件到store,就是Store實例,用Vuex提供到Store方法構造。
export default new Vuex.Store({ strict: true, modules: { moduleA, moduleB } });
咱們來看一下Store的實現。首先是構造函數。
constructor (options = {}) { // Auto install if it is not done yet and `window` has `Vue`. // To allow users to avoid auto-installation in some cases, // this code should be placed here. See #731 /* 在瀏覽器環境下,若是插件還未安裝(!Vue即判斷是否未安裝),則它會自動安裝。 它容許用戶在某些狀況下避免自動安裝。 */ if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue) } if (process.env.NODE_ENV !== 'production') { 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 { /*一個數組,包含應用在 store 上的插件方法。這些插件直接接收 store 做爲惟一參數,能夠監聽 mutation(用於外部地數據持久化、記錄或調試)或者提交 mutation (用於內部數據,例如 websocket 或 某些觀察者)*/ plugins = [], /*使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。*/ strict = false } = options /*從option中取出state,若是state是function則執行,最終獲得一個對象*/ let { state = {} } = options if (typeof state === 'function') { state = state() } // store internal state /* 用來判斷嚴格模式下是不是用mutation修改state的 */ this._committing = false /* 存放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實例自己) */ 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 // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters /*初始化根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)) /* devtool插件 */ if (Vue.config.devtools) { devtoolPlugin(this) } }
Store的構造類除了初始化一些內部變量之外,主要執行了installModule(初始化module)以及resetStoreVM(經過VM使store「響應式」)。
installModule的做用主要是用爲module加上namespace名字空間(若是有)後,註冊mutation、action以及getter,同時遞歸安裝全部子module。
/*初始化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) { 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 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) }) }
在說resetStoreVM以前,先來看一個小demo。
let globalData = { d: 'hello world' }; new Vue({ data () { return { $$state: { globalData } } } }); /* modify */ setTimeout(() => { globalData.d = 'hi~'; }, 1000); Vue.prototype.globalData = globalData; /* 任意模板中 */ <div>{{globalData.d}}</div>
上述代碼在全局有一個globalData,它被傳入一個Vue對象的data中,以後在任意Vue模板中對該變量進行展現,由於此時globalData已經在Vue的prototype上了因此直接經過this.prototype訪問,也就是在模板中的{{prototype.d}}。此時,setTimeout在1s以後將globalData.d進行修改,咱們發現模板中的globalData.d發生了變化。其實上述部分就是Vuex依賴Vue核心實現數據的「響應式化」。
不熟悉Vue.js響應式原理的同窗能夠經過筆者另外一篇文章響應式原理瞭解Vue.js是如何進行數據雙向綁定的。
接着來看代碼。
/* 經過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()) } }
resetStoreVM首先會遍歷wrappedGetters,使用Object.defineProperty方法爲每個getter綁定上get方法,這樣咱們就能夠在組件裏訪問this.$store.getter.test就等同於訪問store._vm.test。
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 }) })
以後Vuex採用了new一個Vue對象來實現數據的「響應式化」,運用Vue.js內部提供的數據雙向綁定功能來實現store的數據與視圖的同步更新。
store._vm = new Vue({ data: { $$state: state }, computed })
這時候咱們訪問store._vm.test也就訪問了Vue實例中的屬性。
這兩步執行完之後,咱們就能夠經過this.$store.getter.test訪問vm中的test屬性了。
Vuex的Store構造類的option有一個strict的參數,能夠控制Vuex執行嚴格模式,嚴格模式下,全部修改state的操做必須經過mutation實現,不然會拋出錯誤。
/* 使能嚴格模式 */ function enableStrictMode (store) { store._vm.$watch(function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { /* 檢測store中的_committing的值,若是是true表明不是經過mutation的方法修改的 */ assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`) } }, { deep: true, sync: true }) }
首先,在嚴格模式下,Vuex會利用vm的$watch方法來觀察$$state,也就是Store的state,在它被修改的時候進入回調。咱們發現,回調中只有一句話,用assert斷言來檢測store._committing,當store._committing爲false的時候會觸發斷言,拋出異常。
咱們發現,Store的commit方法中,執行mutation的語句是這樣的。
this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) })
再來看看_withCommit的實現。
_withCommit (fn) { /* 調用withCommit修改state的值時會將store的committing值置爲true,內部會有斷言檢查該值,在嚴格模式下只容許使用mutation來修改store中的值,而不容許直接修改store的數值 */ const committing = this._committing this._committing = true fn() this._committing = committing }
咱們發現,經過commit(mutation)修改state數據的時候,會再調用mutation方法以前將committing置爲true,接下來再經過mutation函數修改state中的數據,這時候觸發$watch中的回調斷言committing是不會拋出異常的(此時committing爲true)。而當咱們直接修改state的數據時,觸發$watch的回調執行斷言,這時committing爲false,則會拋出異常。這就是Vuex的嚴格模式的實現。
接下來咱們來看看Store提供的一些API。
/* 調用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' ) } }
commit方法會根據type找到並調用_mutations中的全部type對應的mutation方法,因此當沒有namespace的時候,commit方法會觸發全部module中的mutation方法。再執行完全部的mutation以後會執行_subscribers中的全部訂閱者。咱們來看一下_subscribers是什麼。
Store給外部提供了一個subscribe方法,用以註冊一個訂閱函數,會push到Store實例的_subscribers中,同時返回一個從_subscribers中註銷該訂閱者的方法。
/* 註冊一個訂閱函數,返回取消訂閱的函數 */ subscribe (fn) { const subs = this._subscribers if (subs.indexOf(fn) < 0) { subs.push(fn) } return () => { const i = subs.indexOf(fn) if (i > -1) { subs.splice(i, 1) } } }
在commit結束之後則會調用這些_subscribers中的訂閱者,這個訂閱者模式提供給外部一個監視state變化的可能。state經過mutation改變時,能夠有效補獲這些變化。
來看一下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) }
以及registerAction時候作的事情。
/* 遍歷註冊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 } }) }
由於registerAction的時候將push進_actions的action進行了一層封裝(wrappedActionHandler),因此咱們在進行dispatch的第一個參數中獲取state、commit等方法。以後,執行結果res會被進行判斷是不是Promise,不是則會進行一層封裝,將其轉化成Promise對象。dispatch時則從_actions中取出,只有一個的時候直接返回,不然用Promise.all處理再返回。
/* 觀察一個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) }
熟悉Vue的朋友應該很熟悉watch這個方法。這裏採用了比較巧妙的設計,_watcherVM是一個Vue的實例,因此watch就能夠直接採用了Vue內部的watch特性提供了一種觀察數據getter變更的方法。
/* 註冊一個動態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) }
registerModule用以註冊一個動態模塊,也就是在store建立之後再註冊模塊的時候用該接口。內部實現實際上也只有installModule與resetStoreVM兩個步驟,前面已經講過,這裏再也不累述。
/* 註銷一個動態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對應的方法unregisterModule,動態註銷模塊。實現方法是先從state中刪除模塊,而後用resetStore來重製store。
/* 重製store */ 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) }
這裏的resetStore其實也就是將store中的_actions等進行初始化之後,從新執行installModule與resetStoreVM來初始化module以及用Vue特性使其「響應式化」,這跟構造函數中的是一致的。
Vue提供了一個很是好用的插件Vue.js devtools
/* 從window對象的__VUE_DEVTOOLS_GLOBAL_HOOK__中獲取devtool插件 */ const devtoolHook = typeof window !== 'undefined' && window.__VUE_DEVTOOLS_GLOBAL_HOOK__ export default function devtoolPlugin (store) { if (!devtoolHook) return /* devtoll插件實例存儲在store的_devtoolHook上 */ store._devtoolHook = devtoolHook /* 出發vuex的初始化事件,並將store的引用地址傳給deltool插件,使插件獲取store的實例 */ devtoolHook.emit('vuex:init', store) /* 監聽travel-to-state事件 */ devtoolHook.on('vuex:travel-to-state', targetState => { /* 重製state */ store.replaceState(targetState) }) /* 訂閱store的變化 */ store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) }) }
若是已經安裝了該插件,則會在windows對象上暴露一個__VUE_DEVTOOLS_GLOBAL_HOOK__。devtoolHook用在初始化的時候會觸發「vuex:init」事件通知插件,而後經過on方法監聽「vuex:travel-to-state」事件來重置state。最後經過Store的subscribe方法來添加一個訂閱者,在觸發commit方法修改mutation數據之後,該訂閱者會被通知,從而觸發「vuex:mutation」事件。
Vuex是一個很是優秀的庫,代碼量很少且結構清晰,很是適合研究學習其內部實現。最近的一系列源碼閱讀也使我本身受益不淺,寫這篇文章也但願能夠幫助到更多想要學習探索Vuex內部實現原理的同窗。
做者:染陌
Email:answershuto@gmail.com or answershuto@126.com
Github: https://github.com/answershuto
Blog:http://answershuto.github.io/
知乎主頁:https://www.zhihu.com/people/cao-yang-49/activities
知乎專欄:https://zhuanlan.zhihu.com/ranmo
掘金: https://juejin.im/user/58f87ae844d9040069ca7507
osChina:https://my.oschina.net/u/3161824/blog
轉載請註明出處,謝謝。
歡迎關注個人公衆號