Vue-Router

回憶:

????????我們知道,h5的history或者h(yuǎn)ash幫助我們解決了,變化url跳轉(zhuǎn)頁面不發(fā)送請(qǐng)求,并且我們能監(jiān)聽到url變化。這只是第一步,不同url要渲染對(duì)應(yīng)不同內(nèi)容,才是最麻煩的問題。

? ? ? ? 我們有一個(gè)路由列表,我們會(huì)先把遍歷遞歸它,把每個(gè)路由對(duì)象重新進(jìn)行定義描述成RouteRecord對(duì)象,得到pathMap、pathList、nameMap三個(gè)數(shù)組(包括路徑、名稱到路由RouteRecord的映射關(guān)系)。提供了addRoutes方法方便我們可以對(duì)這3個(gè)數(shù)組做動(dòng)態(tài)修改。提供一個(gè)match方法,我們可以通過傳入的位置Location和當(dāng)前路徑RouteRecord計(jì)算出新的位置并匹配到對(duì)應(yīng)的路由RouteRecord,然后根據(jù)新的位置和RouteRecord計(jì)算出一個(gè)新路徑Route對(duì)象。Route對(duì)象有一個(gè)matched屬性,值是一個(gè)數(shù)組,把當(dāng)前路徑匹配到的RouteRecord,往上遍歷它的parent,直到最根路徑,所有RouteRecord都可以先父后子保存到這個(gè)數(shù)組中。

? ? ? ? 我們?cè)谧雎窂角袚QtransitionTo時(shí),先通過match(輸入位置,當(dāng)前路徑RouteRecord)方法得到新Route對(duì)象。然后通過confirmTransition(Route對(duì)象,成功回調(diào),失敗回調(diào))完成一次路徑切換。會(huì)根據(jù)當(dāng)前路徑的Route對(duì)象的matched屬性值和目標(biāo)路徑的Route對(duì)象的matched屬性值,找到兩者數(shù)組中l(wèi)ength長(zhǎng)度最長(zhǎng)的值,遍歷它,找到第一個(gè)不同點(diǎn),得到updated(目標(biāo)RouteRecord和當(dāng)前RouteRecord相同,前面重復(fù)的部分)、activated(目標(biāo)RouteRecord和當(dāng)前RouteRecord不同,后面不同的部分)、deactivated(當(dāng)前RouteRecord和目標(biāo)RouteRecord不同,后面不同的部分)三個(gè)RouteRecord數(shù)組。后面導(dǎo)航守衛(wèi)有用。

? ? ? ? 一、導(dǎo)航守衛(wèi),就是在路勁切換時(shí)候執(zhí)行了一系列鉤子函數(shù)。

? ? ? ? 1、通過deactivated數(shù)組去得到一個(gè)個(gè)即將離開的失活組件內(nèi)的beforeRouteEnter函數(shù),先子后父去調(diào)用他們(1、不能獲取組件實(shí)例this。2、在渲染組件的對(duì)應(yīng)路由被confirm前調(diào)用。3、當(dāng)守衛(wèi)執(zhí)行前,組件實(shí)例還沒有被創(chuàng)建。)

? ? ? ? 2、通過this.router.beforeHooks調(diào)用全局定義beforeEach守衛(wèi)。

? ? ? ? 3、通過updated數(shù)組去得到在重用的組件里調(diào)用的beforeRouteUpdate守衛(wèi)。

? ? ? ? 4、通過調(diào)用activated.map(m => m.beforeEnter),調(diào)用激活的路由配置中定義的?beforeEnter?函數(shù)。

? ? ? ? 5、解析異步路由組件。6、在被激活的組件里調(diào)用beforeRouteEnter。7、調(diào)用全局的beforeResolve守衛(wèi)。8、導(dǎo)航被確認(rèn)。9、調(diào)用全局的afterEach鉤子。10、觸發(fā)DOM更新。11、用創(chuàng)建好的實(shí)例調(diào)用beforeRouteEnter守衛(wèi)中傳給next回調(diào)函數(shù)。

? ? ? ? 二、路徑切換中,url的變化(我們分析hash模式)。當(dāng)我們點(diǎn)擊<router-link>時(shí),會(huì)去執(zhí)行history的push方法。實(shí)際上會(huì)去執(zhí)行transitionTo做一次路徑切換,在切換的成功回調(diào)中會(huì)執(zhí)行pushHash(route.fullPath全路徑)做url變化當(dāng)瀏覽器回退為什么會(huì)觸發(fā)路徑變換?因?yàn)樵谖覀兂跏蓟瘯r(shí)會(huì)做一次路徑變換,成功回調(diào)會(huì)初始化監(jiān)聽器(popstate或者h(yuǎn)ashchange)。在hash模式下為什么會(huì)自動(dòng)給url路徑添加“#”?在index.js中History實(shí)例化過程中,會(huì)去做到。

? ? ? ? 三、組件渲染。首先<router-view>是函數(shù)式組件,和普通render函數(shù)不同,它支持第二個(gè)參數(shù){props,children,parent,data}(和component.options相似)。執(zhí)行var h = parent.$creatElement方法(對(duì)于<router-view>來說,parent就是它占位符所在位置的組件的實(shí)例)。

????????通過props.name取到跳轉(zhuǎn)路由的name屬性。

????????取到parent.$route(路由初始化beforeCreate時(shí),會(huì)把 根Vue._route設(shè)為響應(yīng)式,值為this._router.history.current。若為根vue,把this._routerRoot設(shè)為它自己,若為非根vue,this._routerRoot指向根vue。給Vue原型上定義了$route,默認(rèn)值為this._routerRoot._route,所以非根Vue取$route會(huì)去取根vue的this._routerRoot._route,得到this._router.history.current當(dāng)前路徑對(duì)象并觸發(fā)其響應(yīng)式get函數(shù))。

? ? ? ? 我們?cè)趺粗?lt;router-view>渲染什么組件?<router-view>的層級(jí)關(guān)系和路由表的嵌套關(guān)系就是一一映射的關(guān)系,我們需要根據(jù)路由表的嵌套關(guān)系去找到<router-view>渲染的組件。在<router-view>函數(shù)組件中,我們會(huì)往上循環(huán)parent直到根vue,如果它是一個(gè)嵌套的<router-view>(即它占位符所在位置的組件的實(shí)例也是<router-view>渲染出來的),會(huì)對(duì)depth(深度記錄)++,也就得到了當(dāng)前<router-view>的路由深度。

????????這個(gè)depth有什么用呢?我們?cè)谧雎窂睫D(zhuǎn)換去根據(jù)輸入位置和當(dāng)前路徑得到新路徑時(shí)得到的Route對(duì)象中的matched屬性值是 根路由到當(dāng)前路由的層級(jí)routeRecord記錄。const matched = route.matched[depth].components.name就得到了當(dāng)前路由對(duì)應(yīng)的組件。這樣我們就知道<router-view>對(duì)應(yīng)渲染什么組件。

? ? ? ? 我們導(dǎo)航鉤子執(zhí)行中會(huì)去通過bindGuard去給鉤子函數(shù)綁定上下文,綁定的上下文為對(duì)應(yīng)組建實(shí)例(通過routedRecord的instance屬性值取到)。routeRecord對(duì)象的components屬性值拿到的是組件實(shí)例的options。那instance屬性值(組件實(shí)例)是怎么拿到的?我們?cè)诿總€(gè)組件組件初始化beforeCreated時(shí),會(huì)執(zhí)行registerInstance(vm.$options._parentVnode.data.registerRouteInstance(vm)存在的話會(huì)執(zhí)行),而在<router-view>函數(shù)中,會(huì)給其定義data.registerRouteInstance(vm)方法(給當(dāng)前路由routeRecord的instance屬性值賦值vm組件實(shí)例)。

? ? ? ? 路徑切換的時(shí)候?yàn)槭裁磿?huì)執(zhí)行<router-view>對(duì)應(yīng)的render函數(shù)?前面分析過<view-router>函數(shù),獲取parent.$route時(shí),會(huì)去取根vue的this._routerRoot._route,觸發(fā)其響應(yīng)式get函數(shù),進(jìn)行訂閱者收集并更新視圖。我們會(huì)去監(jiān)聽history變化然后改變Vue._route,觸發(fā)視圖重新渲染(當(dāng)transitionTo路徑切換完,history變化)。

