深入理解Vuex 框架


Vuex是一個專為Vue服務,用於管理頁面數據狀態、提供統一數據操作的生態系統。它集中於MVC模式中的Model層,規定所有的數據操作必須通過 action – mutation – state change 的流程來進行,再結合Vue的數據視圖雙向綁定特性來實現頁面的展示更新。統一的頁面狀態管理以及操作處理,可以讓復雜的組件交互變得簡單清晰,同時可在調試模式下進行時光機般的倒退前進操作,查看數據改變過程,使code debug更加方便。

最近在開發的項目中用到了Vuex來管理整體頁面狀態,遇到了很多問題。決定研究下源碼,在答疑解惑之外,能深入學習其實現原理。

先將問題拋出來,使學習和研究更有針對性:

  1. 使用Vuex只需執行 Vue.use(Vuex),並在Vue的配置中傳入一個store對象的示例,store是如何實現注入的?
  2. state內部是如何實現支持模塊配置和模塊嵌套的?
  3. 在執行dispatch觸發action(commit同理)的時候,只需傳入(type, payload),action執行函數中第一個參數store從哪里獲取的?
  4. 如何區分state是外部直接修改,還是通過mutation方法修改的?
  5. 調試時的“時空穿梭”功能是如何實現的?

注:本文對有Vuex有實際使用經驗的同學幫助更大,能更清晰理解Vuex的工作流程和原理,使用起來更得心應手。初次接觸的同學,可以先參考Vuex官方文檔進行基礎概念的學習。

一、框架核心流程

進行源碼分析之前,先了解一下官方文檔中提供的核心思想圖,它也代表着整個Vuex框架的運行流程。
vuex-core
如圖示,Vuex為Vue Components建立起了一個完整的生態圈,包括開發中的API調用一環。圍繞這個生態圈,簡要介紹一下各模塊在核心流程中的主要功能:

  • Vue Components:Vue組件。HTML頁面上,負責接收用戶操作等交互行為,執行dispatch方法觸發對應action進行回應。
  • dispatch:操作行為觸發方法,是唯一能執行action的方法。
  • actions:操作行為處理模塊。負責處理Vue Components接收到的所有交互行為。包含同步/異步操作,支持多個同名方法,按照注冊的順序依次觸發。向后台API請求的操作就在這個模塊中進行,包括觸發其他action以及提交mutation的操作。該模塊提供了Promise的封裝,以支持action的鏈式觸發。
  • commit:狀態改變提交操作方法。對mutation進行提交,是唯一能執行mutation的方法。
  • mutations:狀態改變操作方法。是Vuex修改state的唯一推薦方法,其他修改方式在嚴格模式下將會報錯。該方法只能進行同步操作,且方法名只能全局唯一。操作之中會有一些hook暴露出來,以進行state的監控等。
  • state:頁面狀態管理容器對象。集中存儲Vue components中data對象的零散數據,全局唯一,以進行統一的狀態管理。頁面顯示所需的數據從該對象中進行讀取,利用Vue的細粒度數據響應機制來進行高效的狀態更新。
  • getters:state對象讀取方法。圖中沒有單獨列出該模塊,應該被包含在了render中,Vue Components通過該方法讀取全局state對象。

Vue組件接收交互行為,調用dispatch方法觸發action相關處理,若頁面狀態需要改變,則調用commit方法提交mutation修改state,通過getters獲取到state新值,重新渲染Vue Components,界面隨之更新。

二、目錄結構介紹

打開Vuex項目,看下源碼目錄結構。

dir_structure

Vuex提供了非常強大的狀態管理功能,源碼代碼量卻不多,目錄結構划分也很清晰。先大體介紹下各個目錄文件的功能:

  • module:提供module對象與module對象樹的創建功能;
  • plugins:提供開發輔助插件,如“時光穿梭”功能,state修改的日志記錄功能等;
  • helpers.js:提供action、mutations以及getters的查找API;
  • index.js:是源碼主入口文件,提供store的各module構建安裝;
  • mixin.js:提供了store在Vue實例上的裝載注入;
  • util.js:提供了工具方法如find、deepCopy、forEachValue以及assert等方法。

