VueRouter?原理解讀之初始化流程
1.1 核心概念
官方介紹
Vue Router 是 Vue.js 的官方路由。它與 Vue.js 核心深度集成,讓用 Vue.js 構(gòu)建單頁應(yīng)用變得輕而易舉。功能包括:
- 嵌套路由映射
- 動(dòng)態(tài)路由選擇
- 模塊化、基于組件的路由配置
- 路由參數(shù)、查詢、通配符
- 展示由 Vue.js 的過渡系統(tǒng)提供的過渡效果
- 細(xì)致的導(dǎo)航控制
- 自動(dòng)激活 CSS 類的鏈接
- HTML5 history 模式或 hash 模式
- 可定制的滾動(dòng)行為
- URL 的正確編碼
使用與閱讀源碼的必要性
現(xiàn)代工程化的前端項(xiàng)目只要使用到 Vue.js 框架,基本都是逃離不了如何對(duì) SPA 路由跳轉(zhuǎn)的處理,而 VueRouter 作為一個(gè)成熟、優(yōu)秀的前端路由管理庫也是被業(yè)界廣泛推薦和使用,因此對(duì) Vue 開發(fā)者來講,深入底層的了解使用 VueRouter ,學(xué)習(xí)其實(shí)現(xiàn)原理是很有必要的。隨著不斷對(duì) VueRouter 的深度使用,一方面就是在實(shí)踐當(dāng)中可能遇到一些需要額外定制化處理的場(chǎng)景,像捕獲一些異常上報(bào)、處理路由緩存等場(chǎng)景需要我們對(duì) VueRouter 有著一定程度的熟悉才能更好的處理;另一方面則是業(yè)余外的學(xué)習(xí)、拓展自身能力的一個(gè)渠道,通過對(duì)源碼的閱讀理解能夠不斷開拓自己的知識(shí)面以及提升自己的 CR 水平還有潛移默化當(dāng)中的一些技術(shù)設(shè)計(jì)、架構(gòu)能力等。
1.2 基本使用
路由配置與項(xiàng)目引入
// 1. 定義相關(guān)路由視圖組件
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 2. 定義相關(guān)路由路徑等路由配置
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// 3. 通過 createRouter 方法傳入路由配置參數(shù)進(jìn)行路由對(duì)象創(chuàng)建
const router = VueRouter.createRouter({
// 4. 選擇路由能力的模式進(jìn)行初始化創(chuàng)建
history: VueRouter.createWebHashHistory(),
routes,
})
// 5. 調(diào)用 Vue.js 對(duì)象的 use 方法來對(duì) VueRouter 路由對(duì)象進(jìn)行初始化安裝處理
const app = Vue.createApp({})
app.use(router)
app.mount('#app')
路由組件使用
<router-view class="view left-sidebar" name="LeftSidebar"></router-view> <router-view class="view main-content"> <router-link to="/" reaplace>Home</router-link> </router-view> <router-view class="view right-sidebar" name="RightSidebar"></router-view>
跳轉(zhuǎn) api 調(diào)用
export default {
methods: {
redirectHome() {
this.$router.replace('/')
},
goToAbout() {
this.$router.push('/about')
},
},
}
當(dāng)然 VueRouter 的能力和相關(guān)的 api 肯定不僅僅是這塊基礎(chǔ)使用這么簡(jiǎn)單,具體其他相關(guān)更高級(jí)、深層次的 api 與用法請(qǐng)參考官方文檔等。因?yàn)槭?VueRouter 源碼分析和原理解析的系列文章,受眾最好是有一定的使用經(jīng)驗(yàn)的開發(fā)者甚至是深度使用者更好,因此可能會(huì)存在一點(diǎn)門檻,這塊需要閱讀者自行斟酌。
2.1 createRouter 初始化入口分析
大致流程
將createRouter這個(gè)方法的代碼簡(jiǎn)單化后如下:能夠看到createRouter方法在內(nèi)部定義了一個(gè) router 對(duì)象并在這個(gè)對(duì)象上掛載一些屬性與方法,最后將這個(gè) router 對(duì)象作為函數(shù)返回值進(jìn)行返回。
// vuejs:router/packages/router/src/router.ts
export function createRouter(options: RouterOptions): Router {
const matcher = createRouterMatcher(options.routes, options)
const parseQuery = options.parseQuery || originalParseQuery
const stringifyQuery = options.stringifyQuery || originalStringifyQuery
const routerHistory = options.history
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(START_LOCATION_NORMALIZED)
let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
function addRoute(parentOrRoute: RouteRecordName | RouteRecordRaw, route?: RouteRecordRaw) {
// ··· ···
}
function removeRoute(name: RouteRecordName) {
// ··· ···
}
function getRoutes() {
// ··· ···
}
function hasRoute(name: RouteRecordName): boolean {
// ··· ···
}
function resolve(rawLocation: Readonly<RouteLocationRaw>, currentLocation?: RouteLocationNormalizedLoaded): RouteLocation & { href: string } {
// ··· ···
}
function push(to: RouteLocationRaw) {
// ··· ···
}
function replace(to: RouteLocationRaw) {
// ··· ···
}
let readyHandlers = useCallbacks<OnReadyCallback>()
let errorHandlers = useCallbacks<_ErrorHandler>()
function isReady(): Promise<void> {
// ··· ···
}
const go = (delta: number) => routerHistory.go(delta)
const router: Router = {
// ··· ···
}
return router
}Router 對(duì)象的定義:
從上面的createRouter方法定義當(dāng)中能夠知道,返回的是一個(gè) Router 的對(duì)象,我們首先來看下 Router 對(duì)象的屬性定義:返回項(xiàng) Router 是創(chuàng)建出來的全局路由對(duì)象,包含了路由的實(shí)例和常用的內(nèi)置操作跳轉(zhuǎn)、獲取信息等方法。
// vuejs:router/packages/router/src/router.ts
export interface Router {
// 當(dāng)前路由
readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
// VueRouter 路由配置項(xiàng)
readonly options: RouterOptions
// 是否監(jiān)聽中
listening: boolean
// 動(dòng)態(tài)增加路由項(xiàng)
addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
addRoute(route: RouteRecordRaw): () => void
// 動(dòng)態(tài)刪除路由項(xiàng)
removeRoute(name: RouteRecordName): void
// 根據(jù)路由配置的 name 判斷是否有該路由
hasRoute(name: RouteRecordName): boolean
// 獲取當(dāng)前所有路由數(shù)據(jù)
getRoutes(): RouteRecord[]
// 當(dāng)前網(wǎng)頁的標(biāo)準(zhǔn)路由 URL 地址
resolve(
to: RouteLocationRaw,
currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string }
// 路由導(dǎo)航跳轉(zhuǎn)操作方法
push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
back(): ReturnType<Router['go']>
forward(): ReturnType<Router['go']>
go(delta: number): void
// 全局守衛(wèi)
beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
afterEach(guard: NavigationHookAfter): () => void
// 路由錯(cuò)誤回調(diào)處理
onError(handler: _ErrorHandler): () => void
// 路由是否已經(jīng)完成初始化導(dǎo)航
isReady(): Promise<void>
// 2.x 版本的 Vue.js 引入 VueRouter 時(shí)候自動(dòng)調(diào)用
install(app: App): void
}
創(chuàng)建路由流程概括
在createRouter的方法當(dāng)中,我們能夠看到該方法其實(shí)主要是做了三件事情,
- 使用
createRouterMatcher創(chuàng)建頁面路由匹配器; - 創(chuàng)建和處理守衛(wèi)相關(guān)方法;
- 定義其他相關(guān)的 router 對(duì)象的屬性和內(nèi)置的方法。

接下來我們來具體分析里面的三個(gè)大步驟到底分別處理做了些什么事情呢。
2.2 創(chuàng)建頁面路由匹配器
在前面的簡(jiǎn)單分析當(dāng)中,在createRouter的第一步就是根據(jù)配置的路由 options 配置調(diào)用createRouterMacher方法創(chuàng)建頁面路由匹配器matcher對(duì)象。
// vuejs:router/packages/router/src/matcher/index.ts
export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher {
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
globalOptions
)
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// ··· ···
}
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
// ··· ···
}
function getRoutes() {
// ··· ···
}
function resolve(location: Readonly<MatcherLocationRaw>, currentLocation: Readonly<MatcherLocation>): MatcherLocation {
// ··· ···
}
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
對(duì)于 VueRouter 整個(gè)庫來看這個(gè)頁面路由匹配器matcher對(duì)象是占據(jù)了比較大的一個(gè)模塊,這篇文章主要是對(duì)路由初始化這部分流程邏輯進(jìn)行分析;也因?yàn)槠脑颍@里就先簡(jiǎn)單從較上帝的視角來看看這個(gè)大概的流程。
- 方法內(nèi)聲明了
matchers與matcherMap兩個(gè)內(nèi)部變量存放經(jīng)過解析的路由配置信息; - 創(chuàng)建相關(guān)的路由匹配器的操作方法:addRoute, resolve, removeRoute, getRoutes, getRecordMatcher;
- 根據(jù)調(diào)用
createRouter方法傳入的參數(shù)遍歷調(diào)用addRoute初始化路由匹配器數(shù)據(jù); - 最后方法返回一個(gè)對(duì)象,并且將該些操作方法掛載到該對(duì)象屬性當(dāng)中;
2.3 創(chuàng)建初始化導(dǎo)航守衛(wèi)
useCallbacks 實(shí)現(xiàn)訂閱發(fā)布中心
// vuejs:router/packages/router/src/utils/callbacks.ts
export function useCallbacks<T>() {
let handlers: T[] = []
function add(handler: T): () => void {
handlers.push(handler)
return () => {
const i = handlers.indexOf(handler)
if (i > -1) handlers.splice(i, 1)
}
}
function reset() {
handlers = []
}
return {
add,
list: () => handlers,
reset,
}
}
這里首先簡(jiǎn)單分析下useCallbackhooks 的方法,其實(shí)就是利用閉包創(chuàng)建一個(gè)內(nèi)部的回調(diào)函數(shù)數(shù)組變量,然后再創(chuàng)建和返回一個(gè)對(duì)象,對(duì)象有三個(gè)屬性方法,分別是add添加一個(gè)回調(diào)執(zhí)行函數(shù)并且返回一個(gè)清除當(dāng)前回調(diào)函數(shù)的一個(gè)函數(shù),list獲取回調(diào)函數(shù)數(shù)組,reset清空當(dāng)前所有回調(diào)方法。是一個(gè)簡(jiǎn)單的標(biāo)準(zhǔn)的發(fā)布訂閱中心處理的實(shí)現(xiàn)。
創(chuàng)建相關(guān)的導(dǎo)航守衛(wèi)
// vuejs:router/packages/router/src/router.ts
import { useCallbacks } from './utils/callbacks'
export function createRouter(options: RouterOptions): Router {
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
// ··· ···
const router: Router = {
// ··· ···
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
// ··· ···
}
return router
}
通過上面經(jīng)過節(jié)選的相關(guān)導(dǎo)航守衛(wèi)處理的部分代碼,能夠看到其實(shí) VueRouter 在createRouter里面對(duì)于全局導(dǎo)航守衛(wèi)的處理還是比較簡(jiǎn)單通俗易懂的,通過useCallbackshooks 方法分別創(chuàng)建了beforeEach、beforeResolve、afterEach三個(gè)對(duì)應(yīng)的全局導(dǎo)航守衛(wèi)的回調(diào)處理對(duì)象(這里主要是初始化創(chuàng)建相關(guān)的訂閱發(fā)布的發(fā)布者對(duì)象);
- beforeEach:在任何導(dǎo)航路由之前執(zhí)行;
- beforeResolve:在導(dǎo)航路由解析確認(rèn)之前執(zhí)行;
- afterEach:在任何導(dǎo)航路由確認(rèn)跳轉(zhuǎn)之后執(zhí)行;
因?yàn)槠鶈栴},VueRouter 的守衛(wèi)其實(shí)不僅僅這些,后面會(huì)梳理整理相關(guān)的守衛(wèi)處理,訂閱回調(diào)的發(fā)布執(zhí)行等相關(guān)邏輯作為一篇守衛(wèi)相關(guān)的文章單獨(dú)編寫,這里就不講述過多的東西了。
2.4 定義掛載相關(guān) Router 方法
在 router 對(duì)象上面還掛載了不少方法,接下來我們來簡(jiǎn)單分析下這些方法的實(shí)現(xiàn)邏輯。
路由配置相關(guān) addRoute、removeRoute、hasRoute、getRoutes
// vuejs:router/packages/router/src/router.ts
export function createRouter(options: RouterOptions): Router {
// ··· ···
// 添加路由項(xiàng) - 兼容處理參數(shù)后使用 addRoute 進(jìn)行添加路由項(xiàng)
function addRoute(
parentOrRoute: RouteRecordName | RouteRecordRaw,
route?: RouteRecordRaw
) {
let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined
let record: RouteRecordRaw
if (isRouteName(parentOrRoute)) {
parent = matcher.getRecordMatcher(parentOrRoute)
record = route!
} else {
record = parentOrRoute
}
return matcher.addRoute(record, parent)
}
// 刪除路由項(xiàng) - 根據(jù)路由名 name 調(diào)用 getRecordMatcher 獲取路由項(xiàng),如果找到記錄則調(diào)用 removeRoute 刪除該路由項(xiàng)
function removeRoute(name: RouteRecordName) {
const recordMatcher = matcher.getRecordMatcher(name)
if (recordMatcher) {
matcher.removeRoute(recordMatcher)
}
}
// 獲取當(dāng)前所有路由項(xiàng) -
function getRoutes() {
return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
// 是否含有路由 - 根據(jù)路由名 name 調(diào)用 getRecordMatcher 獲取路由項(xiàng)
function hasRoute(name: RouteRecordName): boolean {
return !!matcher.getRecordMatcher(name)
}
const router: Router = {
addRoute,
removeRoute,
hasRoute,
getRoutes,
}
return router
}
這部分是對(duì)路由配置的操作方法的實(shí)現(xiàn),但是看下來邏輯并不難,都是比較清晰。在前面的章節(jié)當(dāng)中我們對(duì)頁面路由匹配器matcher進(jìn)行了簡(jiǎn)單的分析,知道了在createRouterMacher方法返回的這個(gè)對(duì)象包含著 addRoute, resolve, removeRoute, getRoutes, getRecordMatcher 這些操作方法,并且內(nèi)部維護(hù)著路由匹配器的信息。
這部分路由操作方法就是利用這個(gè)createRouterMacher所創(chuàng)建的頁面路由匹配器matcher掛載的方法來實(shí)現(xiàn)的。
路由操作相關(guān) push、replace、go、back、forward
// vuejs:router/packages/router/src/router.ts
export function createRouter(options: RouterOptions): Router {
const routerHistory = options.history
function push(to: RouteLocationRaw) {
return pushWithRedirect(to)
}
function replace(to: RouteLocationRaw) {
return push(assign(locationAsObject(to), { replace: true }))
}
const go = (delta: number) => routerHistory.go(delta)
const router: Router = {
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
}
return router
}
先來看比較簡(jiǎn)單的go、back、forward這幾個(gè)方法,這幾個(gè)方法都是直接調(diào)用路由歷史對(duì)象的go方法,底層其實(shí)就是調(diào)用瀏覽器的 history 提供的 go 跳轉(zhuǎn) api,這個(gè)路由跳轉(zhuǎn)的會(huì)在另外的專門講解路由模式的文章當(dāng)中講述,這里就不展開詳細(xì)講述了。
而另外的push與replace方法能從上面看到replace其實(shí)就是調(diào)用push的方法,都是使用pushWithRedirect處理跳轉(zhuǎn),僅一個(gè) replace 的參數(shù)不同。接著我們來分析pushWithRedirect這個(gè)方法。
// vuejs:router/packages/router/src/router.ts
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
// 定義相關(guān)的路由變量屬性
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
const from = currentRoute.value
const data: HistoryState | undefined = (to as RouteLocationOptions).state
const force: boolean | undefined = (to as RouteLocationOptions).force
const replace = (to as RouteLocationOptions).replace === true
// 調(diào)用 handleRedirectRecord 判斷目標(biāo)跳轉(zhuǎn)是否需要重定向 -- 就是這個(gè) to 要跳轉(zhuǎn)的路由的 redirect 屬性是否為 true
const shouldRedirect = handleRedirectRecord(targetLocation)
// 若需要重定向則遞歸調(diào)用 pushWithRedirect 方法
if (shouldRedirect)
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), {
state:
typeof shouldRedirect === 'object'
? assign({}, data, shouldRedirect.state)
: data,
force,
replace,
}),
redirectedFrom || targetLocation
)
// 后續(xù)邏輯是非重定向的路由
const toLocation = targetLocation as RouteLocationNormalized
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void | undefined
// 不設(shè)置強(qiáng)制跳轉(zhuǎn)并且目標(biāo)跳轉(zhuǎn)路由地址與當(dāng)前路由地址一樣的情況下定義相關(guān)的跳轉(zhuǎn)異常以及頁面的滾動(dòng),后續(xù)使用 Promise.resolve 處理異常
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
failure = createRouterError<NavigationFailure>(
ErrorTypes.NAVIGATION_DUPLICATED,
{ to: toLocation, from }
)
handleScroll(from, from, true, false)
}
// 判斷前面的執(zhí)行邏輯是否存在跳轉(zhuǎn)異常或者錯(cuò)誤,如果沒有跳轉(zhuǎn)異常錯(cuò)誤則執(zhí)行 navigate 這個(gè)Promise方法
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.catch((error: NavigationFailure | NavigationRedirectError) => {
// 處理跳轉(zhuǎn)中出現(xiàn)異常后捕獲相關(guān)的錯(cuò)誤并對(duì)不同錯(cuò)誤進(jìn)行處理
isNavigationFailure(error)
? isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT) ? error : markAsReady(error)
: triggerError(error, toLocation, from)
}).then((failure: NavigationFailure | NavigationRedirectError | void) => {
// 跳轉(zhuǎn)調(diào)用 navigate 后的處理
if (failure) {
// 處理跳轉(zhuǎn)和執(zhí)行navigate過程當(dāng)中的錯(cuò)誤異常
} else {
failure = finalizeNavigation(
toLocation as RouteLocationNormalizedLoaded,
from,
true,
replace,
data
)
}
triggerAfterEach(toLocation as RouteLocationNormalizedLoaded, from, failure)
return failure
})
}
在對(duì)pushWithRedirect方法進(jìn)行分析后知道這個(gè)方法是對(duì)頁面的重定向進(jìn)行專門處理,處理完成后會(huì)調(diào)用navigate這個(gè) Promise 方法。
在pushWithRedirect方法的邏輯末尾中,一系列的邏輯處理完成后才會(huì)調(diào)用finalizeNavigation與triggerAfterEach進(jìn)行導(dǎo)航切換路由的確認(rèn)與相關(guān)導(dǎo)航守衛(wèi)鉤子的收尾執(zhí)行。
我們先來看下這個(gè)navigate方法的邏輯:
// vuejs:router/packages/router/src/router.ts
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
let guards: Lazy<any>[]
// extractChangingRecords 方法會(huì)根據(jù)(to/目標(biāo)跳轉(zhuǎn)路由)和(from/離開的路由)到路由匹配器matcher里匹配對(duì)應(yīng)的路由項(xiàng)并且將結(jié)果存到3個(gè)數(shù)組中
// leavingRecords:當(dāng)前即將離開的路由
// updatingRecords:要更新的路由
// enteringRecords:要跳轉(zhuǎn)的目標(biāo)路由
const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from)
// extractComponentsGuards 方法用于提取不同的路由鉤子,第二個(gè)參數(shù)可傳值:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
guards = extractComponentsGuards(
leavingRecords.reverse(), // 這里因?yàn)閂ue組件銷毀順序是從子到父,因此使用reverse反轉(zhuǎn)數(shù)組保證子路由鉤子順序在前
'beforeRouteLeave',
to,
from
)
// 將失活組件的 onBeforeRouteLeave 導(dǎo)航守衛(wèi)都提取并且添加到 guards 里
for (const record of leavingRecords) {
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
// 檢查當(dāng)前正在處理的目標(biāo)跳轉(zhuǎn)路由和 to 是否相同路由,如果不是的話則拋除 Promise 異常
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from)
guards.push(canceledNavigationCheck)
return (
runGuardQueue(guards) // 作為啟動(dòng) Promise 開始執(zhí)行失活組件的 beforeRouteLeave 鉤子
.then(() => {
// 執(zhí)行全局 beforeEach 鉤子
guards = []
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 執(zhí)行重用組件的 beforeRouteUpdate 鉤子
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 執(zhí)行全局 beforeEnter 鉤子
guards = []
for (const record of to.matched) {
if (record.beforeEnter && !from.matched.includes(record)) {
if (isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
} else {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 清除已經(jīng)存在的 enterCallbacks, 因?yàn)檫@些已經(jīng)在 extractComponentsGuards 里面添加
to.matched.forEach(record => (record.enterCallbacks = {}))
// 執(zhí)行被激活組件的 beforeRouteEnter 鉤子
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 執(zhí)行全局 beforeResolve 鉤子
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.catch(err =>
// 處理在過程當(dāng)中拋除的異常或者取消導(dǎo)航跳轉(zhuǎn)操作
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
)
}
這個(gè)navigate方法主要就是使用runGuardQueue封裝即將要執(zhí)行的相關(guān)的一系列導(dǎo)航守衛(wèi)的鉤子回調(diào),這塊封裝的內(nèi)部處理邏輯還是比較復(fù)雜的,這里因?yàn)槠鶈栴}我們這塊還是以了解知道在這塊主要的流程邏輯,后續(xù)也會(huì)對(duì)路由守衛(wèi)這塊專門詳細(xì)的進(jìn)行源碼閱讀分析并且編寫相關(guān)的文章。
接著我們?cè)賮砜聪?code>finalizeNavigation是如何進(jìn)行前端路由跳轉(zhuǎn)的:
// vuejs:router/packages/router/src/router.ts
function finalizeNavigation(
toLocation: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
replace?: boolean,
data?: HistoryState
): NavigationFailure | void {
// 檢查是否需要取消目標(biāo)路由的跳轉(zhuǎn) -- 判斷目標(biāo)跳轉(zhuǎn)的路由和當(dāng)前處理的跳轉(zhuǎn)路由是否不同,不同則取消路由跳轉(zhuǎn)
const error = checkCanceledNavigation(toLocation, from)
if (error) return error
const isFirstNavigation = from === START_LOCATION_NORMALIZED
const state = !isBrowser ? {} : history.state
if (isPush) {
// 處理路由跳轉(zhuǎn),判斷根據(jù) replace 參數(shù)判斷使用 replace 還是 push 的跳轉(zhuǎn)形式
if (replace || isFirstNavigation)
routerHistory.replace(
toLocation.fullPath,
assign({ scroll: isFirstNavigation && state && state.scroll, }, data)
)
else routerHistory.push(toLocation.fullPath, data)
}
currentRoute.value = toLocation
// 處理設(shè)置頁面的滾動(dòng)
handleScroll(toLocation, from, isPush, isFirstNavigation)
markAsReady()
}
在邏輯當(dāng)中能夠看到調(diào)用 VueRouter 的push和replace方法進(jìn)行跳轉(zhuǎn)時(shí)候會(huì)調(diào)用這個(gè)routerHistory路由歷史對(duì)象對(duì)應(yīng)的同名 api 進(jìn)行跳轉(zhuǎn)處理,但是受限于文章的篇幅,這塊先劇透這里底層路由跳轉(zhuǎn)邏輯使用的是瀏覽器的history的pushState與replaceState這兩個(gè) api,后續(xù)很快就會(huì)推出相關(guān)的前端路由能力實(shí)現(xiàn)原理的剖析文章,大家敬請(qǐng)關(guān)注。
鉤子相關(guān) beforeEach、beforeResolve、afterEach、onError
// vuejs:router/packages/router/src/router.ts
import { useCallbacks } from './utils/callbacks'
export function createRouter(options: RouterOptions): Router {
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
let errorHandlers = useCallbacks<_ErrorHandler>()
// ··· ···
const router: Router = {
// ··· ···
// 拋出相關(guān)導(dǎo)航守衛(wèi)或鉤子對(duì)應(yīng)的新增訂閱回調(diào)事件
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
onError: errorHandlers.add,
// ··· ···
}
return router
}這塊鉤子的大部分的邏輯已經(jīng)在前面創(chuàng)建初始化導(dǎo)航守衛(wèi)這個(gè)小章節(jié)里面已經(jīng)講述了,因此這塊定義掛載拋出鉤子事件的邏輯其實(shí)也較明朗了:
- 使用
useCallbacks方法定義相關(guān)鉤子的訂閱發(fā)布中心對(duì)象; createRouter方法返回的 router 對(duì)象定義增加對(duì)應(yīng)的訂閱事件add,這樣子在定義路由時(shí)候配置傳入的鉤子回調(diào)函數(shù)則自動(dòng)被添加到對(duì)應(yīng)鉤子的訂閱回調(diào)列表當(dāng)中。
注冊(cè)安裝 install 方法:
熟悉 Vue.js 技術(shù)棧的同學(xué)都基本知道這個(gè)install方法會(huì)在插件庫引入 Vue 項(xiàng)目當(dāng)中的Vue.use方法當(dāng)中被調(diào)用,這里 VueRouter 的 install 也不例外,同樣會(huì)在下面的 use 方法的調(diào)用當(dāng)中被 Vue.js 內(nèi)部所調(diào)用到。
const app = Vue.createApp({})
app.use(router)
接下來我們真正進(jìn)入到對(duì)install方法的分析當(dāng)中去:
// vuejs:router/packages/router/src/router.ts
install(app: App) {
const router = this
// 注冊(cè) VueRouter 的路由視圖和鏈接組件為 Vue 全局組件
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
// 在全局 Vue this 對(duì)象上掛載 $router 屬性為路由對(duì)象
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
// 判斷瀏覽器環(huán)境并且還沒執(zhí)行初始化路由跳轉(zhuǎn)時(shí)候先進(jìn)行一次 VueRouter 的 push 路由跳轉(zhuǎn)
if (
isBrowser &&
!started &&
currentRoute.value === START_LOCATION_NORMALIZED
) {
started = true
push(routerHistory.location).catch(err => {
if (__DEV__) warn('Unexpected error when starting the router:', err)
})
}
// 使用 computed 計(jì)算屬性來創(chuàng)建一個(gè)記錄當(dāng)前已經(jīng)被激活過的路由的對(duì)象 reactiveRoute
const reactiveRoute = {} as {
[k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
RouteLocationNormalizedLoaded[k]
>
}
for (const key in START_LOCATION_NORMALIZED) {
reactiveRoute[key] = computed(() => currentRoute.value[key])
}
// 全局注入相關(guān)的一些路由相關(guān)的變量
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
// 重寫覆蓋 Vue 項(xiàng)目的卸載鉤子函數(shù) - 執(zhí)行相關(guān)屬性的卸載并且調(diào)用原本 Vue.js 的卸載 unmount 事件
const unmountApp = app.unmount
installedApps.add(app)
app.unmount = function () {
installedApps.delete(app)
if (installedApps.size < 1) {
pendingLocation = START_LOCATION_NORMALIZED
removeHistoryListener && removeHistoryListener()
removeHistoryListener = null
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp() // 執(zhí)行原本 Vue 項(xiàng)目當(dāng)中設(shè)置的 unmount 鉤子函數(shù)
}
}
在install方法當(dāng)中主要邏輯還是對(duì)一些全局的屬性和相關(guān)的組件、變量以及鉤子事件進(jìn)行一個(gè)初始化處理操作:
這塊的邏輯可能有些操作在一開始初始化時(shí)候可能看不太懂為啥要這樣處理,后面會(huì)繼續(xù)推出 VueRouter 系列的源碼解析,到時(shí)候會(huì)回來回顧這塊的一些 install 引入安裝路由庫時(shí)候里面的一些操作與源碼邏輯。
總結(jié):
至此,VueRouter 這個(gè)前端路由庫的初始化流程createRouter就簡(jiǎn)單的分析完成了,這篇初始化的源碼解析的文章更多的像是領(lǐng)入門的流程概述簡(jiǎn)析。
雖然說初始化主要做了前面講述的三個(gè)步驟:創(chuàng)建頁面路由匹配器、導(dǎo)航守衛(wèi)、初始化 router 對(duì)象并且返回。但是這三件事情當(dāng)中其實(shí)還是有著不少的處理細(xì)節(jié),里面還牽涉了不少其他功能模塊的實(shí)現(xiàn),一開始可能還只能大概通過上帝模式去俯瞰這個(gè)初始化的流程,可能僅僅留有個(gè)印象(這里因?yàn)槠鶈栴}未能夠各個(gè)方面都進(jìn)行很詳細(xì)的講解,后續(xù)也會(huì)沿著這些伏筆線索不斷推出相關(guān)的源碼原理解析文章),開篇反而可能存在著一定程度的心智負(fù)擔(dān)。但是相信跟隨著后面的一系列文章,應(yīng)該能夠?qū)⑾到y(tǒng)串聯(lián)起來,對(duì) VueRouter 的實(shí)現(xiàn)有更完整的認(rèn)知。
相關(guān)的參考資料
- Vue Router 官方文檔:router.vuejs.org/
以上就是VueRouter 原理解讀 - 初始化流程的詳細(xì)內(nèi)容,更多關(guān)于VueRouter 初始化流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue2.0在沒有dev-server.js下的本地?cái)?shù)據(jù)配置方法
這篇文章主要介紹了vue2.0在沒有dev-server.js下的本地?cái)?shù)據(jù)配置方法的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-02-02
Vue實(shí)現(xiàn)預(yù)覽文件(Word/Excel/PDF)功能的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何通過Vue實(shí)現(xiàn)預(yù)覽文件(Word/Excel/PDF)的功能,文中的實(shí)現(xiàn)步驟講解詳細(xì),需要的小伙伴可以參考一下2023-03-03
Vue3項(xiàng)目中reset.scss模板使用導(dǎo)入
這篇文章主要為大家介紹了Vue3項(xiàng)目中reset.scss模板使用導(dǎo)入示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
利用Vue模擬實(shí)現(xiàn)element-ui的分頁器效果
這篇文章主要為大家詳細(xì)介紹了如何利用Vue模擬實(shí)現(xiàn)element-ui的分頁器效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動(dòng)手嘗試一下2022-11-11
Vue.js 利用v-for中的index值實(shí)現(xiàn)隔行變色
這篇文章主要介紹了Vue.js 利用v-for中的index值實(shí)現(xiàn)隔行變色效果,首先定義好樣式,利用v-for中的index值,然后綁定樣式來實(shí)現(xiàn)隔行變色,需要的朋友可以參考下2018-08-08
Vue+thinkphp5.1+axios實(shí)現(xiàn)文件上傳
這篇文章主要為大家詳細(xì)介紹了Vue+thinkphp5.1+axios實(shí)現(xiàn)文件上傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
Vue中computed(計(jì)算屬性)和watch(監(jiān)聽屬性)的用法及區(qū)別說明
這篇文章主要介紹了Vue中computed(計(jì)算屬性)和watch(監(jiān)聽屬性)的用法及區(qū)別說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07

