Vuex 源码解析
?
先來看一下這張Vuex的數據流程圖,熟悉Vuex使用的同學應該已經有所了解。
Vuex實現了一個單向數據流,在全局擁有一個State存放數據,所有修改State的操作必須通過Mutation進行,Mutation的同時提供了訂閱者模式供外部插件調用獲取State數據的更新。所有異步接口需要走Action,常見于調用后端接口異步獲取更新數據,而Action也是無法直接修改State的,還是需要通過Mutation來修改State的數據。最后,根據State的變化,渲染到視圖上。Vuex運行依賴Vue內部數據雙向綁定機制,需要new一個Vue對象來實現“響應式化”,所以Vuex是一個專門為Vue.js設計的狀態管理庫。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 injectionif (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,就是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 = {}} = optionsif (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 = thisconst { 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
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 stateif (!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 + keyregisterMutation(store, namespacedType, mutation, local)})/* 遍歷注冊action */module.forEachAction((action, key) => {const namespacedType = namespace + keyregisterAction(store, namespacedType, action, local)})/* 遍歷注冊getter */module.forEachGetter((getter, key) => {const namespacedType = namespace + keyregisterGetter(store, namespacedType, getter, local)})/* 遞歸安裝mudule */module.forEachChild((child, key) => {installModule(store, rootState, path.concat(key), child, hot)}) }
resetStoreVM
在說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 gettersstore.getters = {}const wrappedGetters = store._wrappedGettersconst 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 mechanismcomputed[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 mixinsconst 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 mechanismcomputed[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._committingthis._committing = truefn()this._committing = committing }
我們發現,通過commit(mutation)修改state數據的時候,會再調用mutation方法之前將committing置為true,接下來再通過mutation函數修改state中的數據,這時候觸發$watch中的回調斷言committing是不會拋出異常的(此時committing為true)。而當我們直接修改state的數據時,觸發$watch的回調執行斷言,這時committing為false,則會拋出異常。這就是Vuex的嚴格模式的實現。
接下來我們來看看Store提供的一些API。
commit(mutation)
/* 調用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._subscribersif (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的實現。
/* 調用action的dispatch方法 */ dispatch (_type, _payload) {// check object-style dispatchconst {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處理再返回。
watch
/* 觀察一個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變動的方法。
registerModule
/* 注冊一個動態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兩個步驟,前面已經講過,這里不再累述。
unregisterModule
/* 注銷一個動態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。
resetStore
/* 重制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 modulesinstallModule(store, state, [], store._modules.root, true)// reset vmresetStoreVM(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”事件。
https://juejin.im/post/59f66bd7f265da432d275d30
?
轉載于:https://www.cnblogs.com/smzd/p/8624604.html
總結
- 上一篇: C#代码整洁之道:代码重构与性能提升
- 下一篇: vue render函数