[Vue.js進階]從源碼角度剖析 Vuex

image

前言

以前幾篇解析 Vue 源碼的文章都是完整的分析整個源碼的執行過程,這篇文章我會將重點放在覈心原理的解析javascript

完整源碼地址vue

有興趣的朋友也能夠看我學習源碼時的詳細註釋 源碼地址java

Vuex 版本:3.1.0git

Vuex 簡介

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式,通俗的來講就是將本來分散在各個組件的數據,經過一個公共的倉庫存儲,使得每一個組件都能直接從 Vuex 中獲取數據,能夠把它想象成一個全局變量,可是和全局變量不一樣的是github

  1. Vuex 狀態存儲是響應式的,當 Vuex 中的狀態發生改變,會通知全部依賴到的組件更新數據
  2. 強調狀態的可預測,可追蹤,因此嚴格模式沒法直接從 Vuex 中修改狀態,必須經過提交 mutation 同步修改

當某些數據可能會發生變化,而且被多個不一樣的組件依賴時,能夠考慮將數據放到 Vuex 中存儲,例如表格每頁顯示的最大條數vue-router

將最大頁數存儲在 state 中,一旦用戶修改最大頁數,須要反映到全部分頁器組件,這時只需派發一個 mutation 修改對應的 state 便可vuex

使用 Vuex

使用 Vuex 分爲 3 步api

  1. 安裝 Vuex 插件
  2. 實例化 Vuex 的倉庫 Store
  3. 將第二步的實例傳入根 Vue 實例中

安裝 Vuex 插件核心原理和 vue-router 相同,調用插件暴露的 install 方法,經過 Vue.mixin 全局混入 beforeCreate 鉤子,以後每當初始化一個組件,都會生成一個 $store 屬性指向根 Vue 實例中的 store 對象,最後全部的組件均可以經過 this.$store 訪問根實例中的 store 對象數組

當咱們執行 new Vuex.Store 就會建立一個倉庫實例 store閉包

以後將第二步生成的實例注入根 Vue 實例

實例化 Store

Vuex 全部的行爲都是圍繞 new Vuex.Store 生成的 store 實例展開的,在實例化 Store 的過程當中,主要作了三件事

  1. 初始化模塊
  2. 安裝模塊
  3. 建立一個管理全部數據的 Vue 實例

初始化模塊

咱們知道,Vuex 是支持模塊嵌套的,即在一個 Vuex 模塊內部,能夠經過 modules 屬性嵌套子模塊,從而造成一個樹形的結構,經過模塊的劃分能夠在複雜的狀況更好的管理模塊,Vuex 將這個樹形結構的模塊保存在 store 實例的 _modules 屬性中

this._modules = new ModuleCollection(options)
複製代碼

image.png

ModuleCollection 的實例表明了全部模塊的集合,即這個樹形結構,我稱之爲模塊樹,它在實例化時會調用 register 方法,註冊全部模塊

rawModule 即 new Vuex.Store 傳入的模塊配置項,包括根模塊在內,每一個模塊都是 Module 的一個實例,將第一次調用 register 方法傳入的模塊做爲 root 根模塊,以後會遍歷 modules 對象,遞歸調用 register 註冊子模塊

同時子模塊會經過 get 方法找到父模塊,並經過 addChild 往父模塊的 _children 屬性添加當前子模塊,從而創建父子關係

這裏有個很是重要的參數,即 path ,它是一個數組,第一次調用 register 時, path 是一個空數組,每當遞歸調用時,會將 path 拼接當前子模塊的屬性名,舉個例子

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"],而對於孫子模塊 moduleB,path 的值爲 ["moduleA,"moduleB"],有了這樣的層級關係,就能夠經過 path 數組很好的找到對應的模塊

安裝模塊

安裝模塊和初始化模塊的區別在於,初始化模塊會創建整個模塊樹(ModuleCollection ),而安裝模塊會給模塊添加做用於每一個模塊的 dispatch,mutation,getters 的 context 對象

什麼意思呢,以上圖爲例,當咱們在一個 action 中觸發一個 mutation 時,通常會經過 action 第一個參數 context 的 commit 屬性來觸發

可是若是別的模塊也存在名爲 mutation 的 mutation ,此時就會發生衝突, Vuex 爲了解決這個問題引入了命名空間的概念,引用官網的一句話

若是但願你的模塊具備更高的封裝度和複用性,你能夠經過添加 namespaced: true 的方式使其成爲帶命名空間的模塊。當模塊被註冊後,它的全部 getter、action 及 mutation 都會自動根據模塊註冊的路徑調整命名

當設置 namespaced:true 的模塊,其 context 參數中的 commit 只會影響到當前模塊下的 mutations,實現方法其實很是的簡單:執行 context .commit 最終會給 mutation 拼上模塊的命名前綴再執行全局對應的 commit

若是模塊沒有設置 namespaced 則使用全局的 store.commit,不然會拼上 namespace 再調用全局的 store.commit,而 namespace 是根據以前介紹到的模塊的 path 數組生成的命名前綴

getNamespace 會經過 reduce 遍歷 path 數組,遞歸向下遍歷子模塊,當子模塊設置了 namespaced 時會給 namespace 變量拼接當前模塊名

因此當 mutation 拼上模塊的命名前綴就不會發生衝突,結合以前的例子,由於子模塊 moduleA 中的 path 值爲 ["moduleA"],因此 mutation 最終會變爲 moduleA/mutation ,而孫子模塊 moduleB 中的 path 值爲 ["moduleA","moduleB"], mutation 最終會變爲 moduleA/moduleB/mutation

對於 context.actions 和 context.getters 實現大體的思路也是相同,最後會遞歸的給模塊樹(ModuleCollection )的全部子模塊生成 context 對象

carbon.png

建立 Vue 實例

之因此 Vuex 中的狀態在發生變化時可以通知到全部依賴的組件,是由於 Vuex 在 Store 實例中建立了一個內部的 Vue 實例用來管理全部的狀態

根模塊的 state 表明了整個 Vuex 的全部數據, Vuex 將根模塊的 state 做爲 $$state 屬性的值保存在內部 Vue 實例中,同時將 wrappedGetters (在安裝模塊時,會將全部的 getters 保存在這個對象中)中的全部的 getter 做爲 computed 屬性

經過 Vue 響應式原理能夠知道,若是組件經過 this.$store.<prop 名> 依賴到了 Vuex 的某個數據,當 $$state 中的任何狀態發生變化,都會觸發內部的 setter 函數, 從而通知依賴到的組件發生視圖更新

這裏再介紹一下 Vuex 中的 getters,它們最終都會變成內部 Vue 實例的 computed 屬性, 當某個 getter 依賴的值發生變化會觸發從新計算,從而執行 fn(store) 這個函數,store 是的 Store 實例,而 fn 又是什麼呢?在安裝模塊時,會定義 store._wrappedGetters 這個對象,fn 就是 wrappedGetter 這個函數

根據官方文檔能夠發現,每一個 getter 支持 4 個參數,當前模塊的 state,當前模塊的 getters,全局的 state,全局的 getters,對應 rawGetter 的 4 個參數(rawGetter 即開發者定義的 getter 函數)

Vuex 經過返回一個函數,使其保存了 local(context )對象,又經過傳入參數使得可以訪問全局的 store 實例,很是靈活的運用了閉包

Vuex 核心 api

Vuex 容許開發者經過 dispatch 派發一個異步的 action,經過 commit 提交一個同步的 mutation

之因此區分異步和同步是爲了可以更加準確的追蹤狀態的變化,由於就像沒法準確知道一個響應什麼時候會收到同樣,異步操做並不能準確的知道什麼時候修改的數據,因此不能將修改 state 的操做放在 action 中,可是咱們能夠在異步完成後經過提交一個 commit 的形式同步的修改 state ,同步的特色使得任何狀態的變化都可以確切知道執行先後 state 的狀態,以便完成一些高級操做, 例如記錄日誌,時間旅行等

dispatch

在安裝模塊中給模塊添加做用於每一個模塊的 dispatch 時,會給每一個 action 包裹一層函數,做用是保證每一個 action 都是一個 Promise

而 store 實例的 dispatch 方法會經過 Promise 的 then 方法解析 action ,當存在同名的 action ( 多個模塊含有相同命名的 action 且沒有使用命名空間),會使用 Promise.all 併發的解析

commit

經過 commit 方法能夠同步的執行一個 mutation,以前提到,在嚴格模式下 Vuex 規定只有 mutation 才能同步修改數據,由於這樣才能方便數據追蹤,Vuex 聲明瞭一個 _withCommit 方法,只有調用這個方法才能修改 state,相似一個開關的功能,當執行一個 mutation 時,會調用它使得容許修改 state

至於只有調用 _withCommit 方法才能修改 state 的原理也很簡單,由於 state 都被保存在內部 Vue 實例中,經過 Vue 的 $watch 深度監聽整個 state 當發現 _committing 爲 false 就發出警告

在根模塊設置 strict 爲 true 開啓嚴格模式時纔會啓用檢查,多是考慮到深度監聽影響性能,因此推薦只在開發環境啓用

其餘 API 原理

Vuex 還提供了不少其餘的 API ,涉及到篇幅緣由這裏簡要介紹下內部實現原理

map 系列的輔助函數

在組件中經過 mapState ,mapActions,mapMutations,mapGetters 輔助函數,能夠省去寫 this.$store.<prop 名> ,直接使用 this.<prop 名> 這種寫法,同時讓項目分層更加清晰,也是比較推薦的寫法,這些輔助函數最終都會返回一個對象,因此須要使用 ES9 的對象擴展運算符將對象放入對應的 Vue 屬性中

同時這些 map 輔助函數能夠經過傳入多個參數來實現命名空間的功能

核心原理是將傳入的第一個參數,也就是命名前綴拼上對應的 state 名(action / mutation / getter 名),去 store 實例中 _modulesNamespaceMap 屬性中找到對應模塊(Module 實例),由於在安裝模塊的過程當中會給每一個模塊添加 context 屬性,因此這裏就能夠經過 context 對象拿到做用於當前模塊的 state (action / mutation / getter )

至於 _modulesNamespaceMap 是在以前安裝模塊時生成的,保存了每一個模塊和對應的命名前綴

拿到 context 對象後,根據不一樣的功能返回不一樣的對象給組件

  • state:返回指定模塊內部的 state,若是是一個函數就傳入 store 實例返回執行後的結果
  • action:返回指定模塊 context.dispatch,執行 action 會拼上命名前綴執行 store 的 dispatch
  • mutation: 同 action
  • getter:訪問 getter 會拼上命名前綴訪問 store.getters 對象對應的 getter

plugins

Vuex 自身也提供了一個插件功能,用於監聽 action 和 mutation

原理是採用了觀察者模式,聲明一個訂閱者數組,每當執行完一個 action / mutation 都會遍歷數組中全部訂閱者依次執行回調,同時回調中還會傳入 action / mutation 名和當前的 state 狀態,而插件只須要調用 store.subscribeAction / store.subscribe 將訂閱者放入數組中便可

// 調用訂閱者的回調函數
 this._subscribers.forEach(sub => sub(mutation, this.state))
複製代碼

replaceState

根據傳入的參數替換 Vuex 中的 state,Vuex 使用這個 API 實現時光旅行的功能

原理也很簡單,只需經過 _withCommit 修改內部 Vue 實例中保存全部狀態的 $$state 屬性便可,雖然時光旅行只能在開發模式中使用,可是咱們能夠將它抽象出來,開發一個 plugin 記錄每一個 mutation 提交時的狀態(須要深拷貝)和步驟,調用 replaceState 使數據回滾到指定步驟中

registerModule

Vuex 還提供了動態註冊模塊的功能,經過傳入模塊和模塊插入的位置,來動態注入到已有的模塊樹中

介紹模塊樹 ModuleCollection 時提到它有一個 register 方法,經過傳入的 path 數組和模塊,插入到模塊樹中對應的位置,正好對應 registerModule 的 2 個參數,而 registerModule 在將模塊插入到整個模塊樹以後,還會給傳入的模塊執行安裝模塊的函數,以及重置 Vue 實例

由於全部的數據都會保存在 store.state 中,因此重置 Vue 實例並不會致使丟失以前的數據

參考資料

Vue.js 技術揭祕

相關文章
相關標籤/搜索