? ? ? ? <router-link>也是函數(shù)式組件,props中一系列屬性在我們寫router-link時(shí)可以傳入。render函數(shù)邏輯,就是處理props中屬性(tag,activeClass等),最后切換url。

總結(jié):

????????路由始終會(huì)維護(hù)當(dāng)前的線路,路由切換的時(shí)候會(huì)把當(dāng)前路線切換到目標(biāo)線路,切換過程中會(huì)執(zhí)行一系列的導(dǎo)航守衛(wèi)鉤子函數(shù),會(huì)更改url,同樣會(huì)渲染對(duì)應(yīng)組件,切換完畢會(huì)把目標(biāo)路線更新替換當(dāng)前的線路,作為下一次路徑切換的依據(jù)。

概述:

????????Vue-Router是Vue.js官方提供的路由解決方案,它的作用就是根據(jù)不同路徑映射到不同的視圖。

? ? ? ? 它非常強(qiáng)大,支持hash、history、abstract3種路由方式,提供了<router-link>和<router-view>2種組件,還提供了簡(jiǎn)單的路由配置和一系列好用的API。

一、Vue.use,插件注冊(cè)原理。

????????當(dāng)我們 import VueRouter from 'vue-router'時(shí),VueRouter得到的是什么?Vue-router源碼中,在package.json中moudule的定義是WebPack3對(duì)Vue-router打包后的源碼。在build/configs.js中的genConfig中定義了打包的入口文件。由此我們知道,入口文件在src/index.js中。從入口文件得知,我們得到的是一個(gè)VueRouter的Class(類)。

? ? ? ? Vue.use定義Vue源碼的src/core/global-api中的use.js中,Vue.use=function(plugin){...}。它主要做了兩件事:

????????1、管理注冊(cè)。通過installedPlugins來管理注冊(cè)過的組件,防止組件多次注冊(cè)。

????????2、在插件內(nèi)拿到Vue去做一些事。拿到Vue.use(plugin,...)中plugin后面的參數(shù)(toArray(arguments,1))作為數(shù)組args,把Vue添加到args數(shù)組頭部(之后插件會(huì)通過Vue的屬性方法去做一些事情)。然后判斷plugin.install是否是函數(shù)(typeof ... === 'function' )?是的話,plugin.install.apply(plugin, argus)。否的話,plugin.apply(null, args)。所以通常我們都會(huì)為一個(gè)插件編寫一個(gè)install方法。

? ??????VueRouter中的install方法(vue-router源碼的src/install)。

vue-router的install方法

? ??????install方法,1、通過installed和Vue判斷是否已經(jīng)進(jìn)行過install方法進(jìn)行注冊(cè),進(jìn)行過就return,不會(huì)進(jìn)行多次注冊(cè)。2、通過Vue.mixin(options)把mixin擴(kuò)展到全局Vue的options中,這樣每個(gè)組件的beforeCreaed和destory鉤子函數(shù)里都會(huì)有這里定義的邏輯。3、在Vue原型上定義了$router和$route兩個(gè)屬性,返回值為this._routerRoot._router和this._routerRoot._route(this指代取該值的上下文)。4、注冊(cè)RouterView和RouterLink兩個(gè)組件。

二、var routes = new VueRouter({...}),new Vue({routes}),對(duì)路由進(jìn)行實(shí)例化,并傳入全局Vue。我們看看實(shí)例化路由時(shí)做了什么操作?1、通過createMatcher(options.routes || [], this)得到的this.matchermatch方法(根據(jù)傳入的路徑和當(dāng)前的路徑,計(jì)算出新的路徑)和addRoutes方法(根據(jù)路由列表,創(chuàng)建一個(gè)路由映射表)組成。2、對(duì)路由模式進(jìn)行了判斷,實(shí)例化相應(yīng)路由實(shí)例HTML5History/HashHistory/AbstractHistory,它們都繼承于History類。??