三、初始化裝載與注入

了解大概的目錄及對應功能后,下面開始進行源碼分析。index.js中包含了所有的核心代碼,從該文件入手進行分析。

3.1 裝載實例

先看個簡單的例子:

store.js文件中,加載Vuex框架,創建並導出一個空配置的store對象實例。

然后在index.js中,正常初始化一個頁面根級別的Vue組件,傳入這個自定義的store對象。

問題1所述,以上實例除了Vue的初始化代碼,只是多了一個store對象的傳入。一起看下源碼中的實現方式。

3.2 裝載分析

index.js文件代碼執行開頭,定義局部 Vue 變量,用於判斷是否已經裝載和減少全局作用域查找。

然后判斷若處於瀏覽器環境下且加載過Vue,則執行install方法。

install方法將Vuex裝載到Vue對象上,Vue.use(Vuex) 也是通過它執行,先看下Vue.use方法實現:

若是首次加載,將局部Vue變量賦值為全局的Vue對象,並執行applyMixin方法,install實現如下:

來看下applyMixin方法內部代碼。如果是2.x.x以上版本,可以使用 hook 的形式進行注入,或使用封裝並替換Vue對象原型的_init方法,實現注入。

具體實現:將初始化Vue根組件時傳入的store設置到this對象的$store屬性上,子組件從其父組件引用$store屬性,層層嵌套進行設置。在任意組件中執行 this.$store 都能找到裝載的那個store對象,vuexInit方法實現如下:

看個圖例理解下store的傳遞。

頁面Vue結構圖:
cart_vue_structure

對應store流向:
cart_vue_structure

四、store對象構造

上面對Vuex框架的裝載以及注入自定義store對象進行分析,解決了問題1。接下來詳細分析store對象的內部功能和具體實現,來解答 為什么actions、getters、mutations中能從arguments[0]中拿到store的相關數據? 等問題。

store對象實現邏輯比較復雜,先看下構造方法的整體邏輯流程來幫助后面的理解:

cart_vue_structure

4.1 環境判斷

開始分析store的構造函數,分小節逐函數逐行的分析其功能。

在store構造函數中執行環境判斷,以下都是Vuex工作的必要條件:

  1. 已經執行安裝函數進行裝載;
  2. 支持Promise語法。

assert函數是一個簡單的斷言函數的實現,一行代碼即可實現。

 

4.2 數據初始化、module樹構造

環境判斷后,根據new構造傳入的options或默認值,初始化內部數據。

調用 new Vuex.store(options) 時傳入的options對象,用於構造ModuleCollection類,下面看看其功能。

ModuleCollection主要將傳入的options對象整個構造為一個module對象,並循環調用 this.register([key], rawModule, false) 為其中的modules屬性進行模塊注冊,使其都成為module對象,最后options對象被構造成一個完整的組件樹。ModuleCollection類還提供了modules的更替功能,詳細實現可以查看源文件module-collection.js

4.3 dispatch與commit設置

繼續回到store的構造函數代碼。

封裝替換原型中的dispatch和commit方法,將this指向當前store對象。dispatch和commit方法具體實現如下:

前面提到,dispatch的功能是觸發並傳遞一些參數(payload)給對應type的action。因為其支持2種調用方法,所以在dispatch中,先進行參數的適配處理,然后判斷action type是否存在,若存在就逐個執行(注:上面代碼中的this._actions[type] 以及 下面的 this._mutations[type] 均是處理過的函數集合,具體內容留到后面進行分析)。

commit方法和dispatch相比雖然都是觸發type,但是對應的處理卻相對復雜,代碼如下。

