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

image

前言

Vue 是一個漸進式的框架,這意味着你能夠只使用 Vue 的核心庫來開發,可是當你在開發一個完整的業務項目時,路由是一個必不可少的部分javascript

在曾經的前端領域中,一直都使用的是服務端渲染的模式,即用戶輸入 url 後,瀏覽器向服務器請求這個 url 對應的HTML,服務器返回 HTML給前端,前端再展現,而後當須要瀏覽別的頁面時,須要點擊 a 標籤再向服務器發送一個請求,服務器就會再發給你目標頁面的 HTML前端

這樣會暴露一些缺點:vue

  • 每次跳轉都向服務器請求,會增長服務器的壓力java

  • 每次跳轉都會刷新頁面致使跳轉過程當中會有一瞬間的白屏,用戶體驗不是很是好nginx

  • 因爲是服務端渲染,受到 XSS 的攻擊可能性也較高git

在 MVVM 框架興起的同時,愈來愈多的開發者傾向於使用前端渲染的模式,服務端返回固定 JS 文件給前端,瀏覽器執行 JS 文件再渲染出整個頁面,而在路由方面,前端會維護一個路由的層級表,當輸入 url 後,再也不向後端請求 HTML,而是去這個層級表中找到對應頁面的 JS 文件並執行,從而渲染出新的頁面,整個過程是純前端控制的,因此也被稱爲前端路由github

而 vue-router 做爲 Vue 的路由庫,它是怎麼實現路由地址和組件之間的轉換的呢,這篇文章中,我將會帶你們深刻 vue- router 的源碼,解密 vue-router API 背後的原理vue-router

文中的源碼截圖只保留核心邏輯 完整源碼地址後端

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

須要瞭解一些 Vue 的公共函數(mixins,install,defineReactive)

vue-router 版本:3.0.2

vue-router的使用方法

咱們從 vue-router 的使用方法提及,當使用 vue-router 時,通常會分爲3步

  1. 引入 vue-router,調用 Vue.use(Router)

  2. 實例化 router 對象,傳入一個路由層級表 routes

  3. 在 main.js 中給根實例傳入 router 對象

註冊 vue-router 插件

當咱們調用 Vue.use(Router)時會執行插件的註冊流程

圖1:

image

全部的 Vue 插件都會暴露一個 install 方法,當執行 Vue.use 時,實質上 Vue 會執行插件的 install 方法

混入全局鉤子

瞭解過 Vue 響應式原理的朋友能夠發現,vue-router 會經過 Vue.mixin 的方法全局混入 beforeCreate,destroyed 2個鉤子,由於是全局混入的,因此以後全部的根實例和組件實例都會有這2個生命週期鉤子

當根實例被實例化時,混入的 beforeCreate 第一次被執行,由於咱們在 new Vue 時傳入了 router 對象,它會被 Vue 做爲 $options 的屬性,因此會執行到 true 的邏輯,這裏的核心在於 init 方法,它會初始化整個 vue-router 咱們以後詳解,另外將一個 _route 對象變成一個響應式對象,這個和本章邏輯無關,我會放到之後章節討論

除開根實例,其他全部的組件實例都會執行 false 的邏輯,它會給組件實例定義一個 _routerRoot 屬性,由於 Vue 生成組件時是從上到下的,因此全部組件實例的 _routerRoot 屬性都指向根實例

以後執行 registerInstance 這個也放到後面討論

定義 $router,$route 屬性

隨後 Vue 在原型上定義了 $router,$route 2個對象,攔截 get 方法指向 _routerRoot.router,從上面一節能夠發現,實質上指向的就是根實例的 router 對象,即平常開發中調用的 this.$router 最終都會指向根實例上的 router 對象,至於 $route 咱們放到後面來講

定義全局組件

最後經過 Vue.component 方法註冊了2個全局組件,這樣咱們能夠在任何地方直接使用<router-view>和<router-link>組件

實例化 vue-router

一般使用 vue-router 時,會在 router.js 中經過 new Router 的形式生成一個 router 的實例,並傳入一個路由的層級表 routes 數組

圖2:

在 new Router 時會傳入一個對象,對象含有一個 routes 屬性,值是一個數組 ,咱們稱這個數組的每一個元素叫作一個路由配置項對象(源碼中的類型名叫 RouteConfig),仔細觀察能夠發現它是一個樹形的數據結構,即含有一個 children 數組,數組的元素也是一個路由配置項對象

瞭解路由配置項對象後,咱們找到源碼中對應的 VueRouter 類

圖3:

image

整個 vue-router 實例化的過程核心作了兩件事

  1. 定義 matcher 屬性:經過 createMatcher 建立了一個對象賦值給 matcher

  2. 定義 history 屬性:根據傳入的 mode 屬性實例化不一樣的 history 路由實例

建立路由的映射表

先來看 matcher 屬性,圖中第四行會執行到 createMatcher 方法,返回一個 matcher 對象,包含 matchaddRoutes 這 2 個方法,這 2 個方法是 vue-router 中比較重要的方法,以後咱們會分析它們的做用

