vuex原始碼解析
能看到此文章的人,應該大部分都已經使用過vuex了,想更深一步瞭解vuex的內部實現原理。所以簡介就少介紹一點。官網介紹說Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。資料流的狀態非常清晰,按照 元件dispatch Action -> action內部commit Mutation -> Mutation再 mutate state 的資料,在觸發render函式引起檢視的更新。附上一張官網的流程圖及vuex的官網地址:vuex.vuejs.org/zh/

Questions
在使用vuex的時候,大家有沒有如下幾個疑問,帶著這幾個疑問,再去看原始碼,從中找到解答,這樣對vuex的理解可以加深一些。
- 官網在嚴格模式下有說明:在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函式引起的,將會丟擲錯誤。vuex是如何檢測狀態改變是由mutation函式引起的?
- 通過在根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中。為什麼所有子元件都可以取到store?
- 為什麼用到的屬性在state中也必須要提前定義好,vue檢視才可以響應?
- 在呼叫dispatch和commit時,只需傳入(type, payload),為什麼action函式和mutation函式能夠在第一個引數中解構出來state、commit等? 帶著這些問題,我們來看看vuex的原始碼,從中尋找到答案。
原始碼目錄結構
vuex的原始碼結構非常簡潔清晰,程式碼量也不是很大,大家不要感到恐慌。