該方法同樣支持2種調用方法。先進行參數適配,判斷觸發mutation type,利用_withCommit方法執行本次批量觸發mutation處理函數,並傳入payload參數。執行完成后,通知所有_subscribers(訂閱函數)本次操作的mutation對象以及當前的state狀態,如果傳入了已經移除的silent選項則進行提示警告。

4.4 state修改方法

_withCommit是一個代理方法,所有觸發mutation的進行state修改的操作都經過它,由此來統一管理監控state狀態的修改。實現代碼如下。

緩存執行時的committing狀態將當前狀態設置為true后進行本次提交操作,待操作完畢后,將committing狀態還原為之前的狀態。

4.5 module安裝

綁定dispatch和commit方法之后,進行嚴格模式的設置,以及模塊的安裝(installModule)。由於占用資源較多影響頁面性能,嚴格模式建議只在開發模式開啟,上線后需要關閉。

 

4.5.1 初始化rootState

上述代碼的備注中,提到installModule方法初始化組件樹根組件、注冊所有子組件,並將其中所有的getters存儲到this._wrappedGetters屬性中,讓我們看看其中的代碼實現。

判斷是否是根目錄,以及是否設置了命名空間,若存在則在namespace中進行module的存儲,在不是根組件且不是 hot 條件的情況下,通過getNestedState方法拿到該module父級的state,拿到其所在的 moduleName ,調用 Vue.set(parentState, moduleName, module.state) 方法將其state設置到父級state對象的moduleName屬性中,由此實現該模塊的state注冊(首次執行這里,因為是根目錄注冊,所以並不會執行該條件中的方法)。getNestedState方法代碼很簡單,分析path拿到state,如下。

 

4.5.2 module上下文環境設置

 

命名空間和根目錄條件判斷完畢后,接下來定義local變量和module.context的值,執行makeLocalContext方法,為該module設置局部的 dispatch、commit方法以及getters和state(由於namespace的存在需要做兼容處理)。

4.5.3 mutations、actions以及getters注冊

定義local環境后,循環注冊我們在options中配置的action以及mutation等。逐個分析各注冊函數之前,先看下模塊間的邏輯關系流程圖:

complete_flow

下面分析代碼邏輯:

registerMutation方法中,獲取store中的對應mutation type的處理函數集合,將新的處理函數push進去。這里將我們設置在mutations type上對應的 handler 進行了封裝,給原函數傳入了state。在執行 commit(‘xxx’, payload) 的時候,type為 xxx 的mutation的所有handler都會接收到state以及payload,這就是在handler里面拿到state的原因。

action和getter的注冊也是同理的,看一下代碼(注:前面提到的 this.actions 以及 this.mutations在此處進行設置)。

action handler比mutation handler以及getter wrapper多拿到dispatch和commit操作方法,因此action可以進行dispatch action和commit mutation操作。

4.5.4 子module安裝

注冊完了根組件的actions、mutations以及getters后,遞歸調用自身,為子組件注冊其state,actions、mutations以及getters等。

 

4.5.5 實例結合

前面介紹了dispatch和commit方法以及actions等的實現,下面結合一個官方的購物車實例中的部分代碼來加深理解。

Vuex配置代碼:

Vuex組件module中各模塊state配置代碼部分:

加載上述配置后,頁面state結構如下圖:

cart_state

state中的屬性配置都是按照option配置中module path的規則來進行的,下面看action的操作實例。

Vuecart組件代碼部分:

Vuexcart.js組件action配置代碼部分:

Vue組件中點擊購買執行當前module的dispatch方法,傳入type值為 ‘checkout’,payload值為 ‘products’,在源碼中dispatch方法在所有注冊過的actions中查找’checkout’的對應執行數組,取出循環執行。執行的是被封裝過的被命名為wrappedActionHandler的方法,真正傳入的checkout的執行函數在wrappedActionHandler這個方法中被執行,源碼如下(注:前面貼過,這里再看一次):

