Vue-Router

回憶:

????????我們知道,h5的history或者hash幫助我們解決了,變化url跳轉頁面不發送請求,并且我們能監聽到url變化。這只是第一步,不同url要渲染對應不同內容,才是最麻煩的問題。

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

? ? ? ? 我們在做路徑切換transitionTo時,先通過match(輸入位置,當前路徑RouteRecord)方法得到新Route對象。然后通過confirmTransition(Route對象,成功回調,失敗回調)完成一次路徑切換。會根據當前路徑的Route對象的matched屬性值和目標路徑的Route對象的matched屬性值,找到兩者數組中length長度最長的值,遍歷它,找到第一個不同點,得到updated(目標RouteRecord和當前RouteRecord相同,前面重復的部分)、activated(目標RouteRecord和當前RouteRecord不同,后面不同的部分)deactivated(當前RouteRecord和目標RouteRecord不同,后面不同的部分)三個RouteRecord數組。后面導航守衛有用。

? ? ? ? 一、導航守衛,就是在路勁切換時候執行了一系列鉤子函數。

? ? ? ? 1、通過deactivated數組去得到一個個即將離開的失活組件內的beforeRouteEnter函數,先子后父去調用他們(1、不能獲取組件實例this。2、在渲染組件的對應路由被confirm前調用。3、當守衛執行前,組件實例還沒有被創建。)

? ? ? ? 2、通過this.router.beforeHooks調用全局定義beforeEach守衛。

? ? ? ? 3、通過updated數組去得到在重用的組件里調用的beforeRouteUpdate守衛。

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

? ? ? ? 5、解析異步路由組件。6、在被激活的組件里調用beforeRouteEnter。7、調用全局的beforeResolve守衛。8、導航被確認。9、調用全局的afterEach鉤子。10、觸發DOM更新。11、用創建好的實例調用beforeRouteEnter守衛中傳給next回調函數。

? ? ? ? 二、路徑切換中,url的變化(我們分析hash模式)。當我們點擊<router-link>時,會去執行history的push方法。實際上會去執行transitionTo做一次路徑切換,在切換的成功回調中會執行pushHash(route.fullPath全路徑)做url變化當瀏覽器回退為什么會觸發路徑變換?因為在我們初始化時會做一次路徑變換,成功回調會初始化監聽器(popstate或者hashchange)。在hash模式下為什么會自動給url路徑添加“#”?在index.js中History實例化過程中,會去做到。

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

????????通過props.name取到跳轉路由的name屬性。

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

? ? ? ? 我們怎么知道<router-view>渲染什么組件?<router-view>的層級關系和路由表的嵌套關系就是一一映射的關系,我們需要根據路由表的嵌套關系去找到<router-view>渲染的組件。在<router-view>函數組件中,我們會往上循環parent直到根vue,如果它是一個嵌套的<router-view>(即它占位符所在位置的組件的實例也是<router-view>渲染出來的),會對depth(深度記錄)++,也就得到了當前<router-view>的路由深度。

????????這個depth有什么用呢?我們在做路徑轉換去根據輸入位置和當前路徑得到新路徑時得到的Route對象中的matched屬性值是 根路由到當前路由的層級routeRecord記錄。const matched = route.matched[depth].components.name就得到了當前路由對應的組件。這樣我們就知道<router-view>對應渲染什么組件。

? ? ? ? 我們導航鉤子執行中會去通過bindGuard去給鉤子函數綁定上下文,綁定的上下文為對應組建實例(通過routedRecord的instance屬性值取到)。routeRecord對象的components屬性值拿到的是組件實例的options。那instance屬性值(組件實例)是怎么拿到的?我們在每個組件組件初始化beforeCreated時,會執行registerInstance(vm.$options._parentVnode.data.registerRouteInstance(vm)存在的話會執行),而在<router-view>函數中,會給其定義data.registerRouteInstance(vm)方法(給當前路由routeRecord的instance屬性值賦值vm組件實例)。

? ? ? ? 路徑切換的時候為什么會執行<router-view>對應的render函數?前面分析過<view-router>函數,獲取parent.$route時,會去取根vue的this._routerRoot._route,觸發其響應式get函數,進行訂閱者收集并更新視圖。我們會去監聽history變化然后改變Vue._route,觸發視圖重新渲染(當transitionTo路徑切換完,history變化)。

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

總結:

????????路由始終會維護當前的線路,路由切換的時候會把當前路線切換到目標線路,切換過程中會執行一系列的導航守衛鉤子函數,會更改url,同樣會渲染對應組件,切換完畢會把目標路線更新替換當前的線路,作為下一次路徑切換的依據。

概述:

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

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

一、Vue.use,插件注冊原理。

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

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

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

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

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

vue-router的install方法

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

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

? ??????createMatcher初始化就是根據路由的配置描述創建映射表,包括路徑、名稱到路由record的映射關系。它提供一個match方法 ,會根據傳入的位置和路徑計算出新的位置,并匹配到對應的路由record,然后根據新的位置和record創建新的路徑并返回

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

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

1、了解導航守衛的執行邏輯。導航守衛實質是在路徑切換過程中執行了一些鉤子函數。

路徑切換

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

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

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

? ? ? ? 3、導航守衛。實際上就是發生在路由路徑切換時,執行的一系列鉤子函數。從整體上看一下這些鉤子函數執行的邏輯,首先構造一個隊列?queue,它實際上是一個數組;然后再定義一個迭代器函數?iterator;最后再執行?runQueue?方法執行這個隊列。

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

? ??????第一步,在失活的組件里調用離開守衛。

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

????????flatMapComponents做的事情,通過從上面切換路徑時confirmTransition得到的deactive數組,去得到與組件相關的參數。然后調用作為參數傳入的fn函數,把前面得到的參數傳給fn函數。fn調用完畢即得到守衛列表。

? ? ? ? 第二步,調用用戶注冊的全局?beforeEach?守衛

當我們使用?router.beforeEach?注冊了一個全局守衛,就會往?router.beforeHooks?添加一個鉤子函數,這樣?this.router.beforeHooks?獲取的就是用戶注冊的全局?beforeEach?守衛。

????????list是router實例的beforeHooks鉤子函數數組fn是我們調用beforeEach時自定義回調函數。

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

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

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

路由中定義的

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

????????resolveAsyncComponents?返回的是一個導航守衛函數,有標準的?to、from、next?參數。它的內部實現很簡單,利用了?flatMapComponents?方法從?matched?中獲取到每個組件的定義,判斷如果是異步組件,則執行異步組件加載邏輯,這塊和我們之前分析?Vue?加載異步組件很類似,加載成功后會執行?match.components[key] = resolvedDef?把解析好的異步組件放到對應的?components?上,并且執行?next?函數。

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

后續

? ??????第六步,在被激活的組件里調用?beforeRouteEnter

????????第七步,調用全局的?beforeResolve?守衛。

????????第八步,調用全局的?afterEach?鉤子。

????????那么至此我們把所有導航守衛的執行分析完畢了,我們知道路由切換除了執行這些鉤子函數,從表象上有 2 個地方會發生變化,一個是 url 發生變化,一個是組件發生變化。接下來我們分別介紹這兩塊的實現原理。

2、了解url的變化邏輯。

????????當我們點擊?router-link?的時候,實際上最終會執行?router.push(this.history.push),我們介紹一下hash History的push實現。

hash History的push

????????會先通過transitionTo做路徑切換,成功的回調會執行pushHash(route.fullPath)方法(url相關),handleScroll(滾動條相關)。

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

然后在?history?的初始化中,會設置一個監聽器,監聽歷史棧的變化:

當點擊瀏覽器返回按鈕的時候,如果已經有 url 被壓入歷史棧,則會觸發?popstate?事件,然后拿到當前要跳轉的?hash,執行?transtionTo?方法做一次路徑轉換。

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

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

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

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

把app實例的_route和路徑對應起來
history.listen
在?updateRoute?的時候執行?this.cb

????????也就是我們執行?transitionTo?方法最后執行?updateRoute?的時候會執行回調,然后會更新所有組件實例的?_route?值,所以說?$route?對應的就是當前的路由線路。

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容