前言
【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
过程:
发表回复