前言
之前幾篇解析 Vue 源碼的文章都是完整的分析整個(gè)源碼的執(zhí)行過(guò)程簿透,這篇文章我會(huì)將重點(diǎn)放在核心原理的解析,不會(huì)具體解釋每個(gè)函數(shù)的執(zhí)行順序菱属,調(diào)用棧情況
有興趣的朋友也可以看我學(xué)習(xí)源碼時(shí)的詳細(xì)注釋 源碼地址
Vuex 版本:3.1.0
Vuex 簡(jiǎn)介
Vuex 是一個(gè)專為 Vue.js 應(yīng)用程序開(kāi)發(fā)的狀態(tài)管理模式引瀑,通俗的來(lái)說(shuō)就是將原本分散在各個(gè)組件的數(shù)據(jù),通過(guò)一個(gè)公共的倉(cāng)庫(kù)存儲(chǔ)州藕,使得每個(gè)組件都能直接從 Vuex 中獲取數(shù)據(jù)束世,可以把它想象成一個(gè)全局變量,但是和全局變量不同的是
- Vuex 狀態(tài)存儲(chǔ)是響應(yīng)式的床玻,當(dāng) Vuex 中的狀態(tài)發(fā)生改變毁涉,會(huì)通知所有依賴到的組件更新數(shù)據(jù)
- 強(qiáng)調(diào)狀態(tài)的可預(yù)測(cè),可追蹤锈死,所以嚴(yán)格模式無(wú)法直接從 Vuex 中修改狀態(tài)贫堰,必須通過(guò)提交 mutation 同步修改
當(dāng)某些數(shù)據(jù)可能會(huì)發(fā)生變化,并且被多個(gè)不同的組件依賴時(shí)待牵,可以考慮將數(shù)據(jù)放到 Vuex 中存儲(chǔ)其屏,例如表格每頁(yè)顯示的最大條數(shù)
將最大頁(yè)數(shù)存儲(chǔ)在 state 中,一旦用戶修改最大頁(yè)數(shù)缨该,需要反映到所有分頁(yè)器組件偎行,這時(shí)就可以派發(fā)一個(gè) mutation 修改 Vuex 中的 state 即可
使用 Vuex
使用 Vuex 分為 3 步
- 安裝 Vuex 插件
- 實(shí)例化 Vuex 的倉(cāng)庫(kù) Store
- 將第二步的實(shí)例傳入根 Vue 實(shí)例中
安裝 Vuex 插件核心原理和 vue-router 相同,調(diào)用插件暴露的 install 方法,通過(guò) Vue.mixin 全局混入 beforeCreate 鉤子蛤袒,之后每當(dāng)初始化一個(gè)組件熄云,都會(huì)生成一個(gè) $store 屬性指向根 Vue 實(shí)例中的 store 對(duì)象
當(dāng)我們執(zhí)行 new Vuex.Store 就會(huì)創(chuàng)建一個(gè)倉(cāng)庫(kù)實(shí)例 store
之后將第二步生成的實(shí)例注入根 Vue 實(shí)例
實(shí)例化 Store
Vuex 所有的行為都是圍繞 new Vuex.Store 生成的 store 實(shí)例展開(kāi)的,在實(shí)例化 Store 的過(guò)程中妙真,主要做了三件事
- 初始化模塊
- 安裝模塊
- 創(chuàng)建一個(gè)管理所有數(shù)據(jù)的 Vue 實(shí)例
初始化模塊
我們知道皱碘,Vuex 是支持模塊嵌套的,即在一個(gè) Vuex 模塊內(nèi)部隐孽,可以通過(guò) modules 屬性嵌套子模塊癌椿,從而形成一個(gè)樹(shù)形的結(jié)構(gòu),通過(guò)模塊的劃分可以在復(fù)雜的情況更好的管理模塊菱阵,Vuex 將這個(gè)樹(shù)形結(jié)構(gòu)的模塊保存在 store 實(shí)例的 _modules 屬性中
this._modules = new ModuleCollection(options)
ModuleCollection 的實(shí)例代表了所有模塊的集合踢俄,即這個(gè)樹(shù)形結(jié)構(gòu),我稱之為模塊樹(shù)晴及,它在實(shí)例化時(shí)會(huì)調(diào)用 register
方法都办,注冊(cè)所有模塊
rawModule 即 new Vuex.Store 傳入的模塊配置項(xiàng),包括根模塊在內(nèi)虑稼,每個(gè)模塊都是 Module 的一個(gè)實(shí)例琳钉,將第一次調(diào)用 register
方法傳入的模塊作為 root 根模塊,之后會(huì)遍歷 modules 對(duì)象蛛倦,遞歸調(diào)用 register 注冊(cè)子模塊
同時(shí)子模塊會(huì)通過(guò) get
方法找到父模塊歌懒,并通過(guò) addChild
往父模塊的 _children 屬性添加當(dāng)前子模塊,從而建立父子關(guān)系
這里有個(gè)非常重要的參數(shù)溯壶,即 path 及皂,它是一個(gè)數(shù)組,第一次調(diào)用 register
時(shí)且改, path 是一個(gè)空數(shù)組验烧,每當(dāng)遞歸調(diào)用時(shí),會(huì)將 path 拼接當(dāng)前子模塊的屬性名又跛,舉個(gè)例子
export default new Vuex.Store({
// 根模塊
modules: {
// 子模塊A
moduleA: {
actions: {
action(context ) {context .commit('mutation')},
},
mutations: {
mutation() {}
},
modules: {
// 孫子模塊B
moduleB: {
actions: {
action(context ) {context .commit('mutation')},
},
mutations: {
mutation() {}
},
}
}
},
}
})
在子模塊 moduleA中碍拆,path 的值為 ["moduleA"],而對(duì)于孫子模塊 moduleB慨蓝,path 的值為 ["moduleA,"moduleB"]感混,有了這樣的層級(jí)關(guān)系,就可以通過(guò) path 數(shù)組很好的找到對(duì)應(yīng)的模塊
安裝模塊
安裝模塊和初始化模塊的區(qū)別在于菌仁,初始化模塊會(huì)建立整個(gè)模塊樹(shù)(ModuleCollection )浩习,而安裝模塊會(huì)給模塊添加作用于每個(gè)模塊的 dispatch,mutation济丘,getters 的 context 對(duì)象
什么意思呢谱秽,以 mutation 舉例洽蛀,當(dāng)我們?cè)谝粋€(gè) action 中觸發(fā)一個(gè) mutation 時(shí),一般會(huì)通過(guò) action 第一個(gè)參數(shù) context 的 commit 屬性來(lái)觸發(fā)
actions: {
action(context ) {
context .commit('mutation')
}
}
但是如果別的模塊也存在名為 mutationA 的 mutation 疟赊,此時(shí)就會(huì)發(fā)生沖突郊供, Vuex 為了解決這個(gè)問(wèn)題引入了命名空間的概念,引用官網(wǎng)的一句話
如果希望你的模塊具有更高的封裝度和復(fù)用性近哟,你可以通過(guò)添加 namespaced: true 的方式使其成為帶命名空間的模塊驮审。當(dāng)模塊被注冊(cè)后,它的所有 getter吉执、action 及 mutation 都會(huì)自動(dòng)根據(jù)模塊注冊(cè)的路徑調(diào)整命名
當(dāng)設(shè)置 namespaced:true
的模塊疯淫,其 context 參數(shù)中的 commit 只會(huì)影響到當(dāng)前模塊下的 mutations,實(shí)現(xiàn)方法其實(shí)非常的簡(jiǎn)單:執(zhí)行 context .commit 最終會(huì)給 mutation 拼上模塊的命名前綴再執(zhí)行全局對(duì)應(yīng)的 commit
如果模塊沒(méi)有設(shè)置 namespaced 則使用全局的 store.commit戳玫,否則會(huì)拼上 namespace 再調(diào)用全局的 store.commit熙掺,而 namespace 是根據(jù)之前介紹到的模塊的 path 數(shù)組生成的命名前綴
getNamespace
會(huì)通過(guò) reduce 遍歷 path 數(shù)組,遞歸向下遍歷子模塊咕宿,當(dāng)子模塊設(shè)置了 namespaced 時(shí)會(huì)給 namespace 變量拼接當(dāng)前模塊名
所以當(dāng) mutation 拼上模塊的命名前綴就不會(huì)發(fā)生沖突币绩,結(jié)合之前的例子,因?yàn)樽幽K moduleA 中的 path 值為 ["moduleA"]府阀,所以 action 最終會(huì)變?yōu)?moduleA/action
缆镣,而孫子模塊 moduleB 中的 path 值為 ["moduleA","moduleB"], action 最終會(huì)變?yōu)?moduleA/moduleB/action
對(duì)于 context .actions 和 context .getters 實(shí)現(xiàn)大致的思路也是相同试浙,最后會(huì)遞歸的給模塊樹(shù)(ModuleCollection )的所有子模塊生成 context 對(duì)象
創(chuàng)建 Vue 實(shí)例
之所以 Vuex 中的狀態(tài)在發(fā)生變化時(shí)能夠通知到所有依賴的組件董瞻,是因?yàn)?Vuex 在 Store 實(shí)例中創(chuàng)建了一個(gè)內(nèi)部的 Vue 實(shí)例用來(lái)管理所有的狀態(tài)
Vuex 會(huì)將根模塊的 state 作為 $$state 屬性的值保存在內(nèi)部 Vue 實(shí)例中,同時(shí)將 wrappedGetters (在安裝模塊時(shí)川队,會(huì)將所有的 getters 保存在這個(gè)屬性中)中的所有的 getter 作為 computed 屬性
通過(guò) Vue 響應(yīng)式原理可以知道力细,如果在組件通過(guò) this.$store.<prop 名> 依賴到了 Vuex 的某個(gè)數(shù)據(jù),當(dāng) $$state 中的任何狀態(tài)發(fā)生變化固额,都會(huì)觸發(fā)內(nèi)部的 setter 函數(shù), 從而通知依賴到的組件發(fā)生視圖更新
這里再介紹一下 Vuex 中的 getters煞聪,它們最終都會(huì)變成內(nèi)部 Vue 實(shí)例的 computed 屬性斗躏, 當(dāng)某個(gè) getter 依賴的值發(fā)生變化會(huì)觸發(fā)重新計(jì)算,從而執(zhí)行 fn(store) 這個(gè)函數(shù)昔脯,store 是的 Store 實(shí)例啄糙,而 fn 又是什么呢?在安裝模塊時(shí)云稚,會(huì)定義 store._wrappedGetters 這個(gè)對(duì)象隧饼,fn 就是 wrappedGetter 這個(gè)函數(shù)
根據(jù)官方文檔可以發(fā)現(xiàn),每個(gè) getter 支持 4 個(gè)參數(shù)静陈,當(dāng)前模塊的 state燕雁,當(dāng)前模塊的 getters诞丽,全局的 state,全局的 getters拐格,對(duì)應(yīng) rawGetter 的 4 個(gè)參數(shù)(rawGetter 即開(kāi)發(fā)者定義的 getter 函數(shù))
Vuex 通過(guò)返回一個(gè)函數(shù)僧免,使其保存了 local(context )對(duì)象,又通過(guò)傳入?yún)?shù)使得能夠訪問(wèn)全局的 store 實(shí)例捏浊,非常靈活的運(yùn)用了閉包
Vuex 核心 api
Vuex 允許開(kāi)發(fā)者通過(guò) dispatch 派發(fā)一個(gè)異步的 action懂衩,通過(guò) commit 提交一個(gè)同步的 mutation
之所以區(qū)分異步和同步是為了能夠更加準(zhǔn)確的追蹤狀態(tài)的變化,因?yàn)榫拖駸o(wú)法準(zhǔn)確知道一個(gè)響應(yīng)何時(shí)會(huì)收到一樣金踪,異步操作并不能準(zhǔn)確的知道何時(shí)修改的數(shù)據(jù)浊洞,所以不能將修改 state 的操作放在 action 中,但是我們可以在異步完成后通過(guò)提交一個(gè) commit 的形式同步的修改 state 胡岔,同步的特點(diǎn)使得任何狀態(tài)的變化都能夠確切知道執(zhí)行前后 state 的狀態(tài)沛申,以便完成一些高級(jí)操作, 例如記錄日志姐军,時(shí)間旅行等
dispatch
在安裝模塊中給模塊添加作用于每個(gè)模塊的 dispatch 時(shí)铁材,會(huì)給每個(gè) action 包裹一層函數(shù),作用是保證每個(gè) action 都是一個(gè) Promise
而 store 實(shí)例的 dispatch 方法會(huì)通過(guò) Promise 的 then 方法解析 action 奕锌,當(dāng)存在同名的 action ( 多個(gè)模塊含有相同命名的 action 且沒(méi)有使用命名空間)著觉,會(huì)使用 Promise.all 并發(fā)的解析
commit
通過(guò) commit 方法可以同步的執(zhí)行一個(gè) mutation,之前提到惊暴,在嚴(yán)格模式下 Vuex 規(guī)定只有 mutation 才能同步修改數(shù)據(jù)饼丘,因?yàn)檫@樣才能方便數(shù)據(jù)追蹤,Vuex 聲明了一個(gè) _withCommit
方法辽话,只有調(diào)用這個(gè)方法才能修改 state肄鸽,類似一個(gè)開(kāi)關(guān)的功能,同時(shí)在執(zhí)行一個(gè) mutation 時(shí)油啤,會(huì)調(diào)用它使得允許修改 state
至于只有調(diào)用 _withCommit
方法才能修改 state 的原理也很簡(jiǎn)單典徘,因?yàn)?state 都被保存在內(nèi)部 Vue 實(shí)例中,通過(guò) Vue 的 $watch 深度監(jiān)聽(tīng)整個(gè) state 當(dāng)發(fā)現(xiàn) _committing 為 false 就發(fā)出警告
在根模塊設(shè)置 strict 為 true 開(kāi)啟嚴(yán)格模式時(shí)才會(huì)啟用檢查益咬,可能是考慮到深度監(jiān)聽(tīng)影響性能逮诲,所以推薦只在開(kāi)發(fā)環(huán)境啟用
其他 API 原理
Vuex 還提供了很多其他的 API ,涉及到篇幅原因這里簡(jiǎn)要介紹下內(nèi)部實(shí)現(xiàn)原理
map 系列的輔助函數(shù)
在組件中通過(guò) mapState 幽告,mapActions梅鹦,mapMutations,mapGetters 輔助函數(shù)冗锁,可以省去寫(xiě) this.$store.<prop 名> 齐唆,直接使用 this.<prop 名> 這種寫(xiě)法,并且讓項(xiàng)目分層更加清晰冻河,也是比較推薦的寫(xiě)法箍邮,這些輔助函數(shù)最終都會(huì)返回一個(gè)對(duì)象茉帅,所以需要使用 ES9 的對(duì)象擴(kuò)展運(yùn)算符將對(duì)象放入對(duì)應(yīng)的 Vue 屬性中
同時(shí)這些 map 輔助函數(shù)可以通過(guò)傳入多個(gè)參數(shù)來(lái)實(shí)現(xiàn)命名空間的功能
核心原理是將傳入的第一個(gè)參數(shù),也就是命名前綴拼上對(duì)應(yīng)的 state 名(action / mutation / getter 名)媒殉,去 store 實(shí)例中 _modulesNamespaceMap 屬性中找到對(duì)應(yīng)模塊(Module 實(shí)例)担敌,因?yàn)樵诎惭b模塊的過(guò)程中會(huì)給每個(gè)模塊添加 context 屬性,所以這里就可以通過(guò) context 對(duì)象拿到作用于當(dāng)前模塊的 state (action / mutation / getter )
至于 _modulesNamespaceMap 是在之前安裝模塊時(shí)生成的廷蓉,保存了每個(gè)模塊和對(duì)應(yīng)的命名前綴
拿到 context 對(duì)象后全封,根據(jù)不同的功能返回不同的對(duì)象給組件
- state:返回指定模塊內(nèi)部的 state,如果是一個(gè)函數(shù)就傳入 store 實(shí)例返回執(zhí)行后的結(jié)果
- action:返回指定模塊 context.dispatch桃犬,執(zhí)行 action 會(huì)拼上命名前綴執(zhí)行 store
的 dispatch - mutation: 同 action
- getter:訪問(wèn) getter 會(huì)拼上命名前綴訪問(wèn) store.getters 對(duì)象對(duì)應(yīng)的 getter
plugins
Vuex 自身也提供了一個(gè)插件功能刹悴,用于監(jiān)聽(tīng) action 和 mutation
原理是采用了觀察者模式,聲明一個(gè) subs 數(shù)組攒暇,每當(dāng)執(zhí)行完一個(gè) action / mutation 都會(huì)遍歷所有數(shù)組依次執(zhí)行回調(diào)土匀,而插件只需要調(diào)用 store.subscribeAction / store.subscribe 將插件放入 subs 數(shù)組中即可
// 調(diào)用訂閱者的回調(diào)函數(shù)
this._subscribers.forEach(sub => sub(mutation, this.state))
replaceState
根據(jù)傳入的參數(shù)替換 Vuex 中的 state,Vuex 使用這個(gè) API 實(shí)現(xiàn)時(shí)光旅行的功能
原理也很簡(jiǎn)單形用,通過(guò) _withCommit
修改內(nèi)部 Vue 實(shí)例中保存所有狀態(tài)的 $$state就轧,雖然時(shí)光旅行只能在開(kāi)發(fā)模式中使用,但是我們可以將它抽象出來(lái)田度,開(kāi)發(fā)一個(gè) plugin 記錄每個(gè) mutation 提交時(shí)的狀態(tài)(需要深拷貝)和步驟妒御,調(diào)用 replaceState 使數(shù)據(jù)回滾到指定步驟中
registerModule
Vuex 還提供了動(dòng)態(tài)注冊(cè)模塊的功能,通過(guò)傳入模塊和模塊插入的位置镇饺,來(lái)動(dòng)態(tài)注入到已有的模塊樹(shù)中
介紹模塊樹(shù) ModuleCollection 時(shí)提到它有一個(gè) register 方法乎莉,通過(guò)傳入的 path 數(shù)組和模塊,插入到模塊樹(shù)中對(duì)應(yīng)的位置奸笤,正好對(duì)應(yīng) registerModule 的 2 個(gè)參數(shù)惋啃,而 registerModule 在將模塊插入到整個(gè)模塊樹(shù)之后,還會(huì)給傳入的模塊執(zhí)行安裝模塊的函數(shù)监右,以及重置 Vue 實(shí)例
因?yàn)樗械臄?shù)據(jù)都會(huì)保存在 store.state 中边灭,所以重置 Vue 實(shí)例并不會(huì)導(dǎo)致丟失之前的數(shù)據(jù)
找呀找呀找工作~
本人為18年畢業(yè)本科生,坐標(biāo)上海秸侣,1年多的前端開(kāi)發(fā)經(jīng)驗(yàn)存筏,希望找一個(gè)在前端領(lǐng)域有一定深度和規(guī)模團(tuán)隊(duì)的互聯(lián)網(wǎng)企業(yè),歡迎在評(píng)論區(qū)能留下聯(lián)系方式或者聯(lián)系我的郵箱1996yeyan@gmail.com味榛,非常感謝~