vuex掛載
vue使用外掛的方法很簡單,只需Vue.use(Plugins),對於vuex,只需要Vue.use(Vuex)即可。在use 的內部是如何實現外掛的註冊呢?讀過vue原始碼的都知道,如果傳入的引數有 install 方法,則呼叫外掛的 install 方法,如果傳入的引數本身是一個function,則直接執行。那麼我們接下來就需要去 vuex 暴露出來的 install 方法去看看具體幹了什麼。
store.js
export function install(_Vue) { // vue.use原理:呼叫外掛的install方法進行外掛註冊,並向install方法傳遞Vue物件作為第一個引數 if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== "production") { console.error( "[vuex] already installed. Vue.use(Vuex) should be called only once." ); } return; } Vue = _Vue; // 為了引用vue的watch方法 applyMixin(Vue); } 複製程式碼
在 install 中,將 vue 物件賦給了全域性變數 Vue,並作為引數傳給了 applyMixin 方法。那麼在 applyMixin 方法中幹了什麼呢?
mixin.js
function vuexInit() { const options = this.$options; // store injection if (options.store) { this.$store = typeof options.store === "function" ? options.store() : options.store; } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store; } } 複製程式碼
在這裡首先檢查了一下 vue 的版本,2以上的版本把 vuexInit 函式混入 vuex 的 beforeCreate 鉤子函式中。 在 vuexInit 中,將 new Vue()
時傳入的 store 設定到 this 物件的 $store
屬性上,子元件則從其父元件上引用其 $store
屬性進行層層巢狀設定,保證每一個元件中都可以通過 this.$store 取到 store 物件。 這也就解答了我們問題 2 中的問題。通過在根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中,注入方法是子從父拿,root從options拿。
接下來讓我們看看 new Vuex.Store()
都幹了什麼。
store建構函式
store物件構建的主要程式碼都在store.js中,是vuex的核心程式碼。
首先,在 constructor 中進行了 Vue 的判斷,如果沒有通過 Vue.use(Vuex) 進行 Vuex 的註冊,則呼叫 install 函式註冊。( 通過 script 標籤引入時不需要手動呼叫 Vue.use(Vuex) ) 並在非生產環境進行判斷: 必須呼叫 Vue.use(Vuex) 進行註冊,必須支援 Promise,必須用 new 建立 store。
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.` ); } 複製程式碼
然後進行一系列的屬性初始化。其中的重點是 new ModuleCollection(options)
,這個我們放在後面再講。先把 constructor 中的程式碼過完。
const { plugins = [], strict = false } = options; // store internal state this._committing = false; // 是否在進行提交mutation狀態標識 this._actions = Object.create(null); // 儲存action,_actions裡的函式已經是經過包裝後的 this._actionSubscribers = []; // action訂閱函式集合 this._mutations = Object.create(null); // 儲存mutations,_mutations裡的函式已經是經過包裝後的 this._wrappedGetters = Object.create(null); // 封裝後的getters集合物件 // Vuex支援store分模組傳入,在內部用Module建構函式將傳入的options構造成一個Module物件, // 如果沒有命名模組,預設繫結在this._modules.root上 // ModuleCollection 內部呼叫 new Module建構函式 this._modules = new ModuleCollection(options); this._modulesNamespaceMap = Object.create(null); // 模組名稱空間map this._subscribers = []; // mutation訂閱函式集合 this._watcherVM = new Vue(); // Vue元件用於watch監視變化 複製程式碼
屬性初始化完畢後,首先從 this 中解構出原型上的 dispatch
和 commit
方法,並進行二次包裝,將 this 指向當前 store。
const store = this; const { dispatch, commit } = this; /** 把 Store 類的 dispatch 和 commit 的方法的 this 指標指向當前 store 的例項上. 這樣做的目的可以保證當我們在元件中通過 this.$store 直接呼叫 dispatch/commit 方法時, 能夠使 dispatch/commit 方法中的 this 指向當前的 store 物件而不是當前元件的 this. */ 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); }; 複製程式碼
接著往下走,包括嚴格模式的設定、根state的賦值、模組的註冊、state的響應式、外掛的註冊等等,其中的重點在 installModule
函式中,在這裡實現了所有modules的註冊。
//options中傳入的是否啟用嚴格模式 this.strict = strict; // new ModuleCollection 構造出來的_mudules const state = this._modules.root.state; // 初始化元件樹根元件、註冊所有子元件,並將其中所有的getters儲存到this._wrappedGetters屬性中 installModule(this, state, [], this._modules.root); //通過使用vue例項,初始化 store._vm,使state變成可響應的,並且將getters變成計算屬性 resetStoreVM(this, state); // 註冊外掛 plugins.forEach(plugin => plugin(this)); // 除錯工具註冊 const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools; if (useDevtools) { devtoolPlugin(this); } 複製程式碼
到此為止,constructor 中所有的程式碼已經分析完畢。其中的重點在 new ModuleCollection(options)
和 installModule
,那麼接下來我們到它們的內部去看看,究竟都幹了些什麼。
ModuleCollection
由於 Vuex 使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件。當應用變得非常複雜時,store 物件就有可能變得相當臃腫。Vuex 允許我們將 store 分割成模組(module),每個模組擁有自己的 state、mutation、action、getter、甚至是巢狀子模組。例如下面這樣:
const childModule = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ state, getters, actions, mutations, modules: { childModule: childModule, } }) 複製程式碼
有了模組的概念,可以更好的規劃我們的程式碼。對於各個模組公用的資料,我們可以定義一個common store,別的模組用到的話直接通過 modules 的方法引入即可,無需重複的在每一個模組都寫一遍相同的程式碼。這樣我們就可以通過 store.state.childModule 拿到childModule中的 state 狀態, 對於Module的內部是如何實現的呢?
export default class ModuleCollection { constructor(rawRootModule) { // 註冊根module,引數是new Vuex.Store時傳入的options this.register([], rawRootModule, false); } register(path, rawModule, runtime = true) { if (process.env.NODE_ENV !== "production") { assertRawModule(path, rawModule); } const newModule = new Module(rawModule, runtime); if (path.length === 0) { // 註冊根module this.root = newModule; } else { // 註冊子module,將子module新增到父module的_children屬性上 const parent = this.get(path.slice(0, -1)); parent.addChild(path[path.length - 1], newModule); } // 如果當前模組有子modules,迴圈註冊 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime); }); } } } 複製程式碼
在ModuleCollection中又呼叫了Module建構函式,構造一個Module。
Module建構函式
constructor (rawModule, runtime) { // 初始化時為false this.runtime = runtime // 儲存子模組 this._children = Object.create(null) // 將原來的module儲存,以備後續使用 this._rawModule = rawModule const rawState = rawModule.state // 儲存原來module的state this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } 複製程式碼
通過以上程式碼可以看出,ModuleCollection 主要將傳入的 options 物件整個構造為一個 Module 物件,並迴圈呼叫 this.register([key], rawModule, false) 為其中的 modules 屬性進行模組註冊,使其都成為 Module 物件,最後 options 物件被構造成一個完整的 Module 樹。
經過 ModuleCollection 構造後的樹結構如下:(以上面的例子生成的樹結構)

模組已經建立好之後,接下來要做的就是 installModule。
installModule
首先我們來看一看執行完 constructor 中的 installModule 函式後,這棵樹的結構如何?

從上圖中可以看出,在執行完installModule函式後,每一個 module 中的 state 屬性都增加了 其子 module 中的 state 屬性,但此時的 state 還不是響應式的,並且新增加了 context 這個物件。裡面包含 dispatch 、 commit 等函式以及 state 、 getters 等屬性。它就是 vuex 官方文件中所說的 Action 函式接受一個與 store 例項具有相同方法和屬性的 context 物件
這個 context 物件。我們平時在 store 中呼叫的 dispatch 和 commit 就是從這裡解構出來的。接下來讓我們看看 installModule 裡面執行了什麼。
function installModule(store, rootState, path, module, hot) { // 判斷是否是根節點,跟節點的path = [] const isRoot = !path.length; // 取名稱空間,形式類似'childModule/' const namespace = store._modules.getNamespace(path); // 如果namespaced為true,存入_modulesNamespaceMap中 if (module.namespaced) { store._modulesNamespaceMap[namespace] = module; } // 不是根節點,把子元件的每一個state設定到其父級的state屬性上 if (!isRoot && !hot) { // 獲取當前元件的父元件state const parentState = getNestedState(rootState, path.slice(0, -1)); // 獲取當前Module的名字 const moduleName = path[path.length - 1]; store._withCommit(() => { Vue.set(parentState, moduleName, module.state); }); } // 給context物件賦值 const local = (module.context = makeLocalContext(store, namespace, path)); // 迴圈註冊每一個module的Mutation module.forEachMutation((mutation, key) => { const namespacedType = namespace + key; registerMutation(store, namespacedType, mutation, local); }); // 迴圈註冊每一個module的Action module.forEachAction((action, key) => { const type = action.root ? key : namespace + key; const handler = action.handler || action; registerAction(store, type, handler, local); }); // 迴圈註冊每一個module的Getter module.forEachGetter((getter, key) => { const namespacedType = namespace + key; registerGetter(store, namespacedType, getter, local); }); // 迴圈_childern屬性 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot); }); } 複製程式碼
在installModule函式裡,首先判斷是否是根節點、是否設定了名稱空間。在設定了名稱空間的前提下,把 module 存入 store._modulesNamespaceMap 中。在不是跟節點並且不是 hot 的情況下,通過 getNestedState 獲取到父級的 state,並獲取當前 module 的名字, 用 Vue.set() 方法將當前 module 的 state 掛載到父 state 上。然後呼叫 makeLocalContext 函式給 module.context 賦值,設定區域性的 dispatch、commit方法以及getters和state。那麼來看一看這個函式。
function makeLocalContext(store, namespace, path) { // 是否有名稱空間 const noNamespace = namespace === ""; const local = { // 如果沒有名稱空間,直接返回store.dispatch;否則給type加上名稱空間,類似'childModule/'這種 dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args; let { type } = args; if (!options || !options.root) { type = namespace + type; if ( process.env.NODE_ENV !== "production" && !store._actions[type] ) { console.error( `[vuex] unknown local action type: ${ args.type }, global type: ${type}` ); return; } } return store.dispatch(type, payload); }, // 如果沒有名稱空間,直接返回store.commit;否則給type加上名稱空間 commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args; let { type } = args; if (!options || !options.root) { type = namespace + type; if ( process.env.NODE_ENV !== "production" && !store._mutations[type] ) { console.error( `[vuex] unknown local mutation type: ${ args.type }, global type: ${type}` ); return; } } store.commit(type, payload, options); } }; // getters and state object must be gotten lazily // because they will be changed by vm update Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } }); return local; } 複製程式碼
經過 makeLocalContext 處理的返回值會賦值給 local 變數,這個變數會傳遞給 registerMutation、forEachAction、registerGetter 函式去進行相應的註冊。
mutation可以重複註冊,registerMutation 函式將我們傳入的 mutation 進行了一次包裝,將 state 作為第一個引數傳入,因此我們在呼叫 mutation 的時候可以從第一個引數中取到當前的 state 值。
function registerMutation(store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []); entry.push(function wrappedMutationHandler(payload) { // 將this指向store,將makeLocalContext返回值中的state作為第一個引數,呼叫值執行的payload作為第二個引數 // 因此我們呼叫commit去提交mutation的時候,可以從mutation的第一個引數中取到當前的state值。 handler.call(store, local.state, payload); }); } 複製程式碼
action也可以重複註冊。註冊 action 的方法與 mutation 相似,registerAction 函式也將我們傳入的 action 進行了一次包裝。但是 action 中引數會變多,裡面包含 dispatch 、commit、local.getters、local.state、rootGetters、rootState,因此可以在一個 action 中 dispatch 另一個 action 或者去 commit 一個 mutation。這裡也就解答了問題4中提出的疑問。
function registerAction(store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []); entry.push(function wrappedActionHandler(payload, cb) { //與mutation不同,action的第一個引數是一個物件,裡面包含dispatch、commit、getters、state、rootGetters、rootState 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 ); if (!isPromise(res)) { res = Promise.resolve(res); } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit("vuex:error", err); throw err; }); } else { return res; } }); } 複製程式碼
註冊 getters,從getters的第一個引數中可以取到local state、local getters、root state、root getters。getters不允許重複註冊。
function registerGetter(store, type, rawGetter, local) { // getters不允許重複 if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== "production") { console.error(`[vuex] duplicate getter key: ${type}`); } return; } store._wrappedGetters[type] = function wrappedGetter(store) { // getters的第一個引數包含local state、local getters、root state、root getters return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ); }; } 複製程式碼
現在 store 的 _mutation、_action 中已經有了我們自行定義的的 mutation 和 action函式,並且經過了一層內部報裝。當我們在元件中執行 this.$store.dispatch()
和 this.$store.commit()
的時候,是如何呼叫到相應的函式的呢?接下來讓我們來看一看 store 上的 dispatch 和 commit 函式。
commit
commit 函式先進行引數的適配處理,然後判斷當前 action type 是否存在,如果存在則呼叫 _withCommit 函式執行相應的 mutation 。
// 提交mutation函式 commit(_type, _payload, _options) { // check object-style commit //commit支援兩種呼叫方式,一種是直接commit('getName','vuex'),另一種是commit({type:'getName',name:'vuex'}), //unifyObjectStyle適配兩種方式 const { type, payload, options } = unifyObjectStyle( _type, _payload, _options ); const mutation = { type, payload }; // 這裡的entry取值就是我們在registerMutation函式中push到_mutations中的函式,已經經過處理 const entry = this._mutations[type]; if (!entry) { if (process.env.NODE_ENV !== "production") { console.error(`[vuex] unknown mutation type: ${type}`); } return; } // 專用修改state方法,其他修改state方法均是非法修改,在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函式引起的,將會丟擲錯誤 // 不要在釋出環境下啟用嚴格模式!嚴格模式會深度監測狀態樹來檢測不合規的狀態變更——請確保在釋出環境下關閉嚴格模式,以避免效能損失。 this._withCommit(() => { entry.forEach(function commitIterator(handler) { handler(payload); }); }); // 訂閱者函式遍歷執行,傳入當前的mutation物件和當前的state 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 函式中呼叫了 _withCommit 這個函式, 程式碼如下。 _withCommit 是一個代理方法,所有觸發 mutation 的進行 state 修改的操作都經過它,由此來統一管理監控 state 狀態的修改。在嚴格模式下,會深度監聽 state 的變化,如果沒有通過 mutation 去修改 state,則會報錯。官方建議 不要在釋出環境下啟用嚴格模式! 請確保在釋出環境下關閉嚴格模式,以避免效能損失。這裡就解答了問題1中的疑問。
_withCommit(fn) { // 儲存之前的提交狀態false const committing = this._committing; // 進行本次提交,若不設定為true,直接修改state,strict模式下,Vuex將會產生非法修改state的警告 this._committing = true; // 修改state fn(); // 修改完成,還原本次修改之前的狀態false this._committing = committing; } 複製程式碼
dispatch
dispatch 和 commit 的原理相同。如果有多個同名 action,會等到所有的 action 函式完成後,返回的 Promise 才會執行。
// 觸發action函式 dispatch(_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload); const action = { type, payload }; const entry = this._actions[type]; if (!entry) { if (process.env.NODE_ENV !== "production") { console.error(`[vuex] unknown action type: ${type}`); } return; } // 執行所有的訂閱者函式 this._actionSubscribers.forEach(sub => sub(action, this.state)); return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload); } 複製程式碼
至此,整個 installModule 裡涉及到的內容已經分析完畢。現在我們來看一看store樹結構。

我們在 options 中傳進來的 action 和 mutation 已經在 store 中。但是 state 和 getters 還沒有。這就是接下來的 resetStoreVM 方法做的事情。
resetStoreVM
resetStoreVM 函式中包括初始化 store._vm,觀測 state 和 getters 的變化以及執行是否開啟嚴格模式等。state 屬性賦值給 vue 例項的 data 屬性,因此資料是可響應的。這也就解答了問題 3,用到的屬性在 state 中也必須要提前定義好,vue 檢視才可以響應。
function resetStoreVM(store, state, hot) { //儲存老的vm const oldVm = store._vm; // 初始化 store 的 getters store.getters = {}; // _wrappedGetters 是之前在 registerGetter 函式中賦值的 const wrappedGetters = store._wrappedGetters; const computed = {}; forEachValue(wrappedGetters, (fn, key) => { // 將getters放入計算屬性中,需要將store傳入 computed[key] = () => fn(store); // 為了可以通過this.$store.getters.xxx訪問getters 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 // 用一個vue例項來儲存store樹,將getters作為計算屬性傳入,訪問this.$store.getters.xxx實際上訪問的是store._vm[xxx] const silent = Vue.config.silent; Vue.config.silent = true; store._vm = new Vue({ data: { $$state: state }, computed }); Vue.config.silent = silent; // enable strict mode for new vm // 如果是嚴格模式,則啟用嚴格模式,深度 watch state 屬性 if (store.strict) { enableStrictMode(store); } // 若存在oldVm,解除對state的引用,等dom更新後把舊的vue例項銷燬 if (oldVm) { 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()); } } 複製程式碼
開啟嚴格模式時,會深度監聽 $$state 的變化,如果不是通過this._withCommit()方法觸發的state修改,也就是store._committing如果是false,就會報錯。
function enableStrictMode(store) { store._vm.$watch( function() { return this._data.$$state; }, () => { if (process.env.NODE_ENV !== "production") { assert( store._committing, `do not mutate vuex store state outside mutation handlers.` ); } }, { deep: true, sync: true } ); } 複製程式碼
讓我們來看一看執行完 resetStoreVM 後的 store 結構。現在的 store 中已經有了 getters 屬性,並且 getters 和 state 都是響應式的。

至此 vuex 的核心程式碼初始化部分已經分析完畢。原始碼裡還包括一些外掛的註冊及暴露出來的 API 像 mapState mapGetters mapActions mapMutation等函式就不在這裡介紹了,感興趣的可以自行去原始碼裡看看,比較好理解。這裡就不做過多介紹。
總結
vuex的原始碼相比於vue的原始碼來說還是很好理解的。分析原始碼之前建議大家再細讀一遍官方文件,遇到不太理解的地方記下來,帶著問題去讀原始碼,有目的性的研究,可以加深記憶。閱讀的過程中,可以先寫一個小例子,引入 clone 下來的原始碼,一步一步分析執行過程。