在上篇中主要敘述了 vue-router 的註冊和實例化過程,以及如何生成 $router, $route 對象html
在本篇中將會講述:vue
$route 對象生成的時機html5
路由守衛的原理git
路由懶加載的原理github
文中的源碼截圖只保留核心邏輯 完整源碼地址vue-router
有興趣的朋友也能夠看我學習源碼時的詳細註釋源碼地址數組
vue-router 版本:3.0.2
promise
在上篇中解釋了在調用 new Router 生成 vue-router 實例時,會經過 createMatcher
給實例建立一個 matcher 對象,matcher 對象同時含有 match
和 addRoutes
兩個方法瀏覽器
圖1:閉包
另外上篇中還講了,在建立完 vue-router 實例後,調用 Vue.use(Router) 會混入2個全局鉤子 beforeCreate 和 destroyed
圖2:
此時圖中第7行的 init
方法會初始化整個 vue-router ,而實例化和初始化 vue-router 是有區別的,實例化指的是經過 new Router 生成 vue-router 實例,初始化能夠理解爲進行全局第一次的路由跳轉時,讓 vue-router 實例和組件創建聯繫,使得路由可以接管組件
接下來咱們來看 vue-router 是如何初始化的
圖3:
在上篇中我也講述了 vue-router 會根據當前使用的路由模式(hash,html5,abstract)來生成 history 屬性,接着 init
方法會根據 history 屬性來執行不一樣的邏輯,可是能夠發現,不論是使用 hash 路由仍是 html5 的路由,都會執行 transitionTo
這個方法,它是整個路由跳轉的核心方法
圖4:(刪除了取消路由導航的邏輯)
能夠發現圖4的第5行代碼執行了 vue-router 實例的 match 方法,它最終會執行上篇咱們分析過的 matcher 屬性的 match
方法,而且傳入了2個參數
location:經過圖3中的 getCurrentLocation
方法,最終會生成一個跳轉目標的 loaction 對象(經過 push / replace 方法跳轉),或一個跳轉目標的路徑(經過瀏覽器 url 跳轉)
current:當前頁面的路由 $route 對象
圖5(示例):
圖6:
繼續沿用圖5 的示例,當咱們直接在瀏覽器的 url 中輸入http://localhost:8080/#/comp1/comp1Child
時,能夠觀察到 location 參數爲跳轉目標的路徑,而且此時是全局第一次調用 transitionTo
方法,vue-router 默認第一次跳轉的 current 參數爲根路徑的 $route 對象,而之後的跳轉,current 會變成當前路由的 $route 對象
圖7(第一次 history.current 值爲根路徑轉換而來的 $route 對象):
分析過 match
方法的2個參數後,接着會執行上篇中分析過的 match
方法
(在建立 $router 的 match
方法中,其實 current 參數通常不多用到,主要圍繞 location 參數再結合3個路由映射表生成 $route 對象)
圖8(執行圖4的 vue-router 實例的 match 方法最後會執行到上篇分析的 match
方法):
此時,這個最終執行的這個 match
方法就會建立出一個 $route 對象,並賦值給圖4的 route 屬性,隨後會進入 confirmTransition
這個方法,它負責控制全部的路由守衛的執行,咱們來看一下它的內部是如何運行的
本小結會介紹 vue-router 一個比較重要的部分:路由守衛
和組件的生命週期的鉤子不一樣,路由守衛將重點放在路由上,可以控制路由跳轉,通常用在頁面級別的路由跳轉時控制跳轉的邏輯,好比在路由守衛中檢查用戶是否有進入當前頁面的權限,沒有則跳轉到受權頁面,亦或是在離開頁面時警告用戶有未確認的信息,確認後才能跳轉等等
在路由守衛中,通常會接收3個參數,to,from,next,前兩個分別是跳轉後和跳轉前頁面路由的 $route 對象,第三個參數 next 是一個函數,當執行 next 函數後會進行跳轉,若是一個包含 next 參數的路由守衛裏沒有執行該函數,頁面會沒法跳轉,接下來咱們來解密路由守衛背後的原理
圖9:
首先會拿到當前的頁面的 $route 對象,這個在剛剛分析過,接下來會執行 resolveQueue
函數,這個函數的做用是根據跳轉前和跳轉後 $route 對象的 matched 數組,返回這2個數組包含的路由記錄的區別
在上篇中提到, $route 對象的 matched 屬性是一個數組,經過 formatMatch
函數最終返回 $route 對象以及全部父級的路由記錄
resolveQueue
返回3個數組,updated 表明跳轉先後 matched 數組相同部分,deactivated 表明刪除部分,activated 表明新增部分,舉個例子,當咱們從 comp1Child 頁面跳轉到 comp2 頁面,這3個數組分別對應的值
圖10:
圖11:
跳轉時哪些組件觸發哪些路由守衛就是由這3個數組決定的,從這裏就能夠大體推斷出,vue-router 會在新增的組件會觸發 beforeRouteEnter 之類的進入守衛,在相同部分觸發 beforeRouteUpdate 守衛,在刪除部分觸發 beforeRouteLeave 之類的離開守衛
接下來咱們來證實上述的推斷,執行到圖9的第 9 行會聲明一個 queue 數組, vue-router 會將這些相同的不一樣的路由記錄通過一些函數的轉換,最後放到該數組中,而且經過旁邊定義的類型可以發現,數組的元素都是 NavigationGuard 類型
圖12:
能夠發現 NavigationGuard 就是一個標準的路由守衛的簽名,能夠推斷出,通過 queue 數組內部這些函數的轉換最終會返回路由守衛組成的數組,而這些函數就是將上節中的路由記錄轉換爲路由守衛的函數
同時數組中的守衛的排列順序也是設計好的,對應 vue-router 官方文檔中提到的路由導航解析流程
圖13:
咱們先分析 queue 數組裏第一個執行的函數 extractLeaveGuards
,通過一層封裝最終會執行通用函數 extractGuards
圖14:
此時 records 參數爲刪除的路由記錄,name 爲 beforeRouteLeave,即最終觸發的是 beforeRouteLeave 守衛
圖15:
而後會執行 flatMapComponents
函數,這個函數也是一個通用函數,做用是遍歷 records 數組,每次執行第二個回調函數,相似數組的 map 方法,而對應圖14中回調函數參數解釋以下
def:視圖名對應的組件配置項(由於 vue-router 支持命名視圖因此可能會有多個視圖名,大部分狀況爲 default,及使用默認視圖),當是異步路由時,def爲異步返回路由的函數
instance:組件實例
match:當前遍歷到的路由記錄
key:視圖名
在回調函數內部會執行 extractGuard
函數
圖16:
def 爲組件配置項,經過 Vue 核心庫的函數 extend 將配置項轉爲組件構造器(雖然配置項中就能拿到對應的路由守衛,可是從官方註釋發現只有轉爲構造器後才能拿到一些全局混入的鉤子),在生成構造器時,Vue 會將配置項賦值給構造器的靜態屬性 options(extend 部分的解析能夠看我另外一篇博客),最後返回配置項中對應的路由守衛函數,即若是咱們在跳轉後的組件中定義了 beforeRouteLeave 的話這裏就會返回這個函數
在圖 14 中拿到返回值 guard 後會通過一層處理,例如扁平化,綁定 this 指向,根據 reverse 參數決定是否要反轉數組(由於 matched 中路由記錄順序是父 => 子,而 beforeRouteLeave 須要從最裏層子組件觸發,因此須要進行反轉保證守衛觸發順序),最後 queue 數組的元素以下
圖17:
值得注意的是最後一個 resolveAsyncComponents
函數,它的做用是解析異步路由
什麼是異步路由呢,通俗來講就是使用路由懶加載返回的路由,咱們可使用 import ()
這種語法去動態的加載 JS 文件,放到 vue-router 中,就能夠實現異步加載組件配置項(這裏只討論開發中使用較多的 import()
語法)
圖18:
咱們進入函數內部一探究竟
圖19:
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 不就行了嗎
接下來咱們進入函數內部一探究竟
圖20:
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行代碼:
其中迭代器的參數 next 即 runQueue
中的 step
函數
咱們知道,當在路由守衛中若是沒有執行 next
函數,路由將沒法跳轉,緣由是由於沒有去執行 hook
的第三個回調函數,也就不會執行 iterator
的第三個參數 next
,最終致使不會通知 runQueue
繼續往下遍歷
另外當咱們給 next
函數傳入另外一個路徑時,會取消原來的導航,取而代之跳轉到指定的路徑,緣由是由於知足上圖的 true 邏輯,執行 abort
函數取消導航,隨後會調用 push/replace 將路由從新跳轉到指定的頁面
最後回到以前異步路由中提到的那個 next
函數,當全部的異步路由都被解析完成後,纔會執行 next
函數繼續遍歷 queue 數組的下個元素,一旦有某個路由沒有被解析完成,vue-router 就會一直等待直到接受到爲止,而後纔會去觸發以後的邏輯
當 queue 最後一個元素也就是異步組件被解析完成後,runQueue
會執行傳入的第三個參數,即執行遍歷成功回調
對應圖 9 中的 44-64 行:
能夠看到成功回調裏 vue-router 又往 queue 中添加了路由守衛,同時會開啓第二輪遍歷......
關於第二輪的 queue 數組遍歷礙於篇幅我會放到下篇來講
當 vue 的根實例被實例化時,會執行 vue-router 的初始化邏輯,和 vue-router 的實例化不一樣的是,它的初始化在實例化以後,做用是創建 vue-router 和 Vue 組件之間的關係
當初始化時會進行第一次路由跳轉,根據跳轉路徑生成 loaction 對象,再經過 location 對象生成 $route
$route 對象的 matched 屬性保存了當前和全部父級的路由記錄,在路由跳轉時會根據跳轉先後 $route 對象的這2個 matched 屬性,區分出相同和不一樣的路由記錄,來決定哪些組件觸發哪些路由守衛
vue-router 經過回調的形式異步的執行路由守衛,當前一個解析完畢後會調用回調繼續執行下個守衛
只有懶加載的路由都加載完成後,纔會執行上述的回調,繼續執行下個守衛,不然會一直等待