前言
【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 = redirectedFrom
let 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}
// 返回 Promise
beforeRouteEnter(from, to) {return Promise.resolve(...)
}
// 返回 function
beforeRouteEnter(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 运行完,可能会持续进入 then
return (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
过程: