前言
在上篇中主要敘述了 vue-router 的注冊(cè)和實(shí)例化過(guò)程躯嫉,以及如何生成 $router, $route 對(duì)象
在本篇中會(huì)講述:
$route 對(duì)象生成的時(shí)機(jī)
路由守衛(wèi)的原理
路由懶加載的原理
文中的源碼截圖只保留核心邏輯 完整源碼地址
vue-router 版本:3.0.2
$route 對(duì)象生成的時(shí)機(jī)
在上篇中解釋了在調(diào)用 new Router 生成 vue-router 實(shí)例時(shí),實(shí)例會(huì)包含一個(gè) matcher 對(duì)象堤撵,它是通過(guò) createMatcher
創(chuàng)建的,matcher 對(duì)象含有 match
和 addRoutes
兩個(gè)方法
圖1:
另外上篇中還講了猿挚,在創(chuàng)建完 vue-router 實(shí)例后伞访,調(diào)用 Vue.use(Router) 會(huì)混入2個(gè)全局鉤子 beforeCreate 和 destroyed
圖2:
此時(shí)圖中第7行的 init
方法會(huì)初始化整個(gè) vue-router ,而實(shí)例化和初始化 vue-router 是有一點(diǎn)的區(qū)別的没讲,實(shí)例化指的是通過(guò) new Router 生成 vue-router 實(shí)例眯娱,初始化可以理解為進(jìn)行全局第一次的路由跳轉(zhuǎn)時(shí),讓 vue-router 實(shí)例和組件建立聯(lián)系爬凑,使得路由能夠接管組件
接下來(lái)我們進(jìn)入到 vue-router 的 init
方法
圖3:
在上篇中我講述了 vue-router 實(shí)例的 history 屬性等于當(dāng)前使用的路由模式(hash,html5,abstract)的實(shí)例徙缴,init
方法會(huì)根據(jù) history 屬性也就是根據(jù)不同模式的路由來(lái)執(zhí)行不同的邏輯,但是可以發(fā)現(xiàn)嘁信,不管是使用 hash 路由還是 html5 的路由于样,都會(huì)執(zhí)行 transitionTo
這個(gè)方法,它是整個(gè)路由跳轉(zhuǎn)的核心方法
路由跳轉(zhuǎn)
圖4:(刪除了取消路由導(dǎo)航的部分邏輯潘靖,這里只分析跳轉(zhuǎn)成功的邏輯)
可以發(fā)現(xiàn)圖4的第5行代碼執(zhí)行了 vue-router 實(shí)例的 match 方法穿剖,它最終會(huì)執(zhí)行上篇我們分析過(guò)的 matcher 屬性的 match
方法,并且傳入了2個(gè)參數(shù)
location:通過(guò)圖3中的
getCurrentLocation
方法卦溢,最終會(huì)生成一個(gè)跳轉(zhuǎn)目標(biāo)的 loaction 對(duì)象(通過(guò) push / replace 方法跳轉(zhuǎn))糊余,或一個(gè)跳轉(zhuǎn)目標(biāo)的路徑(通過(guò)瀏覽器 url 跳轉(zhuǎn))current:當(dāng)前頁(yè)面的路由 $route 對(duì)象
圖6:
繼續(xù)沿用上篇中示例秀又,當(dāng)我們直接在瀏覽器的 url 中輸入http://localhost:8080/#/comp1/comp1Child
時(shí),可以觀察到 location 參數(shù)為跳轉(zhuǎn)目標(biāo)的路徑贬芥,并且此時(shí)是全局第一次調(diào)用 transitionTo
方法吐辙,vue-router 默認(rèn)第一次跳轉(zhuǎn)的 current 參數(shù)為根路徑的 $route 對(duì)象,而以后的跳轉(zhuǎn)蘸劈,current 會(huì)變成當(dāng)前路由的 $route 對(duì)象
圖7(第一次 history.current 值為根路徑的 $route 對(duì)象):
分析過(guò) match
方法的2個(gè)參數(shù)后昏苏,接著會(huì)執(zhí)行上篇中分析過(guò)的 match
方法
(在創(chuàng)建 $router 的 match
方法中,其實(shí) current 參數(shù)一般很少用到威沫,主要圍繞 location 參數(shù)再結(jié)合3個(gè)路由映射表生成 $route 對(duì)象)
圖8(執(zhí)行圖4的 vue-router 實(shí)例的 match 方法最后會(huì)執(zhí)行到上篇分析的 match
方法):
此時(shí)贤惯,這個(gè)最終執(zhí)行的這個(gè) match
方法就會(huì)創(chuàng)建出一個(gè) $route 對(duì)象,并賦值給圖4的 route 屬性棒掠,隨后會(huì)進(jìn)入 confirmTransition
這個(gè)方法救巷,它負(fù)責(zé)控制所有的路由守衛(wèi)的執(zhí)行,我們來(lái)看一下它的內(nèi)部是如何運(yùn)行的
路由守衛(wèi)的原理
本小結(jié)會(huì)介紹 vue-router 一個(gè)比較重要的部分:路由守衛(wèi)
和組件的生命周期的鉤子不同句柠,路由守衛(wèi)將重點(diǎn)放在路由上浦译,能夠控制路由跳轉(zhuǎn),一般用在頁(yè)面級(jí)別的路由跳轉(zhuǎn)時(shí)控制跳轉(zhuǎn)的邏輯溯职,比如在路由守衛(wèi)中檢查用戶(hù)是否有進(jìn)入當(dāng)前頁(yè)面的權(quán)限精盅,沒(méi)有則跳轉(zhuǎn)到授權(quán)頁(yè)面,亦或是在離開(kāi)頁(yè)面時(shí)警告用戶(hù)有未確認(rèn)的信息谜酒,確認(rèn)后才能跳轉(zhuǎn)等等
在路由守衛(wèi)中叹俏,一般會(huì)接收3個(gè)參數(shù),to僻族,from粘驰,next,前兩個(gè)分別是跳轉(zhuǎn)后和跳轉(zhuǎn)前頁(yè)面路由的 $route 對(duì)象述么,第三個(gè)參數(shù) next 是一個(gè)函數(shù)蝌数,當(dāng)執(zhí)行 next 函數(shù)后會(huì)進(jìn)行跳轉(zhuǎn),如果一個(gè)包含 next 參數(shù)的路由守衛(wèi)里沒(méi)有執(zhí)行該函數(shù)度秘,頁(yè)面會(huì)無(wú)法跳轉(zhuǎn)顶伞,接下來(lái)我們來(lái)解密路由守衛(wèi)背后的原理
尋找跳轉(zhuǎn)前后路由的區(qū)別
圖9:
首先會(huì)拿到當(dāng)前的頁(yè)面的 route 對(duì)象的 matched 數(shù)組,返回這2個(gè)數(shù)組包含的路由記錄的區(qū)別**
在上篇中提到垢乙, $route 對(duì)象的 matched 屬性是一個(gè)數(shù)組锨咙,通過(guò) formatMatch
函數(shù)最終返回 $route 對(duì)象以及所有父級(jí)的路由記錄
resolveQueue
返回3個(gè)數(shù)組,updated 代表跳轉(zhuǎn)前后 matched 數(shù)組相同部分追逮,deactivated 代表刪除部分酪刀,activated 代表新增部分粱侣,舉個(gè)例子,當(dāng)我們從 comp1Child 頁(yè)面跳轉(zhuǎn)到 comp2 頁(yè)面蓖宦,這3個(gè)數(shù)組分別對(duì)應(yīng)的值
圖10:
圖11:
跳轉(zhuǎn)時(shí)哪些組件觸發(fā)哪些路由守衛(wèi)就是由這3個(gè)數(shù)組決定的,從這里就可以大致推斷出油猫,vue-router 會(huì)在新增的組件會(huì)觸發(fā) beforeRouteEnter 之類(lèi)的進(jìn)入守衛(wèi)稠茂,在相同部分觸發(fā) beforeRouteUpdate 守衛(wèi),在刪除部分觸發(fā) beforeRouteLeave 之類(lèi)的離開(kāi)守衛(wèi)
生成路由守衛(wèi)
接下來(lái)我們來(lái)證明上述的推斷情妖,執(zhí)行到圖9的第 9 行會(huì)聲明一個(gè) queue 數(shù)組睬关,它是一個(gè)隊(duì)列,看到 vue-router 會(huì)將這些相同的不同的路由記錄經(jīng)過(guò)一些函數(shù)的轉(zhuǎn)換毡证,最后放到數(shù)組中
通過(guò)旁邊定義的類(lèi)型能夠發(fā)現(xiàn)电爹,數(shù)組的元素都是 NavigationGuard 類(lèi)型
圖12:
可以發(fā)現(xiàn) NavigationGuard 就是一個(gè)標(biāo)準(zhǔn)的路由守衛(wèi)的簽名,可以推斷出料睛,經(jīng)過(guò) queue 數(shù)組內(nèi)部這些函數(shù)的轉(zhuǎn)換最終會(huì)返回路由守衛(wèi)組成的數(shù)組丐箩,而這些函數(shù)就是將上節(jié)中的路由記錄轉(zhuǎn)換為路由守衛(wèi)的函數(shù)
同時(shí)數(shù)組中的守衛(wèi)的排列順序也是設(shè)計(jì)好的,對(duì)應(yīng) vue-router 官方文檔中提到的路由導(dǎo)航解析流程
圖13:
我們先分析 queue 數(shù)組里第一個(gè)執(zhí)行的函數(shù) extractLeaveGuards
恤煞,經(jīng)過(guò)一層封裝最終會(huì)執(zhí)行通用函數(shù) extractGuards
圖14:
此時(shí) records 參數(shù)為刪除的路由記錄屎勘,name 為 beforeRouteLeave,即最終觸發(fā)的是 beforeRouteLeave 守衛(wèi)
然后會(huì)執(zhí)行 flatMapComponents
函數(shù)居扒,這個(gè)函數(shù)也是一個(gè)通用函數(shù)概漱,作用是 records 數(shù)組,每次執(zhí)行第二個(gè)回調(diào)函數(shù)喜喂,類(lèi)似數(shù)組的 forEach 方法瓤摧,而回調(diào)的參數(shù)解析如下
def:視圖名對(duì)應(yīng)的組件配置項(xiàng)(因?yàn)?vue-router 支持命名視圖所以可能會(huì)有多個(gè)視圖名,大部分情況為 default玉吁,及使用默認(rèn)視圖)照弥,當(dāng)是異步路由時(shí),def為異步返回路由的函數(shù)
instance:組件實(shí)例
match:當(dāng)前遍歷到的路由記錄
key:視圖名
在回調(diào)函數(shù)內(nèi)部會(huì)執(zhí)行 extractGuard
函數(shù)
圖15:
def 為組件配置項(xiàng)进副,通過(guò) Vue 核心庫(kù)的函數(shù) extend 將配置項(xiàng)轉(zhuǎn)為組件構(gòu)造器(雖然配置項(xiàng)中就能拿到對(duì)應(yīng)的路由守衛(wèi)产喉,但是從官方注釋發(fā)現(xiàn)只有轉(zhuǎn)為構(gòu)造器后才能拿到一些全局混入的鉤子),在生成構(gòu)造器時(shí)敢会,Vue 會(huì)將配置項(xiàng)賦值給構(gòu)造器的靜態(tài)屬性 options(extend 部分的解析可以看我另一篇博客)曾沈,最后返回配置項(xiàng)中對(duì)應(yīng)的路由守衛(wèi)函數(shù),即如果我們?cè)谔D(zhuǎn)后的組件中定義了 beforeRouteLeave 的話這里就會(huì)返回這個(gè)函數(shù)
在圖 14 中拿到返回值 guard 后會(huì)經(jīng)過(guò)一層處理鸥昏,例如扁平化塞俱,綁定 this 指向,根據(jù) reverse 參數(shù)決定是否要反轉(zhuǎn)數(shù)組(因?yàn)?matched 中路由記錄順序是父 => 子吏垮,而 beforeRouteLeave 需要從最里層子組件觸發(fā)障涯,所以需要進(jìn)行反轉(zhuǎn)保證守衛(wèi)觸發(fā)順序)罐旗,最后 queue 數(shù)組的元素如下
圖16:
值得注意的是最后一個(gè) resolveAsyncComponents
函數(shù),它的作用是解析異步路由
路由懶加載的原理
什么是異步路由呢唯蝶,通俗來(lái)說(shuō)就是使用路由懶加載返回的路由九秀,我們可以使用 import ()
這種語(yǔ)法去動(dòng)態(tài)的加載 JS 文件,放到 vue-router 中粘我,就可以實(shí)現(xiàn)異步加載組件配置項(xiàng)即路由懶加載(這里只討論開(kāi)發(fā)中使用較多的 import()
語(yǔ)法)
圖17:
我們進(jìn)入函數(shù)內(nèi)部一探究竟
圖18:
resolveAsyncComponents
函數(shù)最終會(huì)返回一個(gè)函數(shù)鼓蜒,并且符合路由守衛(wèi)的函數(shù)簽名(這里 vue-router 可能只是為了保證返回函數(shù)的一致性,實(shí)質(zhì)上在這個(gè)函數(shù)中征字,并不會(huì)用到 to,from 這2個(gè)參數(shù))
這個(gè)函數(shù)只是被定義了都弹,并沒(méi)有執(zhí)行,但是我們可以通過(guò)函數(shù)體觀察它是如何加載異步路由的匙姜。同樣通過(guò) flatMapComponents
遍歷新增的路由記錄畅厢,每次遍歷都執(zhí)行第二個(gè)回調(diào)函數(shù)
在回調(diào)函數(shù)里,會(huì)定義一個(gè) resolve
函數(shù)氮昧,當(dāng)異步組件加載完成后框杜,會(huì)通過(guò) then 的形式解析 promise,最終會(huì)調(diào)用 resolve
函數(shù)并傳入異步組件的配置項(xiàng)作為參數(shù)袖肥, resolve
函數(shù)接收到組件配置項(xiàng)后會(huì)像 Vue 中一樣將配置項(xiàng)轉(zhuǎn)為構(gòu)造器 霸琴,同時(shí)將值賦值給當(dāng)前路由記錄的 componts 屬性中(key 屬性默認(rèn)為 default)
另外 resolveAsyncComponents
函數(shù)會(huì)通過(guò)閉包保存一個(gè) pending 變量,代表接收的異步組件數(shù)量昭伸,在 flatMapComponents
遍歷的過(guò)程中梧乘,每次會(huì)將 pending 加一,而當(dāng)異步組件被解析完畢后再將 pending 減一庐杨,也就是說(shuō)选调,當(dāng) pengding 為 0 時(shí),代表異步組件全部解析完成灵份, 隨即執(zhí)行 next
方法伯诬,next
方法是 vue-router 控制整個(gè)路由導(dǎo)航順序的核心方法
執(zhí)行路由守衛(wèi)
在分析 next
方法之前承匣,我們先來(lái)看一下 vue-router 是如何處理 queue 數(shù)組中的元素的,在上文中,雖然定義了 queue 數(shù)組锌奴,其中包括了路由守衛(wèi)以及解析異步組件的函數(shù)倍奢,但是還沒(méi)有執(zhí)行
走到圖 9 的 24 行七芭,定義了一個(gè) iterator
函數(shù)片习,顧名思義它是一個(gè)迭代器,最后將 queue 和這個(gè)迭代器放入 runQueue
函數(shù)執(zhí)行枪眉,由此可以發(fā)現(xiàn)這個(gè) runQueue
是一個(gè)用來(lái)遍歷 queue 數(shù)組的函數(shù)捺檬,看到這里有些朋友會(huì)有疑問(wèn),為啥 vue-router 大費(fèi)周章的定義一個(gè) runQueue 函數(shù)贸铜,直接一個(gè) forEach 不就好了嗎
[圖片上傳失敗...(image-554cb-1558881177066)]
接下來(lái)我們進(jìn)入函數(shù)內(nèi)部一探究竟
遍歷 queue 數(shù)組
圖19:
runQueue
內(nèi)部聲明了一個(gè) step
的函數(shù)堡纬,它一個(gè)是控制 runQueue
是否繼續(xù)遍歷的函數(shù)聂受,當(dāng)我們第一次執(zhí)行時(shí),給 step
函數(shù)傳入?yún)?shù) 0 表示開(kāi)始遍歷 queue 第 1 個(gè)元素烤镐,通過(guò) step
函數(shù)內(nèi)部可以發(fā)現(xiàn)蛋济,它最終會(huì)執(zhí)行參數(shù) fn,也就是 iterator
這個(gè)迭代器函數(shù)炮叶,給它傳入當(dāng)前遍歷的 queue 元素以及一個(gè)回調(diào)函數(shù)碗旅,這個(gè)回調(diào)函數(shù)里保存著遍歷下個(gè)元素的邏輯,也就是說(shuō)runQueue
將是否需要繼續(xù)遍歷的控制權(quán)傳入了 iterator
函數(shù)中
這里先拋出結(jié)論
runQueue
函數(shù)只負(fù)責(zé)遍歷數(shù)組悴灵,并不會(huì)執(zhí)行邏輯,它依次遍歷 queue 數(shù)組的元素骂蓖,每次遍歷時(shí)會(huì)將當(dāng)前元素交給外部定義的iterator
迭代器去執(zhí)行积瞒,而iterator
迭代器一旦處理完元素就讓runQueue
再次執(zhí)行下個(gè)元素,當(dāng)數(shù)組全部遍歷結(jié)束時(shí)登下,會(huì)執(zhí)行參數(shù) cb 這個(gè)回調(diào)函數(shù)
runQueue
和普通的 forEach 遍歷數(shù)組不同點(diǎn)在于茫孔,forEach 是同步的,而 vue-router 中可能會(huì)存在異步路由被芳,所以需要設(shè)計(jì)一個(gè)支持異步的遍歷函數(shù)缰贝,只有當(dāng) iterator
函數(shù)執(zhí)行完一次后 runQueue
才會(huì)接著遍歷下一個(gè)元素
接著我們來(lái)看一下 runQueue
將元素交給迭代器執(zhí)行時(shí)發(fā)生了什么
迭代器
對(duì)應(yīng)圖 9 中24-41行代碼:
其中迭代器的參數(shù) next 即 runQueue
中的 step
函數(shù)
我們知道,當(dāng)在路由守衛(wèi)中如果沒(méi)有執(zhí)行 next
函數(shù)畔濒,路由將無(wú)法跳轉(zhuǎn)剩晴,原因是因?yàn)闆](méi)有去執(zhí)行 hook
的第三個(gè)回調(diào)函數(shù),也就不會(huì)執(zhí)行 iterator
的第三個(gè)參數(shù) next
侵状,最終導(dǎo)致不會(huì)通知 runQueue
繼續(xù)往下遍歷
另外當(dāng)我們給 next
函數(shù)傳入另一個(gè)路徑時(shí)赞弥,會(huì)取消原來(lái)的導(dǎo)航,取而代之跳轉(zhuǎn)到指定的路徑趣兄,原因是因?yàn)闈M(mǎn)足上圖的 true 邏輯绽左,執(zhí)行 abort
函數(shù)取消導(dǎo)航,隨后會(huì)調(diào)用 push/replace 將路由重新跳轉(zhuǎn)到指定的頁(yè)面
最后回到之前異步路由中提到的那個(gè) next
函數(shù)艇潭,當(dāng)所有的異步路由都被解析完成后拼窥,才會(huì)執(zhí)行 next
函數(shù)繼續(xù)遍歷 queue 數(shù)組的下個(gè)元素,一旦有某個(gè)路由沒(méi)有被解析完成蹋凝,vue-router 就會(huì)一直等待直到接受到為止鲁纠,然后才會(huì)去觸發(fā)之后的邏輯
遍歷成功后的回調(diào)
當(dāng) queue 最后一個(gè)元素也就是異步組件被解析完成后,runQueue
會(huì)執(zhí)行傳入的第三個(gè)參數(shù)鳍寂,即執(zhí)行遍歷成功回調(diào)
對(duì)應(yīng)圖 9 中的 44-64 行:
可以看到成功回調(diào)里 vue-router 又往 queue 中添加了路由守衛(wèi)房交,同時(shí)會(huì)開(kāi)啟第二輪遍歷......
關(guān)于第二輪的 queue 數(shù)組遍歷礙于篇幅我會(huì)放到下篇來(lái)說(shuō)
總結(jié)
當(dāng) vue 的根實(shí)例被實(shí)例化時(shí),會(huì)執(zhí)行 vue-router 的初始化邏輯伐割,和實(shí)例化不同的是候味,初始化在實(shí)例化之后刃唤,作用是建立 vue-router 和 Vue 組件之間的關(guān)系
當(dāng)初始化時(shí)會(huì)進(jìn)行第一次路由跳轉(zhuǎn),根據(jù)跳轉(zhuǎn)路徑生成 loaction 對(duì)象白群,再通過(guò) location 對(duì)象生成 $route
$route 對(duì)象的 matched 屬性保存了當(dāng)前和所有父級(jí)的路由記錄尚胞,在路由跳轉(zhuǎn)時(shí)會(huì)根據(jù)跳轉(zhuǎn)前后 $route 對(duì)象的這2個(gè) matched 屬性,區(qū)分出相同和不同的路由記錄帜慢,來(lái)決定哪些組件觸發(fā)哪些路由守衛(wèi)
vue-router 通過(guò)回調(diào)的形式異步的執(zhí)行路由守衛(wèi)笼裳,當(dāng)前一個(gè)解析完畢后會(huì)調(diào)用回調(diào)繼續(xù)執(zhí)行下個(gè)守衛(wèi)
只有懶加載的路由都加載完成后,才會(huì)執(zhí)行上述的回調(diào)粱玲,繼續(xù)執(zhí)行下個(gè)守衛(wèi)躬柬,否則會(huì)一直等待