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

image

前言

Vue 是一個漸進式的框架效拭,這意味著你可以只使用 Vue 的核心庫來開發(fā),但是當你在開發(fā)一個完整的業(yè)務項目時孽尽,路由是一個必不可少的部分

在曾經(jīng)的前端領(lǐng)域中田巴,一直都使用的是服務端渲染的模式,即用戶輸入 url 后锡凝,瀏覽器向服務器請求這個 url 對應的HTML粘昨,服務器返回 HTML給前端,前端再展示窜锯,然后當需要瀏覽別的頁面時张肾,需要點擊 a 標簽再向服務器發(fā)送一個請求,服務器就會再發(fā)給你目標頁面的 HTML

這樣會暴露一些缺點:

  • 每次跳轉(zhuǎn)都向服務器請求锚扎,會增加服務器的壓力

  • 每次跳轉(zhuǎn)都會刷新頁面導致跳轉(zhuǎn)過程中會有一瞬間的白屏吞瞪,用戶體驗不是非常好

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

在 MVVM 框架興起的同時驾孔,越來越多的開發(fā)者傾向于使用前端渲染的模式芍秆,服務端返回固定 JS 文件給前端,瀏覽器執(zhí)行 JS 文件再渲染出整個頁面翠勉,而在路由方面妖啥,前端會維護一個路由的層級樹,當輸入 url 后对碌,不再向后端請求 HTML荆虱,而是去這個層級樹中找到對應頁面的 JS 文件并執(zhí)行,從而渲染出新的頁面朽们,整個過程是純前端控制的克伊,所以也被稱為前端路由

而 vue-router 作為 Vue 的路由庫,它是怎么實現(xiàn)路由地址和組件之間的轉(zhuǎn)換的呢华坦,這篇文章中愿吹,我將會帶大家深入 vue- router 的源碼,解密 vue-router API 背后的原理

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

需要了解一些 Vue 的公共函數(shù)(mixins,install,defineReactive)

vue-router 版本:3.0.2

vue-router的使用方法

我們從 vue-router 的使用方法說起惜姐,當使用 vue-router 時犁跪,一般會分為3步

  1. 引入 vue-router椿息,調(diào)用 Vue.use(Router)

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

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

注冊 vue-router 插件

當我們調(diào)用 Vue.use(Router)時會執(zhí)行插件的注冊流程

圖1:

image

(刪除了部分和入口無關(guān)的邏輯)

所有的 Vue 插件都會暴露一個 install 方法坷衍,當執(zhí)行 Vue.use 時寝优,實質(zhì)上 Vue 會執(zhí)行插件的 install 方法

混入全局鉤子

了解過 Vue 響應式原理的朋友可以發(fā)現(xiàn),vue-router 會通過 Vue.mixin 的方法全局混入 beforeCreate枫耳,destroyed 2個鉤子乏矾,因為是全局混入的,所以之后所有的根實例和組件實例都會有這2個生命周期鉤子

當根實例被實例化時迁杨,混入的 beforeCreate 第一次被執(zhí)行钻心,因為我們在 new Vue 時傳入了 router 對象,它會被 Vue 作為 $options 的屬性铅协,所以會執(zhí)行到 true 的邏輯捷沸,這里的核心在于 init 方法,它會初始化整個 vue-router 我們之后詳解狐史,另外將傳入的 router 對象變成一個響應式對象痒给,這個我們也之后討論

除開根實例,其余所有的組件實例都會執(zhí)行 false 的邏輯骏全,它會給組件實例定義一個 _routerRoot 屬性苍柏,因為 Vue 生成組件時是從上到下的,所以所有組件實例的 _routerRoot 屬性都指向根實例

之后執(zhí)行 registerInstance 這個也放到后面討論

定義 $router,$route 屬性

隨后 Vue 在原型上定義了 $router姜贡,$route 2個對象试吁,攔截 get 方法指向 _routerRoot.router,從上面一章可以發(fā)現(xiàn)鲁豪,實質(zhì)上指向的就是根實例的 router 對象潘悼,即日常開發(fā)中調(diào)用的 this.$router 最終都會指向根實例上的 router 對象