handler在這里就是傳入的checkout函數,其執行需要的commit以及state就是在這里被傳入,payload也傳入了,在實例中對應接收的參數名為products。commit的執行也是同理的,實例中checkout還進行了一次commit操作,提交一次type值為types.CHECKOUT_REQUEST的修改,因為mutation名字是唯一的,這里進行了常量形式的調用,防止命名重復,執行跟源碼分析中一致,調用 function wrappedMutationHandler (payload) { handler(local.state, payload) } 封裝函數來實際調用配置的mutation方法。

看到完源碼分析和上面的小實例,應該能理解dispatch action和commit mutation的工作原理了。接着看源碼,看看getters是如何實現state實時訪問的。

4.6 store._vm組件設置

執行完各module的install后,執行resetStoreVM方法,進行store組件的初始化。

綜合前面的分析可以了解到,Vuex其實構建的就是一個名為store的vm組件,所有配置的state、actions、mutations以及getters都是其組件的屬性,所有的操作都是對這個vm組件進行的。

一起看下resetStoreVM方法的內部實現。

resetStoreVm方法創建了當前store實例的_vm組件,至此store就創建完畢了。上面代碼涉及到了嚴格模式的判斷,看一下嚴格模式如何實現的。

很簡單的應用,監視state的變化,如果沒有通過 this._withCommit() 方法進行state修改,則報錯。

4.7 plugin注入

最后執行plugin的植入。

devtoolPlugin提供的功能有3個:

源碼分析到這里,Vuex框架的實現原理基本都已經分析完畢。

五、總結

最后我們回過來看文章開始提出的5個問題。

1.  使用Vuex只需執行 Vue.use(Vuex),並在Vue的配置中傳入一個store對象的示例,store是如何實現注入的?

:Vue.use(Vuex) 方法執行的是install方法,它實現了Vue實例對象的init方法封裝和注入,使傳入的store對象被設置到Vue上下文環境的$store中。因此在Vue Component任意地方都能夠通過this.$store訪問到該store。

2.  state內部支持模塊配置和模塊嵌套,如何實現的?

:在store構造方法中有makeLocalContext方法,所有module都會有一個local context,根據配置時的path進行匹配。所以執行如dispatch(‘submitOrder’, payload)這類action時,默認的拿到都是module的local state,如果要訪問最外層或者是其他module的state,只能從rootState按照path路徑逐步進行訪問。

3.  在執行dispatch觸發action(commit同理)的時候,只需傳入(type, payload),action執行函數中第一個參數store從哪里獲取的?

:store初始化時,所有配置的action和mutation以及getters均被封裝過。在執行如dispatch(‘submitOrder’, payload)的時候,actions中type為submitOrder的所有處理方法都是被封裝后的,其第一個參數為當前的store對象,所以能夠獲取到 { dispatch, commit, state, rootState } 等數據。

4.  Vuex如何區分state是外部直接修改,還是通過mutation方法修改的?

:Vuex中修改state的唯一渠道就是執行 commit(‘xx’, payload) 方法,其底層通過執行 this._withCommit(fn) 設置_committing標志變量為true,然后才能修改state,修改完畢還需要還原_committing變量。外部修改雖然能夠直接修改state,但是並沒有修改_committing標志位,所以只要watch一下state,state change時判斷是否_committing值為true,即可判斷修改的合法性。

5.  調試時的”時空穿梭”功能是如何實現的?

:devtoolPlugin中提供了此功能。因為dev模式下所有的state change都會被記錄下來,’時空穿梭’ 功能其實就是將當前的state替換為記錄中某個時刻的state狀態,利用 store.replaceState(targetState) 方法將執行this._vm.state = state 實現。

源碼中還有一些工具函數類似registerModule、unregisterModule、hotUpdate、watch以及subscribe等,如有興趣可以打開源碼看看,這里不再細述。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM