回憶:
????????我們知道,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)。
? ??????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.matcher由match方法(根據傳入的路徑和當前的路徑,計算出新的路徑)和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?方法執行這個隊列。
? ??????第一步,在失活的組件里調用離開守衛。
????????flatMapComponents做的事情,通過從上面切換路徑時confirmTransition得到的deactive數組,去得到與組件相關的參數。然后調用作為參數傳入的fn函數,把前面得到的參數傳給fn函數。fn調用完畢即得到守衛列表。
? ? ? ? 第二步,調用用戶注冊的全局?beforeEach?守衛。
????????list是router實例的beforeHooks鉤子函數數組,fn是我們調用beforeEach時自定義回調函數。
? ? ? ? 第三步,extractUpdateHooks(updated)。調用所有重用的組件中定義的?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實現。
????????會先通過transitionTo做路徑切換,成功的回調會執行pushHash(route.fullPath)方法(url相關),handleScroll(滾動條相關)。
然后在?history?的初始化中,會設置一個監聽器,監聽歷史棧的變化:
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中:
????????也就是我們執行?transitionTo?方法最后執行?updateRoute?的時候會執行回調,然后會更新所有組件實例的?_route?值,所以說?$route?對應的就是當前的路由線路。
????????<router-view>?是支持嵌套的,回到?render?函數,其中定義了?depth?的概念,它表示?<router-view>?嵌套的深度。每個?<router-view>?在渲染的時候,執行如下邏輯: