当我们创建Vue-Router实例的时候,我们会选择三种模式。
对应着三种不同的history模式。
其中我们比较常用的是history和hash模式
Vue-Router 构造函数
源码路径:src/router.js
对于构造函数来讲其中比较重要的两个地方是:创建matcher实例和创建history实例
Matcher(待补充)
通过createMatche方法创建,构造函数有两个参数:routes和VueRouter实例
routes:我们在定义VueRouter时传入的对象 VueRouter实例则是当前VueRouter实例
Matcher主要用于提供addRoute、addRoutes(现已抛弃)、matchRoute等方法
它是真正存储和管理我们路由的类。
常用方法:比如match方法,这个方法会为我们匹配到一个路由并且创建它。
那这个函数做了什么事儿呢?
首先我们可以看到它先是通过name去匹配到了我们目标路由。
History
有三种类型:HTML5History、HashHistory、AbstractHistory
这三个类都继承了 History
后边我们主要以HTML5History举例
History的子类是真正为我们提供路由跳转能力的类
接下来我们主要聊聊router的push方法
当我们在调用router.push方法的时候
我们先来看方法中的参数。
其中location就是咱们平时调用push方法传的参数
onComplete: 路由跳转成功之后的回调
onAbort: 路由跳转中止的回调
其实从第一张图我们可以看到,其实push方法主要调用的是我们history实例的push方法,我们接下来继续看。
这个是我们HTML5History类中的push方法。
可以看到它其实是调用了父类提供的transitionTo方法
首先我们看下transitionTo的三个参数:
第一个是location咱们就不用多说了对吧。
第二个是onComplete,它是一个跳转成功之后的回调。
那我们可以看到在history类中push方法里调用transitionTo的时候,传入了一个成功的回调函数。
回调函数中包括一个pushState和一个handleScroll。
其中pushState是向浏览器的历史记录中推送记录的,后边我们详解代码实现。
而handleScroll是我们在创建VueRouter时传入的回调函数,是在我们页面跳转后,用来控制页面滚动到什么位置的。
接下来我们先回到transitionTo方法
其实在方法内部它调用了一个核心方法,就是confirmTransition。
它同样是接收三个参数route、onComplete、onbrot
但是不同的是这个地方的route已经是我们通过调用匹配到的新对象了
this.$router.match(location,this.current)
完整的confirmTransition函数
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
this.pending = route
const abort = err => {
// 创建用于抛出异常的函数
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),
// 这边是我们注册的前置路由守卫,后置路由守卫会在路由跳转以后调用
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)
}
}
// 这是类中的迭代函数,使用iterator来迭代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)
})
}
})
})
}
其实到这里很多细节并没有补充完毕,但是基本的调用逻辑我们已经清晰了。
RouterView组件
当我们在使用VueRouter的时候经常会配合使用到RouterView组件。
这个组件是用于挂载我们的路由组件的。
其实大部分逻辑在源码中都有非常详细的讲解。
这边我们只聊重点。
可以看出,这里我们使用的是父组件的$createElement
函数。
为什么要这样使用呢?
你可以理解为,我们使用的是哪个组件的$createElement
,我们的组件就会渲染在哪个组件下边。
从注释也可以看出来。
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
之后我们会通过matcher匹配到要进入的路由
这里我们可以看到,如果我们没有匹配到的话,就会渲染空的元素
如果匹配到的话,就存到cache里边
然后cache的内容有什么用呢?
其实主要是为keep-alive的提供缓存的支持,对于已经keep-alive的路由组件,直接拿缓存的组件渲染就可以了。 对于没有经过keep-alive的路由组件,则在匹配到后,直接进行渲染。
番外(补充pushState逻辑)
向浏览器历史记录推送数据的核心逻辑
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
以上内容可能有误,有什么问题欢迎大家随时讨论哦!