前言
【vue-router源码】系列文章将带你从0开始理解vue-router
的具体实现。该系列文章源码参考vue-router v4.0.15
。
源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解vue-router
的根本应用,如果你没有应用过的话,可通过vue-router官网学习下。
该篇文章将剖析router.push
和router.replace
的实现,通过该文章你会理解一个略微残缺的导航解析流程。
应用
应用router.push
办法导航到不同的 URL。这个办法会向history
栈增加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。
应用router.replace
办法导航到不同的 URL。这个办法会在history
栈替换历史记录。
router.push('/search?name=pen')router.push({ path: '/search', query: { name: 'pen' } })router.push({ name: 'search', query: { name: 'pen' } })// 以上三种形式是等效的。router.replace('/search?name=pen')router.replace({ path: '/search', query: { name: 'pen' } })router.replace({ name: 'search', query: { name: 'pen' } })// 以上三种形式是等效的。
push
push
办法接管一个to
参数,示意要跳转的路由,它能够是个字符串,也能够是个对象。在push
办法中调用了一个pushWithRedirect
函数,并返回其后果。
function push(to: RouteLocationRaw | RouteLocation) { return pushWithRedirect(to)}
pushWithRedirect
接管两个参数:to
、redirectedFrom
,并返回pushWithRedirect
的后果。其中to
是要跳转到的路由,redirectedFrom
代表to
是从哪个路由重定向来的,如果屡次重定向,它只是最后重定向的那个路由。
function pushWithRedirect( to: RouteLocationRaw | RouteLocation, redirectedFrom?: RouteLocation): Promise<NavigationFailure | void | undefined> { // ...}
因为要到的to
中可能存在重定向,所以pushWithRedirect
中首先要解决重定向:当to
中存在重定向时,递归调用pushWithRedirect
。
// 将to解决为规范化的路由const targetLocation: RouteLocation = (pendingLocation = resolve(to))// 以后路由const from = currentRoute.value// 应用 History API(history.state) 保留的状态const data: HistoryState | undefined = (to as RouteLocationOptions).state// force代表强制触发导航,即便与以后地位雷同const force: boolean | undefined = (to as RouteLocationOptions).force// replace代表是否替换以后历史记录const replace = (to as RouteLocationOptions).replace === true// 获取要重定向的记录const shouldRedirect = handleRedirectRecord(targetLocation)// 如果须要重定向,递归调用pushWithRedirect办法if (shouldRedirect) return pushWithRedirect( assign(locationAsObject(shouldRedirect), { state: data, force, replace, }), // 重定向的根起源 redirectedFrom || targetLocation )
handleRedirectRecord
函数的实现:
function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void { // 找到匹配的路由,to.matched中的路由程序是父路由在子路由后面,所以最初一个路由是咱们的最终路由 const lastMatched = to.matched[to.matched.length - 1] // 如果路由存在redirect if (lastMatched && lastMatched.redirect) { const { redirect } = lastMatched // 如果redirect是函数,须要执行函数 let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect // 如果newTargetLocation是string if (typeof newTargetLocation === 'string') { // 如果newTargetLocation中存在?或#,须要将newTargetLocation解析成一个LocationNormalized类型的对象 newTargetLocation = newTargetLocation.includes('?') || newTargetLocation.includes('#') ? (newTargetLocation = locationAsObject(newTargetLocation)) : { path: newTargetLocation } // 设置params为一个空对象 newTargetLocation.params = {} } // 如果newTargetLocation中没有path和name属性,则无奈找到重定向的路由,开发环境下进行提醒 if ( __DEV__ && !('path' in newTargetLocation) && !('name' in newTargetLocation) ) { warn( `Invalid redirect found:\n${JSON.stringify( newTargetLocation, null, 2 )}\n when navigating to "${ to.fullPath }". A redirect must contain a name or path. This will break in production.` ) throw new Error('Invalid redirect') } return assign( { query: to.query, hash: to.hash, params: to.params, }, newTargetLocation ) }}
解决完重定向后,接下来会检测要跳转到的路由和以后路由是否为同一个路由,如果是同一个路由并且不强制跳转,会创立一个失败函数赋给failure
,而后解决滚动行为。
const toLocation = targetLocation as RouteLocationNormalized// 设置重定向的起源toLocation.redirectedFrom = redirectedFromlet failure: NavigationFailure | void | undefined// 如果要跳转到的路由与以后路由统一并且不强制跳转if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) { // 创立一个错误信息,该错误信息代表反复的导航 failure = createRouterError<NavigationFailure>( ErrorTypes.NAVIGATION_DUPLICATED, { to: toLocation, from } ) // 解决滚动行为 handleScroll( from, from, true, false )}
对于handleScroll
的实现如下:首先从options
中找到scrollBehavior
选项,如果不是浏览器环境或不存在scrollBehavior
,返回一个Promise
对象。相同,获取滚动地位(依据历史记录中的position和path获取),而后在下一次DOM刷新后,执行定义的滚动行为函数,滚动行为函数执行完后,将滚动行为函数后果作为最终的滚动地位将页面滚动到指定地位。
function handleScroll( to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded, isPush: boolean, isFirstNavigation: boolean): Promise<any> { const { scrollBehavior } = options if (!isBrowser || !scrollBehavior) return Promise.resolve() // 获取滚动地位 const scrollPosition: _ScrollPositionNormalized | null = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) || ((isFirstNavigation || !isPush) && (history.state as HistoryState) && history.state.scroll) || null // 下一次DOM更新后触发滚动行为,滚动行为执行完后,滚动到指定地位 return nextTick() .then(() => scrollBehavior(to, from, scrollPosition)) .then(position => position && scrollToPosition(position)) .catch(err => triggerError(err, to, from))}export function getScrollKey(path: string, delta: number): string { // history.state.position记录着以后路由在历史记录中的地位,该地位从0开始 const position: number = history.state ? history.state.position - delta : -1 // key值为 在历史记录中的地位+path return position + path}export function getSavedScrollPosition(key: string) { // 依据key值查找滚动地位 const scroll = scrollPositions.get(key) // 查完后,删除对应记录 scrollPositions.delete(key) return scroll}
在pushWithRedirect
最初返回一个Promise
。如果有failure
,返回failure
。如果没有failure
则执行navigate(toLocation, from)
。
那么navigate
是做什么的呢?navigate
函数接管两个参数:to
、from
。
navigate
中首先调用了一个extractChangingRecords
函数,该函数的作用是将from
、to
所匹配到的路由别离存到三个数组中:from
、to
所共有的路由放入updatingRecords
(正在更新的路由)、from
独有的路由放入leavingRecords
(正要来到的路由)、to
独有的路由放入enteringRecords
(正在进入的新路由)。紧接着又调用了一个extractComponentsGuards
函数,用来获取组件内的beforeRouteLeave
钩子,留神extractComponentsGuards
函数只能获取应用beforeRouteLeave(){}
形式注册的函数,对于应用onBeforeRouteLeave
注册的函数须要独自解决。
const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from)guards = extractComponentsGuards( // 这里leavingRecords须要反转,因为matched中的程序是父路由在子路由前,当来到时,应先来到子路由再来到父路由 leavingRecords.reverse(), 'beforeRouteLeave', to, from)// 向guards中增加应用onBeforeRouteLeave形式注册的办法for (const record of leavingRecords) { record.leaveGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)) })}// 如果产生了新的导航canceledNavigationCheck能够帮忙跳过后续所有的导航const canceledNavigationCheck = checkCanceledNavigationAndReject.bind( null, to, from)guards.push(canceledNavigationCheck)
extractChangingRecords
的实现过程:如果to
和from
配配到的路由中有公共的,阐明这些路由在跳转过程中是更新操作,将其退出updatingRecords
中;如果是from
所匹配到独有的路由,阐明要来到这些路由,将它们放入leavingRecords
中;相同,如果to
匹配到的路由中,from
没有匹配到,阐明是新的路由,将它们放入enteringRecords
中。
function extractChangingRecords( to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded) { // 要来到的路由 const leavingRecords: RouteRecordNormalized[] = [] // 更新的路由 const updatingRecords: RouteRecordNormalized[] = [] // 要进入的新的路由(在from.matched中未呈现过) const enteringRecords: RouteRecordNormalized[] = [] const len = Math.max(from.matched.length, to.matched.length) for (let i = 0; i < len; i++) { const recordFrom = from.matched[i] if (recordFrom) { // 如果recordFrom在to.matched中存在,将recordFrom退出到updatingRecords,否则退出到leavingRecords中 if (to.matched.find(record => isSameRouteRecord(record, recordFrom))) updatingRecords.push(recordFrom) else leavingRecords.push(recordFrom) } const recordTo = to.matched[i] if (recordTo) { // 如果recordTo在from.matched中找不到,阐明是个新的路由,将recordTo退出到enteringRecords if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) { enteringRecords.push(recordTo) } } } return [leavingRecords, updatingRecords, enteringRecords]}
extractComponentsGuards
是专门用来从路由组件中提取钩子函数的。extractComponentsGuards
接管四个参数:matched
(从to
、from
中提取出的leavingRecords
、updatingRecords
、enteringRecords
之一)、guardType
(钩子类型,能够取的值beforeRouteEnter
、beforeRouteUpdate
、beforeRouteLeave
)、to
、from
。返回值是一个钩子函数列表。
export function extractComponentsGuards( matched: RouteRecordNormalized[], guardType: GuardType, to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded) { // 申明一个数组保留钩子函数 const guards: Array<() => Promise<void>> = [] for (const record of matched) { // 遍历路由对应的组件components for (const name in record.components) { let rawComponent = record.components[name] // 开发环境下进行提醒 if (__DEV__) { // 如果组件不存在或组件不是object和function,提醒不是无效的组件 if ( !rawComponent || (typeof rawComponent !== 'object' && typeof rawComponent !== 'function') ) { warn( `Component "${name}" in record with path "${record.path}" is not` + ` a valid component. Received "${String(rawComponent)}".` ) // 抛出谬误 throw new Error('Invalid route component') } else if ('then' in rawComponent) { // 如果应用import('./xxx.vue')的形式应用组件,进行提醒,并转为() => import('./xxx.vue') warn( `Component "${name}" in record with path "${record.path}" is a ` + `Promise instead of a function that returns a Promise. Did you ` + `write "import('./MyPage.vue')" instead of ` + `"() => import('./MyPage.vue')" ? This will break in ` + `production if not fixed.` ) const promise = rawComponent rawComponent = () => promise } else if ( (rawComponent as any).__asyncLoader && // warn only once per component !(rawComponent as any).__warnedDefineAsync ) { // 如果应用defineAsyncComponent()形式定义的组件,进行提醒 ;(rawComponent as any).__warnedDefineAsync = true warn( `Component "${name}" in record with path "${record.path}" is defined ` + `using "defineAsyncComponent()". ` + `Write "() => import('./MyPage.vue')" instead of ` + `"defineAsyncComponent(() => import('./MyPage.vue'))".` ) } } // 如果路由组件没有被挂载跳过update和leave钩子 if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue // 如果是个路由组件 // 路由组件须要满足:rawComponent是object || rawComponent有['displayName', 'props`、`__vccOpts`]中的任一属性 if (isRouteComponent(rawComponent)) { // __vccOpts是由vue-class-component增加的 const options: ComponentOptions = (rawComponent as any).__vccOpts || rawComponent const guard = options[guardType] // 向guards中增加一个异步函数 guard && guards.push(guardToPromiseFn(guard, to, from, record, name)) } else { // 能进入这个办法的示意rawComponent是个函数;例如懒加载() => import('./xx.vue');函数式组件() => h('div', 'HomePage') // 留神这个的分支只产生在调用beforeRouteEnter之前,后续过程不会进行该过程。 // 因为在调用beforeRouteEnter钩子之前,会进行异步路由组件的解析,一旦异步路由组件解析胜利,会将解析后的组件挂载至对应的components[name]下 // 执行rawComponent,例如懒加载() => import('./xx.vue');如果函数式组件未声明displayName也会进入此分支 let componentPromise: Promise< RouteComponent | null | undefined | void > = (rawComponent as Lazy<RouteComponent>)() // 对于函数式组件须要增加一个displayName属性,如果没有,进行提醒,并将componentPromise转为一个Promise if (__DEV__ && !('catch' in componentPromise)) { warn( `Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.` ) componentPromise = Promise.resolve(componentPromise as RouteComponent) } // 向guards中增加一个钩子函数,在这个钩子的执行过程中先解析异步路由组件,而后调用钩子函数 guards.push(() => componentPromise.then(resolved => { // 如果解析失败抛出谬误 if (!resolved) return Promise.reject( new Error( `Couldn't resolve component "${name}" at "${record.path}"` ) ) // 判断解析后的组件是否为esm,如果是esm,须要取resolved.default const resolvedComponent = isESModule(resolved) ? resolved.default : resolved // 应用解析完的组件替换对应的components[name] record.components[name] = resolvedComponent const options: ComponentOptions = (resolvedComponent as any).__vccOpts || resolvedComponent // 对应的组件内的钩子 const guard = options[guardType] // 钩子转promise,并执行 return guard && guardToPromiseFn(guard, to, from, record, name)() }) ) } } } return guards}
在navigate
函数最初会调用guards
中的钩子,并在beforeRouteLeave
钩子执行完后执行了一系列操作。其实在这里就体现了vue-router
中钩子的执行程序:
return ( runGuardQueue(guards) .then(() => { // 调用全局beforeEach钩子 guards = [] for (const guard of beforeGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .then(() => { // 获取组件中的beforeRouteUpdate钩子,以beforeRouteUpdate() {}形式申明 guards = extractComponentsGuards( updatingRecords, 'beforeRouteUpdate', to, from ) // 以onBeforeRouteUpdate注册的 for (const record of updatingRecords) { record.updateGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)) }) } guards.push(canceledNavigationCheck) // 调用beforeRouteUpdate钩子 return runGuardQueue(guards) }) .then(() => { guards = [] for (const record of to.matched) { // 不在重用视图上触发beforeEnter // 路由配置中有beforeEnter,并且from不匹配record if (record.beforeEnter && !from.matched.includes(record)) { if (Array.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) // 调用路由配置中的beforeEnter return runGuardQueue(guards) }) .then(() => { // 革除存在的enterCallbacks 由extractComponentsGuards增加 to.matched.forEach(record => (record.enterCallbacks = {})) // 获取被激活组件中的beforeRouteEnter钩子,在之前会解决异步路由组件 guards = extractComponentsGuards( enteringRecords, 'beforeRouteEnter', to, from ) guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .then(() => { guards = [] // 解决全局beforeResolve钩子 for (const guard of beforeResolveGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) // 捕捉任何勾销的导航 .catch(err => isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED) ? err : Promise.reject(err) ) )
截止目前一个欠残缺的导航的解析流程(蕴含钩子的执行程序)如下 :
- 导航被触发
- 调用失活组件中的
beforeRouteLeave
钩子 - 调用全局
beforeEach
钩子 - 调用重用组件内的
beforeRouteUpdate
钩子 - 调用路由配置中的
beforeEnter
钩子 - 解析异步路由组件
- 调用激活组件中的
beforeRouteEnter
钩子 - 调用全局的
beforeResolve
钩子
你可能发现了,在每放入一个周期的钩子函数之后,都会紧跟着向guards
中增加一个canceledNavigationCheck
函数。这个canceledNavigationCheck
的函数作用是如果在导航期间有了新的导航,则会reject
一个ErrorTypes.NAVIGATION_CANCELLED
错误信息。
function checkCanceledNavigationAndReject( to: RouteLocationNormalized, from: RouteLocationNormalized): Promise<void> { const error = checkCanceledNavigation(to, from) return error ? Promise.reject(error) : Promise.resolve()}function checkCanceledNavigation( to: RouteLocationNormalized, from: RouteLocationNormalized): NavigationFailure | void { if (pendingLocation !== to) { return createRouterError<NavigationFailure>( ErrorTypes.NAVIGATION_CANCELLED, { from, to, } ) }}
在向guards
中放入钩子时,都应用了一个guardToPromiseFn
,guardToPromiseFn
能够将钩子函数转为promise
函数。
export function guardToPromiseFn( guard: NavigationGuard, to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, record?: RouteRecordNormalized, name?: string): () => Promise<void> { const enterCallbackArray = record && (record.enterCallbacks[name!] = record.enterCallbacks[name!] || []) return () => new Promise((resolve, reject) => { // 这个next函数就是beforeRouteEnter中的next const next: NavigationGuardNext = ( valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error ) => { // 如果调用next时传入的是false,勾销导航 if (valid === false) reject( createRouterError<NavigationFailure>( ErrorTypes.NAVIGATION_ABORTED, { from, to, } ) ) else if (valid instanceof Error) { // 如果传入了一个Error实例 reject(valid) } else if (isRouteLocation(valid)) { // 如果是个路由。能够进行重定向 reject( createRouterError<NavigationRedirectError>( ErrorTypes.NAVIGATION_GUARD_REDIRECT, { from: to, to: valid, } ) ) } else { // 如果valid是个函数,会将这个函数增加到record.enterCallbacks[name]中 // 对于record.enterCallbacks的执行机会,将会在RouterView中进行剖析 if ( enterCallbackArray && // since enterCallbackArray is truthy, both record and name also are record!.enterCallbacks[name!] === enterCallbackArray && typeof valid === 'function' ) enterCallbackArray.push(valid) resolve() } } // 调用guard,绑定this为组件实例 const guardReturn = guard.call( record && record.instances[name!], to, from, // next应该只容许被调用一次,如果应用了屡次开发环境下给出提醒 __DEV__ ? canOnlyBeCalledOnce(next, to, from) : next ) // 应用Promise.resolve包装guard的返回后果,以容许异步guard let guardCall = Promise.resolve(guardReturn) // 如果guard参数小于3,guardReturn会作为next的参数 if (guard.length < 3) guardCall = guardCall.then(next) // 如果guard参数大于2 if (__DEV__ && guard.length > 2) { const message = `The "next" callback was never called inside of ${ guard.name ? '"' + guard.name + '"' : '' }:\n${guard.toString()}\n. If you are returning a value instead of calling "next", make sure to remove the "next" parameter from your function.` // guardReturn是个promise if (typeof guardReturn === 'object' && 'then' in guardReturn) { guardCall = guardCall.then(resolvedValue => { // 未调用next。如: // beforeRouteEnter(to, from ,next) { // return Promise.resolve(11) // } if (!next._called) { warn(message) return Promise.reject(new Error('Invalid navigation guard')) } return resolvedValue }) // TODO: test me! } else if (guardReturn !== undefined) { // 如果有返回值,并且未调用next。如 // beforeRouteEnter(to, from ,next) { // return 11 // } if (!next._called) { warn(message) reject(new Error('Invalid navigation guard')) return } } } // 捕捉谬误 guardCall.catch(err => reject(err)) })}
guardToPromiseFn
中申明的的next
办法会作为钩子函数的第三个参数。如果在应用钩子函数时,形参的数量<3
,那么钩子函数的返回值会作为next
函数的参数;形参数量>2
时,如果钩子函数的返回值是Promise
,但未调用next
,会抛出谬误Invalid navigation guard
,如果钩子函数的返回值不为undefined
,也未调用next
也会抛出谬误Invalid navigation guard
。
所以如果在应用路由钩子的过程中,如果钩子函数的形参>2
,也就是你的形参中有next
,你必须要调用next
。如果你不想本人调用next
,那么你要保障形参<2
,同时钩子函数返回某个数据,这样vue-router
会主动调用next
。这里须要留神如果传递给next
的参数是个function
,那么这个function
会被存入record.enterCallbacks[name]
中,对于enterCallbacks
的执行机会,在这里不去深究,在后续的RouterView
源码剖析中,你会失去你想要的答案。对于钩子函数中next
的应用以下是一些示例:
beforeRouteEnter(from, to) { return false}// 等同于beforeRouteEnter(from, to, next) { next(false)}// 不能写为如下beforeRouteEnter(from, to, next) { return false}// 返回PromisebeforeRouteEnter(from, to) { return Promise.resolve(...)}// 返回functionbeforeRouteEnter(from, to) { return function() { ... }}
执行钩子列表的函数runGuardQueue
,只有以后钩子执行结束,才会执行下一个钩子:
function runGuardQueue(guards: Lazy<any>[]): Promise<void> { return guards.reduce( (promise, guard) => promise.then(() => guard()), Promise.resolve() )}
在pushWithRedirect
函数最初,在navigate
执行完后并没有完结,而是又进行了以下操作:
// 首先判断之前的操作是否出错// 如果出错,将failure应用Promise.resolve包装,进入.then// 如果未出错,调用navigate(),navigate过程中失败,进入.catch,胜利进入.then// 留神这里catch产生在then之前,所以catch运行完,可能会持续进入thenreturn (failure ? Promise.resolve(failure) : navigate(toLocation, from)) .catch((error: NavigationFailure | NavigationRedirectError) => isNavigationFailure(error) ? isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT) ? error // navigate过程中产生的重定向,进入.then : markAsReady(error) : // reject 未知的谬误 triggerError(error, toLocation, from) ) .then((failure: NavigationFailure | NavigationRedirectError | void) => { if (failure) { // 如果是重定向谬误 if ( isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT) ) { // 如果是循环的重定向(检测循环次数超过10次) if ( __DEV__ && // 重定向的地位与toLocation雷同 isSameRouteLocation( stringifyQuery, resolve(failure.to), toLocation ) && redirectedFrom && // 循环次数 (redirectedFrom._count = redirectedFrom._count ? redirectedFrom._count + 1 : 1) > 10 ) { warn( `Detected an infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This will break in production if not fixed.` ) return Promise.reject( new Error('Infinite redirect in navigation guard') ) } // 递归调用pushWithRedirect,进行重定向 return pushWithRedirect( // keep options assign(locationAsObject(failure.to), { state: data, force, replace, }), // preserve the original redirectedFrom if any redirectedFrom || toLocation ) } } else { // 如果在navigate过程中没有抛出错误信息 failure = finalizeNavigation( toLocation as RouteLocationNormalizedLoaded, from, true, replace, data ) } // 触发全局afterEach钩子 triggerAfterEach( toLocation as RouteLocationNormalizedLoaded, from, failure ) return failure })
能够发现,如果navigate
过程执行顺利的话,最初会执行一个finalizeNavigation
办法,而后触发全局afterEach
钩子。那么咱们来看下finalizeNavigation
是做什么的。
function finalizeNavigation( toLocation: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded, isPush: boolean, replace?: boolean, data?: HistoryState): NavigationFailure | void { // 查看是否勾销了导航 const error = checkCanceledNavigation(toLocation, from) if (error) return error // 第一次导航 const isFirstNavigation = from === START_LOCATION_NORMALIZED const state = !isBrowser ? {} : history.state // 仅当用户进行了push/replace并且不是初始导航时才更改 URL,因为它只是反映了 url if (isPush) { // replace为true或首次导航,应用routerHistory.replace if (replace || isFirstNavigation) routerHistory.replace( toLocation.fullPath, assign( { // 如果是第一次导航,重用history.state中的scroll scroll: isFirstNavigation && state && state.scroll, }, data ) ) else routerHistory.push(toLocation.fullPath, data) } // toLocation成为了以后导航 currentRoute.value = toLocation // 解决滚动 handleScroll(toLocation, from, isPush, isFirstNavigation) // 路由相干操作筹备结束 markAsReady()}
能够看出finalizeNavigation
函数的作用是确认咱们的导航,它次要做两件事:扭转url
(如果须要扭转)、解决滚动行为。在最初有个markAsReady
办法,咱们持续看markAsReady
是做什么的。
function markAsReady<E = any>(err?: E): E | void { // 只在ready=false时进行以下操作 if (!ready) { // 如果产生谬误,代表还是未筹备好 ready = !err // 设置监听器 setupListeners() // 执行ready回调 readyHandlers .list() .forEach(([resolve, reject]) => (err ? reject(err) : resolve())) // 重置ready回调列表 readyHandlers.reset() } return err}
markAsReady
函数会标记路由的筹备状态,执行通过isReady
增加的回调。
截止到此,push
办法也就完结了,此时一个欠残缺的的导航解析流程能够更新为:
- 导航被触发
- 调用失活组件中的
beforeRouteLeave
钩子 - 调用全局
beforeEach
钩子 - 调用重用组件内的
beforeRouteUpdate
钩子 - 调用路由配置中的
beforeEnter
钩子 - 解析异步路由组件
- 调用激活组件中的
beforeRouteEnter
钩子 - 调用全局的
beforeResolve
钩子 - 导航被确认
10.调用全局的afterEach
钩子
残余的流程,咱们将在RouterView
中持续进行补充。
replace
replace
与push
作用简直雷同,如果push
时指定replace: true
,那么和间接应用replace
统一。
function replace(to: RouteLocationRaw | RouteLocationNormalized) { return push(assign(locationAsObject(to), { replace: true }))}
这里调用了一个locationAsObject
,如果to
是string
,会调用parseURL
解析to
,对于parseURL
的实现可参考之前router.resolve
的剖析,它的次要作用是将to
解析成一个含有fullPath
(fullPath = path + searchString + hash
)、path
(一个绝对路径)、query
(query
对象)、hash
(#
及#
之后的字符串)的对象。
function locationAsObject( to: RouteLocationRaw | RouteLocationNormalized): Exclude<RouteLocationRaw, string> | RouteLocationNormalized { return typeof to === 'string' ? parseURL(parseQuery, to, currentRoute.value.path) : assign({}, to)}
总结
简略形容push
的执行流程:先进行重定向的判断,如果须要重定向,立马指向重定向的路由;而后判断要跳转到的路由地址与from
的路由地址是否雷同,如果雷同,在未指定force
的状况下,会创立一个错误信息,并解决滚动行为;紧接着调用extractChangingRecords
,将to
与from
所匹配到的路由进行分组,并依此提取并执行钩子函数,如果过程中不出错的话,最初会执行finalizeNavigation
办法,在finalizeNavigation
调用routerHistory.reaplce/push
更新历史栈,并解决滚动,最初执行markAsReady
,将ready
设置为true
,并调用通过isReady
增加的办法。
通过剖析push
的实现过程,咱们能够初步得出了一个略微残缺的导航解析流程:
- 导航被触发
- 调用失活组件中的
beforeRouteLeave
钩子 - 调用全局
beforeEach
钩子 - 调用重用组件内的
beforeRouteUpdate
钩子 - 调用路由配置中的
beforeEnter
钩子 - 解析异步路由组件
- 调用激活组件中的
beforeRouteEnter
钩子 - 调用全局的
beforeResolve
钩子 - 导航被确认
- 调用全局的
afterEach
钩子
上面咱们应用流程图来总结下整个push
过程: