[Vue.js進階]從源碼角度剖析vue-router(二)

前言

上篇中主要敘述了 vue-router 的註冊和實例化過程,以及如何生成 $router, $route 對象html

在本篇中將會講述:vue

  • $route 對象生成的時機html5

  • 路由守衛的原理git

  • 路由懶加載的原理github

文中的源碼截圖只保留核心邏輯 完整源碼地址vue-router

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

vue-router 版本:3.0.2promise

$route 對象生成的時機

在上篇中解釋了在調用 new Router 生成 vue-router 實例時,會經過 createMatcher 給實例建立一個 matcher 對象,matcher 對象同時含有 matchaddRoutes 兩個方法瀏覽器

圖1:閉包

image

另外上篇中還講了,在建立完 vue-router 實例後,調用 Vue.use(Router) 會混入2個全局鉤子 beforeCreate 和 destroyed

圖2:

image

此時圖中第7行的 init 方法會初始化整個 vue-router ,而實例化和初始化 vue-router 是有區別的,實例化指的是經過 new Router 生成 vue-router 實例,初始化能夠理解爲進行全局第一次的路由跳轉時,讓 vue-router 實例和組件創建聯繫,使得路由可以接管組件

接下來咱們來看 vue-router 是如何初始化的

圖3:

image

在上篇中我也講述了 vue-router 會根據當前使用的路由模式(hash,html5,abstract)來生成 history 屬性,接着 init 方法會根據 history 屬性來執行不一樣的邏輯,可是能夠發現,不論是使用 hash 路由仍是 html5 的路由,都會執行 transitionTo 這個方法,它是整個路由跳轉的核心方法

路由跳轉

圖4:(刪除了取消路由導航的邏輯)

image

能夠發現圖4的第5行代碼執行了 vue-router 實例的 match 方法,它最終會執行上篇咱們分析過的 matcher 屬性的 match 方法,而且傳入了2個參數

  • location:經過圖3中的 getCurrentLocation 方法,最終會生成一個跳轉目標的 loaction 對象(經過 push / replace 方法跳轉),或一個跳轉目標的路徑(經過瀏覽器 url 跳轉)

  • current:當前頁面的路由 $route 對象

圖5(示例):

image

圖6:

image

繼續沿用圖5 的示例,當咱們直接在瀏覽器的 url 中輸入http://localhost:8080/#/comp1/comp1Child 時,能夠觀察到 location 參數爲跳轉目標的路徑,而且此時是全局第一次調用 transitionTo 方法,vue-router 默認第一次跳轉的 current 參數爲根路徑的 $route 對象,而之後的跳轉,current 會變成當前路由的 $route 對象

圖7(第一次 history.current 值爲根路徑轉換而來的 $route 對象):

image

分析過 match 方法的2個參數後,接着會執行上篇中分析過的 match 方法

(在建立 $router 的 match 方法中,其實 current 參數通常不多用到,主要圍繞 location 參數再結合3個路由映射表生成 $route 對象)

圖8(執行圖4的 vue-router 實例的 match 方法最後會執行到上篇分析的 match 方法):

image

此時,這個最終執行的這個 match 方法就會建立出一個 $route 對象,並賦值給圖4的 route 屬性,隨後會進入 confirmTransition 這個方法,它負責控制全部的路由守衛的執行,咱們來看一下它的內部是如何運行的

路由守衛的原理

本小結會介紹 vue-router 一個比較重要的部分:路由守衛

和組件的生命週期的鉤子不一樣,路由守衛將重點放在路由上,可以控制路由跳轉,通常用在頁面級別的路由跳轉時控制跳轉的邏輯,好比在路由守衛中檢查用戶是否有進入當前頁面的權限,沒有則跳轉到受權頁面,亦或是在離開頁面時警告用戶有未確認的信息,確認後才能跳轉等等

在路由守衛中,通常會接收3個參數,to,from,next,前兩個分別是跳轉後和跳轉前頁面路由的 $route 對象,第三個參數 next 是一個函數,當執行 next 函數後會進行跳轉,若是一個包含 next 參數的路由守衛裏沒有執行該函數,頁面會沒法跳轉,接下來咱們來解密路由守衛背後的原理

尋找跳轉先後路由的區別

圖9:

image

首先會拿到當前的頁面的 $route 對象,這個在剛剛分析過,接下來會執行 resolveQueue 函數,這個函數的做用是根據跳轉前和跳轉後 $route 對象的 matched 數組,返回這2個數組包含的路由記錄的區別

在上篇中提到, $route 對象的 matched 屬性是一個數組,經過 formatMatch 函數最終返回 $route 對象以及全部父級的路由記錄

resolveQueue 返回3個數組,updated 表明跳轉先後 matched 數組相同部分,deactivated 表明刪除部分,activated 表明新增部分,舉個例子,當咱們從 comp1Child 頁面跳轉到 comp2 頁面,這3個數組分別對應的值

圖10:

image

圖11:

image

跳轉時哪些組件觸發哪些路由守衛就是由這3個數組決定的,從這裏就能夠大體推斷出,vue-router 會在新增的組件會觸發 beforeRouteEnter 之類的進入守衛,在相同部分觸發 beforeRouteUpdate 守衛,在刪除部分觸發 beforeRouteLeave 之類的離開守衛

生成路由守衛

接下來咱們來證實上述的推斷,執行到圖9的第 9 行會聲明一個 queue 數組, vue-router 會將這些相同的不一樣的路由記錄通過一些函數的轉換,最後放到該數組中,而且經過旁邊定義的類型可以發現,數組的元素都是 NavigationGuard 類型

圖12:

image

能夠發現 NavigationGuard 就是一個標準的路由守衛的簽名,能夠推斷出,通過 queue 數組內部這些函數的轉換最終會返回路由守衛組成的數組,而這些函數就是將上節中的路由記錄轉換爲路由守衛的函數

同時數組中的守衛的排列順序也是設計好的,對應 vue-router 官方文檔中提到的路由導航解析流程

圖13:

image

咱們先分析 queue 數組裏第一個執行的函數 extractLeaveGuards,通過一層封裝最終會執行通用函數 extractGuards

圖14:

image

此時 records 參數爲刪除的路由記錄,name 爲 beforeRouteLeave,即最終觸發的是 beforeRouteLeave 守衛

圖15:

而後會執行 flatMapComponents 函數,這個函數也是一個通用函數,做用是遍歷 records 數組,每次執行第二個回調函數,相似數組的 map 方法,而對應圖14中回調函數參數解釋以下

  • def:視圖名對應的組件配置項(由於 vue-router 支持命名視圖因此可能會有多個視圖名,大部分狀況爲 default,及使用默認視圖),當是異步路由時,def爲異步返回路由的函數

  • instance:組件實例

  • match:當前遍歷到的路由記錄

  • key:視圖名

在回調函數內部會執行 extractGuard 函數

圖16:

image

def 爲組件配置項,經過 Vue 核心庫的函數 extend 將配置項轉爲組件構造器(雖然配置項中就能拿到對應的路由守衛,可是從官方註釋發現只有轉爲構造器後才能拿到一些全局混入的鉤子),在生成構造器時,Vue 會將配置項賦值給構造器的靜態屬性 options(extend 部分的解析能夠看我另外一篇博客),最後返回配置項中對應的路由守衛函數,即若是咱們在跳轉後的組件中定義了 beforeRouteLeave 的話這裏就會返回這個函數

在圖 14 中拿到返回值 guard 後會通過一層處理,例如扁平化,綁定 this 指向,根據 reverse 參數決定是否要反轉數組(由於 matched 中路由記錄順序是父 => 子,而 beforeRouteLeave 須要從最裏層子組件觸發,因此須要進行反轉保證守衛觸發順序),最後 queue 數組的元素以下

圖17:

image

值得注意的是最後一個 resolveAsyncComponents 函數,它的做用是解析異步路由

路由懶加載的原理

什麼是異步路由呢,通俗來講就是使用路由懶加載返回的路由,咱們可使用 import () 這種語法去動態的加載 JS 文件,放到 vue-router 中,就能夠實現異步加載組件配置項(這裏只討論開發中使用較多的 import() 語法)

圖18:

image

咱們進入函數內部一探究竟

圖19:

image

resolveAsyncComponents 函數最終會返回一個函數,而且符合路由守衛的函數簽名(這裏 vue-router 可能只是爲了保證返回函數的一致性,實質上在這個函數中,並不會用到 to,from 這2個參數)

這個函數只是被定義了,並無執行,可是咱們能夠經過函數體觀察它是如何加載異步路由的。一樣經過 flatMapComponents 遍歷新增的路由記錄,每次遍歷都執行第二個回調函數

在回調函數裏,會定義一個 resolve 函數,當異步組件加載完成後,會經過 then 的形式解析 promise,最終會調用 resolve 函數並傳入異步組件的配置項做爲參數, resolve 函數接收到組件配置項後會像 Vue 中同樣將配置項轉爲構造器 ,同時將值賦值給當前路由記錄的 componts 屬性中(key 屬性默認爲 default)

另外 resolveAsyncComponents 函數會經過閉包保存一個 pending 變量,表明接收的異步組件數量,在 flatMapComponents 遍歷的過程當中,每次會將 pending 加一,而當異步組件被解析完畢後再將 pending 減一,也就是說,當 pengding 爲 0 時,表明異步組件所有解析完成, 隨即執行 next 方法,next 方法是 vue-router 控制整個路由導航順序的核心方法

執行路由守衛

在分析 next 方法以前,咱們先來看一下 vue-router 是如何處理 queue 數組中的元素的,在上文中,雖然定義了 queue 數組,其中包括了路由守衛以及解析異步組件的函數,可是尚未執行

走到圖 9 的 24 行,定義了一個 iterator 函數,顧名思義它是一個迭代器,最後將 queue 和這個迭代器放入 runQueue 函數執行,由此能夠發現這個 runQueue 是一個用來遍歷 queue 數組的函數,看到這裏有些朋友會有疑問,爲啥 vue-router 還要額外的定義一個 runQueue 函數,直接一個 forEach 不就行了嗎

接下來咱們進入函數內部一探究竟

遍歷 queue 數組

圖20:

image

runQueue 內部聲明瞭一個 step 的函數,它一個是控制 runQueue 是否繼續遍歷的函數,當咱們第一次執行時,給 step 函數傳入參數 0 表示開始遍歷 queue 第 1 個元素,經過 step 函數內部能夠發現,它最終會執行參數 fn,也就是 iterator 這個迭代器函數,給它傳入當前遍歷的 queue 元素以及一個回調函數,這個回調函數裏保存着遍歷下個元素的邏輯,也就是說runQueue 將是否須要繼續遍歷的控制權傳入了 iterator 函數中

這裏先拋出結論

runQueue 函數只負責遍歷數組,並不會執行邏輯,它依次遍歷 queue 數組的元素,每次遍歷時會將當前元素交給外部定義的 iterator 迭代器去執行,而 iterator 迭代器一旦處理完當前元素就讓 runQueue 遍歷下個元素,且當數組所有遍歷結束時,會執行做爲回調的參數 cb

runQueue 和普通的 forEach 遍歷數組不一樣點在於,forEach 是同步的,而 vue-router 中可能會存在異步路由,因此須要設計一個支持異步的遍歷函數,只有當 iterator 函數執行完一次且通知 runQueue 纔會接着遍歷下一個元素

接着咱們來看一下 runQueue 將元素交給迭代器執行時發生了什麼

迭代器

對應圖 9 中24-41行代碼:

image

其中迭代器的參數 next 即 runQueue 中的 step 函數

咱們知道,當在路由守衛中若是沒有執行 next 函數,路由將沒法跳轉,緣由是由於沒有去執行 hook 的第三個回調函數,也就不會執行 iterator 的第三個參數 next,最終致使不會通知 runQueue 繼續往下遍歷

另外當咱們給 next 函數傳入另外一個路徑時,會取消原來的導航,取而代之跳轉到指定的路徑,緣由是由於知足上圖的 true 邏輯,執行 abort 函數取消導航,隨後會調用 push/replace 將路由從新跳轉到指定的頁面

最後回到以前異步路由中提到的那個 next 函數,當全部的異步路由都被解析完成後,纔會執行 next 函數繼續遍歷 queue 數組的下個元素,一旦有某個路由沒有被解析完成,vue-router 就會一直等待直到接受到爲止,而後纔會去觸發以後的邏輯

遍歷成功後的回調

當 queue 最後一個元素也就是異步組件被解析完成後,runQueue 會執行傳入的第三個參數,即執行遍歷成功回調

對應圖 9 中的 44-64 行:

image

能夠看到成功回調裏 vue-router 又往 queue 中添加了路由守衛,同時會開啓第二輪遍歷......

image

關於第二輪的 queue 數組遍歷礙於篇幅我會放到下篇來講

總結

  • 當 vue 的根實例被實例化時,會執行 vue-router 的初始化邏輯,和 vue-router 的實例化不一樣的是,它的初始化在實例化以後,做用是創建 vue-router 和 Vue 組件之間的關係

  • 當初始化時會進行第一次路由跳轉,根據跳轉路徑生成 loaction 對象,再經過 location 對象生成 $route

  • $route 對象的 matched 屬性保存了當前和全部父級的路由記錄,在路由跳轉時會根據跳轉先後 $route 對象的這2個 matched 屬性,區分出相同和不一樣的路由記錄,來決定哪些組件觸發哪些路由守衛

  • vue-router 經過回調的形式異步的執行路由守衛,當前一個解析完畢後會調用回調繼續執行下個守衛

  • 只有懶加載的路由都加載完成後,纔會執行上述的回調,繼續執行下個守衛,不然會一直等待

參考資料

Vue.js 技術揭祕

相關文章
相關標籤/搜索