? ??????createMatcher初始化就是根據(jù)路由的配置描述創(chuàng)建映射表,包括路徑、名稱到路由record的映射關(guān)系。它提供一個(gè)match方法 ,會(huì)根據(jù)傳入的位置和路徑計(jì)算出新的位置,并匹配到對(duì)應(yīng)的路由record,然后根據(jù)新的位置和record創(chuàng)建新的路徑并返回。

三、我們通過import..拿到VueRouter(class),又通過Vue.use(VueRouter)注冊(cè)了插件。接下來是插件在組件中的初始化。

????????install時(shí)通過Vue.mixin對(duì)全局Vue擴(kuò)展了兩個(gè)鉤子函數(shù)beforeCreate和destroyed。初始化就在beforeCreate時(shí)進(jìn)行,1、如果是根Vue,調(diào)用this._router.init()方法,否則把子組件的this._routerRoot指向根Vue的_routerRoot。1.1、this._router.init()又會(huì)去執(zhí)行transitionTo方法做路徑切換(還有history的push和replace都會(huì)觸發(fā)路徑切換)。

1、了解導(dǎo)航守衛(wèi)的執(zhí)行邏輯。導(dǎo)航守衛(wèi)實(shí)質(zhì)是在路徑切換過程中執(zhí)行了一些鉤子函數(shù)。

路徑切換

? ? ? 1、?根據(jù)目標(biāo)location和當(dāng)前路徑,生成新路徑。 transitionTo?首先根據(jù)目標(biāo)?location?和當(dāng)前路徑?this.current?執(zhí)行?this.router.match?方法去匹配到目標(biāo)的路徑。這里?this.current?是?history?維護(hù)的當(dāng)前路徑,?this.current?的初始值是在?history的構(gòu)造函數(shù)中初始化的:this.current=START,export const START=createRoute(null,{path:'/'})。這樣就創(chuàng)建了一個(gè)初始的?Route,而?transitionTo?實(shí)際上也就是在切換?this.current,稍后我們會(huì)看到。

? ? ? ??2、做路徑切換。拿到新的路徑后,那么接下來就會(huì)執(zhí)行?confirmTransition?方法去做真正的路徑切換,由于這個(gè)過程可能有一些異步的操作(如異步組件),所以整個(gè)?confirmTransition?API 設(shè)計(jì)成帶有成功回調(diào)函數(shù)和失敗回調(diào)函數(shù)。

????????confirmTransition。1、定義了?abort?函數(shù)(取消跳轉(zhuǎn)調(diào)用),然后判斷如果計(jì)算后的?route?和?current?是相同路徑的話,調(diào)用?this.ensureUrl(之后介紹) 和?abort。2、根據(jù)?current.matched?和?route.matched?執(zhí)行了?resolveQueue?方法解析出 3 個(gè)隊(duì)列updated、activated、deactivated。從當(dāng)前route、新route開始往上查找父routeRecord,直到根routeRecord,形成2個(gè)數(shù)組。從頭對(duì)比這兩個(gè)數(shù)組,直到第一個(gè)不同點(diǎn),取到其index。updated:是目標(biāo)切換route和當(dāng)前route相同,前面重復(fù)的部分。activated:是目標(biāo)切換route和當(dāng)前route不同,后面不同的部分。deactivated:是當(dāng)前route和目標(biāo)切換route不同,后面不同的部分。后面需要根據(jù)3個(gè)隊(duì)列判斷哪些守衛(wèi)導(dǎo)航需要執(zhí)行。

? ? ? ? 3、導(dǎo)航守衛(wèi)。實(shí)際上就是發(fā)生在路由路徑切換時(shí),執(zhí)行的一系列鉤子函數(shù)。從整體上看一下這些鉤子函數(shù)執(zhí)行的邏輯,首先構(gòu)造一個(gè)隊(duì)列?queue,它實(shí)際上是一個(gè)數(shù)組;然后再定義一個(gè)迭代器函數(shù)?iterator;最后再執(zhí)行?runQueue?方法執(zhí)行這個(gè)隊(duì)列。

這是一個(gè)非常經(jīng)典的異步函數(shù)隊(duì)列化執(zhí)行的模式,?queue?是一個(gè)?NavigationGuard?類型的數(shù)組,我們定義了?step?函數(shù),每次根據(jù)?index?從?queue?中取一個(gè)?guard,然后執(zhí)行?fn?函數(shù),并且把?guard?作為參數(shù)傳入,第二個(gè)參數(shù)是一個(gè)函數(shù),當(dāng)這個(gè)函數(shù)執(zhí)行的時(shí)候再遞歸執(zhí)行?step?函數(shù),前進(jìn)到下一個(gè),注意這里的?fn?就是我們剛才的?iterator?函數(shù)
iterator?函數(shù)邏輯很簡(jiǎn)單,它就是去執(zhí)行每一個(gè) 導(dǎo)航守衛(wèi)?hook,并傳入?route、current?和匿名函數(shù),這些參數(shù)對(duì)應(yīng)文檔中的?to、from、next,當(dāng)執(zhí)行了匿名函數(shù),會(huì)根據(jù)一些條件執(zhí)行?abort?或?next,只有執(zhí)行?next?的時(shí)候,才會(huì)前進(jìn)到下一個(gè)導(dǎo)航守衛(wèi)鉤子函數(shù)中,這也就是為什么官方文檔會(huì)說只有執(zhí)行?next?方法來?resolve?這個(gè)鉤子函數(shù)。
queue?是怎么構(gòu)造的:1、在失活的組件里調(diào)用離開守衛(wèi)。2、調(diào)用全局的?beforeEach?守衛(wèi)。3、在重用的組件里調(diào)用?beforeRouteUpdate 守衛(wèi)。4、在激活的路由配置里調(diào)用?beforeEnter。5、解析異步路由組件。

? ??????第一步,在失活的組件里調(diào)用離開守衛(wèi)

執(zhí)行extractGuards,傳入當(dāng)前路由(即將離開)的deactive(比較出來不同的即將不要的routeRecord),beforeRouteLeave是在組件中定義的。
我們要去得到失活組件的離開守衛(wèi),并把它們排成列表 先子后父
去得到離開守衛(wèi)列表
去組件中得到守衛(wèi)函數(shù)
把守衛(wèi)函數(shù)的上下文綁定對(duì)應(yīng)組件實(shí)例進(jìn)行執(zhí)行

????????flatMapComponents做的事情,通過從上面切換路徑時(shí)confirmTransition得到的deactive數(shù)組,去得到與組件相關(guān)的參數(shù)。然后調(diào)用作為參數(shù)傳入的fn函數(shù),把前面得到的參數(shù)傳給fn函數(shù)。fn調(diào)用完畢即得到守衛(wèi)列表。

? ? ? ? 第二步,調(diào)用用戶注冊(cè)的全局?beforeEach?守衛(wèi)

當(dāng)我們使用?router.beforeEach?注冊(cè)了一個(gè)全局守衛(wèi),就會(huì)往?router.beforeHooks?添加一個(gè)鉤子函數(shù),這樣?this.router.beforeHooks?獲取的就是用戶注冊(cè)的全局?beforeEach?守衛(wèi)。

????????list是router實(shí)例的beforeHooks鉤子函數(shù)數(shù)組fn是我們調(diào)用beforeEach時(shí)自定義回調(diào)函數(shù)。

? ? ? ? 第三步,extractUpdateHooks(updated)。調(diào)用所有重用的組件中定義的?beforeRouteUpdate?鉤子函數(shù)

和?extractLeaveGuards(deactivated)?類似都是調(diào)用extractGuards,只不過傳入的數(shù)據(jù)是updated,name為beforeRouteUpdate

? ? ? ? 第四步,執(zhí)行?activated.map(m => m.beforeEnter),調(diào)用激活的路由配置中定義的?beforeEnter?函數(shù)。

路由中定義的

? ? ? ? 第五步,執(zhí)行?resolveAsyncComponents(activated)?解析異步組件。

????????resolveAsyncComponents?返回的是一個(gè)導(dǎo)航守衛(wèi)函數(shù),有標(biāo)準(zhǔn)的?to、from、next?參數(shù)。它的內(nèi)部實(shí)現(xiàn)很簡(jiǎn)單,利用了?flatMapComponents?方法從?matched?中獲取到每個(gè)組件的定義,判斷如果是異步組件,則執(zhí)行異步組件加載邏輯,這塊和我們之前分析?Vue?加載異步組件很類似,加載成功后會(huì)執(zhí)行?match.components[key] = resolvedDef?把解析好的異步組件放到對(duì)應(yīng)的?components?上,并且執(zhí)行?next?函數(shù)。

????????這樣在?resolveAsyncComponents(activated)?解析完所有激活的異步組件后,我們就可以拿到這一次所有激活的組件。這樣我們?cè)?b>做完這 5 步后又做了一些事情:

后續(xù)

? ??????第六步,在被激活的組件里調(diào)用?beforeRouteEnter

????????第七步,調(diào)用全局的?beforeResolve?守衛(wèi)。

????????第八步,調(diào)用全局的?afterEach?鉤子。

????????那么至此我們把所有導(dǎo)航守衛(wèi)的執(zhí)行分析完畢了,我們知道路由切換除了執(zhí)行這些鉤子函數(shù),從表象上有 2 個(gè)地方會(huì)發(fā)生變化,一個(gè)是 url 發(fā)生變化,一個(gè)是組件發(fā)生變化。接下來我們分別介紹這兩塊的實(shí)現(xiàn)原理。

2、了解url的變化邏輯。

????????當(dāng)我們點(diǎn)擊?router-link?的時(shí)候,實(shí)際上最終會(huì)執(zhí)行?router.push(this.history.push),我們介紹一下hash History的push實(shí)現(xiàn)。

hash History的push

????????會(huì)先通過transitionTo做路徑切換,成功的回調(diào)會(huì)執(zhí)行pushHash(route.fullPath)方法(url相關(guān)),handleScroll(滾動(dòng)條相關(guān))。

pushHash
判斷是否支持h5的pushState
如果支持h5的pushState去執(zhí)行replace或者pushState

然后在?history?的初始化中,會(huì)設(shè)置一個(gè)監(jiān)聽器,監(jiān)聽歷史棧的變化:

當(dāng)點(diǎn)擊瀏覽器返回按鈕的時(shí)候,如果已經(jīng)有 url 被壓入歷史棧,則會(huì)觸發(fā)?popstate?事件,然后拿到當(dāng)前要跳轉(zhuǎn)的?hash,執(zhí)行?transtionTo?方法做一次路徑轉(zhuǎn)換。

3、了解組建渲染的邏輯。

????????路由最終的渲染離不開組件,Vue-Router 內(nèi)置了?<router-view>?組件,它的定義在?src/components/view.js?中。<router-view>?是一個(gè)?functional?組件,它的渲染也是依賴?render?函數(shù),那么?<router-view>具體應(yīng)該渲染什么組件呢?

????????首先獲取當(dāng)前的路徑:const route=parent.$route。在?src/install.js?中,我們給 Vue 的原型上定義了?$route。

? ??????然后在?VueRouter?的實(shí)例執(zhí)行?router.init?方法的時(shí)候,會(huì)執(zhí)行如下邏輯,定義在?src/index.js中:

把a(bǔ)pp實(shí)例的_route和路徑對(duì)應(yīng)起來
history.listen
在?updateRoute?的時(shí)候執(zhí)行?this.cb

????????也就是我們執(zhí)行?transitionTo?方法最后執(zhí)行?updateRoute?的時(shí)候會(huì)執(zhí)行回調(diào),然后會(huì)更新所有組件實(shí)例的?_route?值,所以說?$route?對(duì)應(yīng)的就是當(dāng)前的路由線路。

????????<router-view>?是支持嵌套的,回到?render?函數(shù),其中定義了?depth?的概念,它表示?<router-view>?嵌套的深度。每個(gè)?<router-view>?在渲染的時(shí)候,執(zhí)行如下邏輯:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容