定義全局組件

最后通過 Vue.component 方法注冊了2個全局組件律秃,這樣我們可以在任何地方直接使用<router-view>和<router-link>組件

實例化 vue-router

通常使用 vue-router 時爬橡,會在 router.js 中通過 new Router 的形式生成一個 router 的實例,并傳入一個路由的層級表 routes 數(shù)組

圖2:

隨后我們找到源碼中的 vue-router 類

圖3:

image

整個 vue-router 實例化的過程核心就做了2件事

  1. 創(chuàng)建路由的映射表

  2. 根據(jù)傳入的 mode 屬性實例化不同的 history 路由實例

創(chuàng)建路由的映射表

圖中第四行會執(zhí)行到 createMatcher 方法棒动,返回一個對象糙申,包含 matchaddRoutes 這2個方法,這2個方法是 vue-router 中比較重要的函數(shù)船惨,之后我們會分析它們的作用柜裸,在這之前先看一下 createMatcher 函數(shù)中的 createRouteMap 函數(shù)

圖4:

image

createRouteMap 這個函數(shù)就是用來創(chuàng)建路由的映射表的,它是一個記錄所有信息(路由記錄)的對象粱锐,將傳入的 routes 數(shù)組進行一系列處理疙挺,生成 pathList,pathMap怜浅,nameMap 3張路由映射表

圖5:

image

createRouteMap 內(nèi)部會遍歷 routes 數(shù)組铐然,執(zhí)行 addRouteRecord 方法來為**每一個數(shù)組的每個元素(route 對象)創(chuàng)建記錄蔬崩,并儲存在這3個路由映射表中

圖6:

image

addRouteRecord 會將每個 route 對象轉(zhuǎn)換為一個路由記錄并保存在之前聲明的3個路由映射表中,通過源代碼發(fā)現(xiàn)搀暑,路由記錄(record 對象)非常詳細的記錄了 route 對象的很多屬性

  • path:路由的完整路徑

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

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

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

  • name:route 對象的名字

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

  • matchAs:路由別名

  • redirect:路由重定向

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

  • meta:路由元信息

  • props:路由跳轉(zhuǎn)時的傳參

在創(chuàng)建路由記錄前自点,會使用 normalizedPath 規(guī)范化 route 對象的路徑桐罕,如果傳入的 route 對象含有父級 route 對象,會將父級 route 對象的 path 拼上當前的 path

圖7:

image

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

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

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

圖8:

隨后因為 route 可能含有 children 屬性功炮,即含有子的 route 對象組成的數(shù)組,所以需要進行遞歸的遍歷埠啃,然后將 record 對象放入這3個路由映射表中死宣,而這3個路由映射表的區(qū)別在于

  • pathList:數(shù)組,保存了 route 對象的路徑

  • pathMap:對象碴开,保存了所有 route 對象對應的 record 對象

  • nameMap:對象毅该,保存了所有含有name屬性的 route 對象對應的 record 對象

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

pathList:

pathMap:

nameMap:

可以看到 pathMap 和 nameMap 是一樣的,因為圖2中的路由都有 name 屬性潦牛,如果某個路由沒有 name 屬性眶掌,則只會在 pathMap 中存在

對比保存了所有 route 對象的 routes 數(shù)組和這3個路由映射表,我們可以發(fā)現(xiàn):routes 對象是一個遞歸的樹形結(jié)構(gòu)巴碗,而路由映射表是一個扁平的一維結(jié)構(gòu)朴爬,通過路由映射表里的 parent 屬性來維護父子關(guān)系

動態(tài)添加路由的 addRoutes 函數(shù)

在創(chuàng)建完路由映射表后,會向外暴露一個動態(tài)添加路由的 API addRoutes

圖10:

image

它的原理其實很簡單橡淆,就是接受一個 route 對象召噩,并且把它轉(zhuǎn)換成 record 對象,然后合并到之前生成的路由映射表中逸爵,所以我們可以在外部調(diào)用 router.addRoutes 動態(tài)注冊路由

返回 $route 對象的 match 函數(shù)

createMatcher 返回的第二個函數(shù)是 match具滴,match 函數(shù)會返回一個 route 對象

圖11:

image

之前說的 route 是針對 new Router 時傳入的 routes 數(shù)組的每個元素,而 $route 是最終返回作為 Vue.prototype.$route 使用的對象师倔,在 flow 語言中构韵,route 的類型是 RouteConfig,而 $route 的類型是 Route趋艘,具體接口的定義可以查看源代碼疲恢,雖然在源碼中兩者變量名都是 route,但我下文會使用 $route 來區(qū)分通過 this.$route 返回 route 對象

圖12:

routes :

$route :

前者表示的是路由的一些基礎(chǔ)配置項瓷胧,而后者是真正經(jīng)過 vue-router 處理后表示當前路由的對象

每次路由跳轉(zhuǎn)的時候都會執(zhí)行這個 match 函數(shù)生成一個 $route 對象显拳,具體什么時候會觸發(fā) match 放到下篇中講,這章先分析 match 函數(shù)是如何最終生成一個真正的 $route 對象的

生成 loaction 對象

match函數(shù)首先會執(zhí)行 normalizeLocation 函數(shù)搓萧,它是一個輔助函數(shù)杂数,會將調(diào)用 router.push / router.replace 時跳轉(zhuǎn)的路由地址轉(zhuǎn)為一個 location 對象

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

Location接口表示其鏈接到的對象的位置(URL)遇八。所做的修改反映在與之相關(guān)的對象上。 DocumentWindow 接口都有這樣一個鏈接的Location耍休,分別通過 Document.locationWindow.location 訪問刃永。

通俗的來說就是用一個對象來描述當前 url 的一些信息。當我們在地址欄中輸入 www.baidu.com 羊精,按 F12 打開控制臺斯够,輸入 loaction 就能展示出當前地址的一些信息

圖13:

image

vue-router 在 location 接口的基礎(chǔ)上做了一些增強,添加了 name,path,hash 等 vue-router 特有的屬性

舉個例子喧锦,當調(diào)用 router.push({name:"comp1"}) 使用 name 的形式進行路由跳轉(zhuǎn)時读规,返回的 loaction 對象就會有一個 name 屬性,當 name 存在時燃少,會走到圖11中的 true 邏輯束亏,從之前 createMatcher 生成的 nameMap 路由映射表中找到對應 name 的路由記錄 record 對象,最終會執(zhí)行 _createRoute 這個方法

而調(diào)用 router.push("/comp1") 使用路徑的形式進行路由跳轉(zhuǎn)阵具,同樣也會返回一個 location 對象碍遍,但不會有 name 屬性,走圖11的 false 邏輯阳液,從另外2個路由映射表 pathMap怕敬,pathList 中找到對應的路由記錄,最終也會執(zhí)行 _createRoute 這個方法

可見無論使用 name 跳轉(zhuǎn)還是使用 path 跳轉(zhuǎn)帘皿,最終都會執(zhí)行 _createRoute 东跪,帶下劃線的 _createRoute 是一個私有方法,它最終會調(diào)用 createRoute 生成 $route 對象

生成 $route 對象

圖14:
image

經(jīng)過對一些 query 參數(shù)的處理鹰溜,最終返回 $route 對象虽填,其中有一個 matched 屬性值得注意,它通過 formatMatch 函數(shù)生成曹动,查看過 this.$route 返回值的朋友應該知道斋日,matched 是一個數(shù)組,每個元素都是一個路由記錄(record)

圖15:

image

還記得之前在生成路由記錄的時定義的 parent 屬性嗎仁期?它的其中一個用途就是通過不斷的向上查找父級的路由記錄桑驱,放入 matched 數(shù)組中竭恬,最終返回一個保存了當前路由記錄和所有父級數(shù)組跛蛋,順序是 父 => 子

圖16:

而這個 matched 數(shù)組最終會決定觸發(fā)哪些路由組件的哪些路由守衛(wèi)鉤子,關(guān)于路由鉤子部分我們放到下篇來說

生成 history 路由實例

再次回到圖3痊硕,vue-router 根據(jù)傳入?yún)?shù)的 mode 屬性來實例化不同的路由類(HTML5赊级,hash,abstract)岔绸,這也是官方提供給開發(fā)者的3種不同的選擇來生成路由

  • HTML5 路由是相對比較美觀的一種路由理逊,和正常的 url 顯示沒有什么區(qū)別橡伞,核心依靠 pushStatereplaceState 來實現(xiàn)不向后端發(fā)送請求的路由跳轉(zhuǎn),但是當用戶點擊刷新按鈕時會存在找不到頁面的情況晋被,需要配合 nginx 來做一層轉(zhuǎn)發(fā)

  • hash 路由是默認使用的路由兑徘,在 url 中會存在一個 # 號,核心依靠這個 # 號也就是曾經(jīng)作為路由的錨點來實現(xiàn)不向后端發(fā)送請求的路由跳轉(zhuǎn)

  • abstract 路由是一種抽象路由羡洛,一般用在非瀏覽器端挂脑,維護一種抽象的路由結(jié)構(gòu),使得能夠嫁接在客戶端或者服務端等沒有 history 路由的地方

總結(jié)

  • 當調(diào)用 Vue.use(Router) 時欲侮,會給全局的 beforeCreate崭闲,destroyed 混入2個鉤子,使得在組件初始化時能夠通過 this.$router / this.$route 訪問到根實例的 router / route 對象威蕉,同時還定義了全局組件 router-view / router-link

  • 在實例化 vue-router 時刁俭,通過 createRouteMap 創(chuàng)建3個路由映射表,保存了所有路由的記錄韧涨,另外創(chuàng)建了 match 函數(shù)用來創(chuàng)建 $route 對象牍戚,addRoutes 函數(shù)用來動態(tài)生成路由,這2個函數(shù)都是需要依賴路由映射表生成的

  • vue-router 還給開發(fā)者提供了3種不同的路由模式虑粥,每個模式下的跳轉(zhuǎn)邏輯都有所差異

vue-router 定義了 match 方法用來生成 $route 對象翘魄,而什么時候會調(diào)用 match 方法還沒有分析過,另外文章開頭的 registerInstance 又是做什么的舀奶,在下篇中我會分析 vue-router 中的跳轉(zhuǎn)邏輯暑竟,包括路由守衛(wèi),vue-router 的全局組件育勺,以及組件相關(guān)的視圖更新

參考資料

Vue.js 技術(shù)揭秘

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末但荤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涧至,更是在濱河造成了極大的恐慌腹躁,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件南蓬,死亡現(xiàn)場離奇詭異纺非,居然都是意外死亡,警方通過查閱死者的電腦和手機赘方,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門烧颖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人窄陡,你說我怎么就攤上這事炕淮。” “怎么了跳夭?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵涂圆,是天一觀的道長们镜。 經(jīng)常有香客問我,道長润歉,這世上最難降的妖魔是什么模狭? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮踩衩,結(jié)果婚禮上胞皱,老公的妹妹穿的比我還像新娘。我一直安慰自己九妈,他們只是感情好反砌,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著萌朱,像睡著了一般宴树。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晶疼,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天酒贬,我揣著相機與錄音,去河邊找鬼翠霍。 笑死锭吨,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的寒匙。 我是一名探鬼主播零如,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锄弱!你這毒婦竟也來了考蕾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤会宪,失蹤者是張志新(化名)和其女友劉穎肖卧,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掸鹅,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡塞帐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了巍沙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葵姥。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赎瞎,靈堂內(nèi)的尸體忽然破棺而出牌里,到底是詐尸還是另有隱情颊咬,我是刑警寧澤务甥,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布牡辽,位于F島的核電站,受9級特大地震影響敞临,放射性物質(zhì)發(fā)生泄漏态辛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一挺尿、第九天 我趴在偏房一處隱蔽的房頂上張望奏黑。 院中可真熱鬧,春花似錦编矾、人聲如沸熟史。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹂匹。三九已至,卻和暖如春凹蜈,著一層夾襖步出監(jiān)牢的瞬間限寞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工仰坦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留履植,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓悄晃,卻偏偏與公主長得像玫霎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妈橄,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355