在這以前先看一下 createMatcher 函數執行時觸發的 createRouteMap 函數

圖4:

image

createRouteMap 這個函數就是用來建立路由的映射表的,它是一個記錄全部信息(路由記錄)的對象,將參數 routes 數組(即上面提到的路由配置項數組)進行一系列處理,生成 pathList,pathMap,nameMap 3張路由映射表

圖5:

image

createRouteMap 內部會遍歷 routes 數組,執行 addRouteRecord 方法,將每一個路由配置項對象轉爲路由記錄(RouteConfig類型 -> RouteRecord類型)

接着將獲得的路由記錄分別再作一些轉換,而後儲存在 pathList,pathMap,nameMap 3張路由映射表中,咱們先仔細分析 addRouteRecord 方法,再分析這三個映射表的區別

圖6:

image

經過源代碼發現,路由記錄基於路由配置項對象擴展了一些額外屬性,如下是對應的介紹

  • path:路由的完整路徑

  • regex:匹配到當前 route 對象的正則

  • components:route 對象的組件(由於 vue-router 中有命名視圖,因此會默認放在 default 屬性下,instances 同理)

  • instances: route 對象對應的 vm 實例

  • name:route 對象的名字

  • parent:route 對象的父級路由記錄

  • matchAs:路由別名

  • redirect:路由重定向

  • beforeEnter:組件級別的路由鉤子

  • meta:路由元信息

  • props:路由跳轉時的傳參

在建立路由記錄前,會使用 normalizedPath 規範化 route 對象的路徑,若是傳入的 route 對象含有父級 route 對象,會將父級 route 對象的 path 拼上當前的 path

圖7:

image

例如圖2中的 comp1Child 這個 route 對象,它的 path 最終會變成

"/comp1" + "/" + "comp1Child" => "/comp1/com1Child"

而最終會生成的路由記錄是這樣的

圖8:

隨後由於 routes 不只是一個數組,也是一個樹形結構,因此須要進行遞歸的遍歷,而後將路由 對象放入這3個路由映射表中,而這3個路由映射表的區別在於

  • pathList:數組,保存了 route 對象的路徑

  • pathMap:對象,保存了全部 route 對象對應的 record 對象

  • nameMap:對象,保存了全部含有name屬性的 route 對象對應的 record 對象

圖2中的路由對應的3張路由映射表以下:

pathList:

pathMap:

nameMap:

能夠看到 pathMap 和 nameMap 幾乎是同樣的,由於圖2中的路由都有 name 屬性,假設某個路由沒有 name 屬性,則只會在 pathMap 中存在

對比保存了路由配置項對象的 routes 數組和這3個路由映射表,咱們能夠發現:routes 數組是一個樹形結構,而路由映射表是一個扁平的一維結構,經過路由映射表裏的 parent 屬性來維護父子關係,只因此沒有直接在 routes 數組中擴展是由於一維數組可以更方便的找到對應數據,反觀樹形結構只能遞歸查找,性能堪憂

動態添加路由的 addRoutes 函數

根據 routes 生成三個路由映射表後,會向外暴露一個動態添加路由的 API addRoutes

圖10:

這個 api 平常開發也遇到過,用於動態註冊路由,它的原理其實很簡單,就是接受一個 routes 數組,再次調用 createRouteMap 將數組每一個元素轉換成路由記錄 (RouteRecord) ,而後合併到以前生成的路由映射表中

返回 $route 對象的 match 函數

createMatcher 返回的第二個函數是 matchmatch 函數用於建立 $route 對象

圖11:

image

以前說的 route 是針對 new Router 時傳入的 routes 數組的每一個元素,也就是路由配置項對象,而 $route 是最終返回做爲 Vue.prototype.$route 的對象,在類型定義中,route 的類型是 RouteConfig,而 $route 的類型是 Route,具體接口的定義能夠查看源代碼,雖然在源碼中二者變量名都是 route,但我下文會使用 $route 來區分最終返回的 route 對象

圖12:

route(路由配置項) :

$route(當前頁面的路由對象) :

前者表示的是路由的一些基礎配置項,然後者是真正通過 vue-router 處理後表示當前頁面的路由對象

每次路由跳轉的時候都會執行這個 match 函數從新生成一個 $route 對象,具體何時會觸發 match 放到下篇中講,這章先分析 match 函數是如何最終生成一個真正的 $route 對象的

生成 loaction 對象

match函數首先會執行 normalizeLocation 函數,它是一個輔助函數,會將調用 router.push / router.replace 時跳轉的路由地址轉爲一個 location 對象

this.$router.push("/login") // 路由地址即 "/login",還多是一個對象 {path: "/login"}
複製代碼

那什麼是 location 對象? MDN 上是這麼解釋的

Location接口表示其連接到的對象的位置(URL)。所作的修改反映在與之相關的對象上。 DocumentWindow 接口都有這樣一個連接的Location,分別經過 Document.locationWindow.location 訪問。

通俗的來講就是用一個對象來描述當前 url 的一些信息。當咱們在地址欄中輸入 www.baidu.com ,按 F12 打開控制檯,輸入 loaction 就能展現出當前地址的一些信息

圖13:

image

vue-router 在 location 接口的基礎上作了一些加強,添加了 name,path,hash 等 vue-router 特有的屬性

舉個例子,當調用 router.push({name:"comp1"}) 使用 name 的形式進行路由跳轉時,返回的 loaction 對象就會有一個 name 屬性,當 name 存在時,會走到圖11中的 true 邏輯,從以前 createMatcher 生成的 nameMap 路由映射表中找到對應 name 的路由記錄對象,最終會執行 _createRoute 這個方法

而調用 router.push("/comp1") 使用路徑的形式進行路由跳轉,一樣也會返回一個 location 對象,但不會有 name 屬性,走圖11的 false 邏輯,從 pathMap 和 pathList 中找到對應的路由記錄對象,最終也會執行 _createRoute 這個方法

可見不管使用 name 跳轉仍是使用 path 跳轉,最終都會執行 _createRoute ,帶下劃線的 _createRoute 是一個私有方法,它最終會調用 createRoute 生成 $route 對象

生成 $route 對象

圖14:

image

通過對一些 query 參數的處理,最終返回 $route 對象,其中有一個 matched 屬性值得注意,它經過 formatMatch 函數生成,有朋友打印過 $route 返回值的話應該知道,matched 是一個數組,每一個元素都是一個路由記錄

圖15:

image

還記得以前在生成路由記錄的時定義的 parent 屬性嗎?它的其中一個用途就是經過不斷的向上查找父級的路由記錄,放入 matched 數組中,最終返回一個保存了當前路由記錄和全部父級數組,順序是 父 => 子

圖16 $route 對象:

而這個 matched 數組最終會決定觸發哪些路由組件的哪些路由守衛鉤子,關於路由鉤子部分咱們放到下篇來講

路由配置項,路由記錄,$route 三者區別

路由配置項(RouteConfig),路由記錄(RouteRecord),$route(Route)的區別在於

  • 路由配置項是在 new Router 時定義的,表明每一個路由的基本結構,它是靜態的
  • 路由記錄基於路由配置項,擴展了一些額外屬性,例如當前路由實例,匹配到當前路由所須要的正則,父路由對象,它和路由配置項同樣也是靜態的
  • $route 基於跳轉時的 url( 例如調用 router.push("/login") 就基於 "/login" ),擴展了一些額外屬性,例如 matched 屬性表明當前路由匹配到了哪些路由記錄等,因此 $route 是動態的,由於跳轉的路由不肯定

流程圖

這裏畫了一張流程圖來表達實例化 vue-router 時的 matcher 屬性內部的依賴關係

生成 history 路由實例

再次回到圖3,vue-router 根據傳入參數的 mode 屬性來實例化不一樣的路由類(HTML5,hash,abstract),這也是官方提供給開發者的3種不一樣的選擇來生成路由

  • HTML5 路由是相對比較美觀的一種路由,和正常的 url 顯示沒有什麼區別,核心依靠 pushStatereplaceState 來實現不向後端發送請求的路由跳轉,可是這相似一層「假裝」,當用戶點擊刷新按鈕時就會暴露,最後仍是會發送請求,致使找不到頁面的狀況,因此須要配合 nginx 來實現找不到頁面時返回主頁的操做

  • hash 路由是默認使用的路由,在 url 中會存在一個 # 號,核心依靠這個 # 號也就是曾經做爲路由的錨點來實現不向後端發送請求的路由跳轉

  • abstract 路由是一種抽象路由,通常用在非瀏覽器端,維護一種抽象的路由結構,使得可以嫁接在客戶端或者服務端等沒有 history 路由的地方

vue-router 會以後根據 history 的類型,採起不一樣的方式切換路由和監聽路由變化的方式

總結

  • 當調用 Vue.use(Router) 時,會給全局的 beforeCreate,destroyed 混入2個鉤子,使得在組件初始化時可以經過 this.$router / this.$route 訪問到根實例的 router / route 對象,同時還定義了全局組件 router-view / router-link

  • 在實例化 vue-router 時,經過 createRouteMap 建立3個路由映射表,保存了全部路由的記錄,另外建立了 match 函數用來建立 $route 對象,addRoutes 函數用來動態生成路由,這2個函數都是須要依賴路由映射表生成的

  • vue-router 還給開發者提供了3種不一樣的路由模式,每一個模式下的跳轉邏輯都有所差別

vue-router 定義了 match 方法用來生成 $route 對象,而何時會調用 match 方法尚未分析過,另外文章開頭的 registerInstance 又是作什麼的,在下篇中我會分析 vue-router 中的跳轉邏輯,包括路由守衛,vue-router 的全局組件,以及組件相關的視圖更新

參考資料

Vue.js 技術揭祕

相關文章
相關標籤/搜索