Vue-router源码解析
status: Draft tags: Vue, 源码解析 Created time: March 7, 2023 10:57 AM emoji: https://vuejs.org/logo.svg
VueRouter类
VueRouter
三种mode
实现导航
一次导航的完整步骤
vue-router一次完整的导航包含以下步骤:
在失活的组件里调用 beforeRouteLeave
守卫。
在重用的组件里调用 beforeRouteUpdate
守卫 (2.2+)。
在被激活的组件里调用 beforeRouteEnter
。
调用全局的 beforeResolve
守卫 (2.5+)。
调用 beforeRouteEnter
守卫中传给 next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
实现push
前端浏览器跳转页面通常有两种方式,代码跳转和浏览器跳转,代码跳转包含调用this.$router.push()
和点击<router-link>
组件,浏览器跳转则是点击浏览器特定按钮,例如前进和后退按钮进行跳转。
无论是代码跳转还是浏览器跳转,其最终都调用了transitionTo函数,下面以HTML5History模式为例。
export class HTML5History extends History {
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
const location = getLocation(this.base)
if (this.current === START && location === this._startLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
}
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
}
transitionTo函数
transitionTo位于类History,它是HashHistory、HTML5History 、AbstractHistory的父类。
transitionTo会再调用confirmTransition 函数来确定是否跳转路由,并传递一个onComplete函数作为完成时的回调。
export class History {
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
route = this.router.match(location, this.current)
} catch (e) {
this.errorCbs.forEach(cb => {
cb(e)
})
// Exception should still be thrown
throw e
}
const prev = this.current
this.confirmTransition(
route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
// ...
}
)
}
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
this.pending = route
const abort = err => {
// changed after adding errors with
// https://github.com/vuejs/vue-router/pull/3047 before that change,
// redirect and aborted navigation would produce an err == null
if (!isNavigationFailure(err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
if (process.env.NODE_ENV !== 'production') {
warn(false, 'uncaught error during route navigation:')
}
console.error(err)
}
}
onAbort && onAbort(err)
}
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL()
if (route.hash) {
handleScroll(this.router, current, route, false)
}
return abort(createNavigationDuplicatedError(current, route))
}
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})
}
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
}
// runQueue函数的定义位于util/async文件下
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
confirmTransition
confirmTransition
函数将所有要执行的函数存储到队列中,以确保执行顺序的正确。最初执行:
const queue: Array<?NavigationGuard> = [].concat(
// 组件内beforeRouteLeave守卫
extractLeaveGuards(deactivated),
// 全局 beforeEach守卫
this.router.beforeHooks,
// 组件内update守卫(组件重用时调用)
extractUpdateHooks(updated),
// router配置文件中的beforeEnter守卫
activated.map(m => m.beforeEnter),
// 解析异步组件
resolveAsyncComponents(activated)
}
runQueue 函数会按顺序将这些守卫函数传入iterator 函数中执行,当用户在导航守卫中调用next()调转其他路由时,又会重新调用push跳转到目标路由。
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
在confirmTransition函数中调用runQueue时,又传入了一个回调函数,以便queue执行完毕后执行。
runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})
在这个函数中又会以相同的方式执行
最后又调用了
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
这里的onComplete来自transitionTo函数:
this.confirmTransition(
route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
// updateRoute
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
// onComplete 又来自最开始的HTML5History
export class HTML5History extends History {
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
// 最后这里的onComplete 来自用户所传的onComplete 函数,即this.$router.push(routeObj,onCompelete)
onComplete && onComplete(route)
}, onAbort)
}
}
updateRoute函数执行的过程中会调用this.cb(route)
,这里的cb函数来自前面VueRouter类的init方法中:
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
由于app._route是响应式的,改变app._route时就会导致页面重新渲染,也就是触发了DOM更新。
也就是说在这一过程中做了:
为什么app._route = route
先执行,但是DOM却在后面才触发更新呢?这是因为Vue组件渲染是异步的,由一个异步调度系统控制。
最后,调用 beforeRouteEnter
守卫中传给 next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
这里使用nextTick
的原因也同样是因为要延迟到DOM更新后再执行。
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
// handleRouteEntered 位于util/route.js文件下
export function handleRouteEntered (route: Route) {
for (let i = 0; i < route.matched.length; i++) {
const record = route.matched[i]
for (const name in record.instances) {
const instance = record.instances[name]
const cbs = record.enteredCbs[name]
if (!instance || !cbs) continue
delete record.enteredCbs[name]
for (let i = 0; i < cbs.length; i++) {
if (!instance._isBeingDestroyed) cbs[i](instance)
}
}
}
}
至此,导航解析核心流程执行完毕:
在失活的组件里调用 beforeRouteLeave
守卫。
在重用的组件里调用 beforeRouteUpdate
守卫 (2.2+)。
在被激活的组件里调用 beforeRouteEnter
。
调用全局的 beforeResolve
守卫 (2.5+)。
调用 beforeRouteEnter
守卫中传给 next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
监听浏览器事件
组件
router-link
router-link