以下内容都是基于路由处于hash模式时
使用this.$router.push()
切换路由
当调用push
方法时,使用的是Router
实例的方法,需要传入1-3个参数,分别是跳转的路径,成功函数,失败函数
避免内容太乱太复杂,对下面的函数都进行了一定的删减方便理解!
//index.js
push(location, onComplete, onAbort) {
this.history.push(location, onComplete, onAbort)
}
调用this.$router.push()
会执行history
中的push
方法,history
是HashHistory
继承至History
类的实例,History
作为基类,拥有公共方法,如transitionTo``confirmTransition
这种公共方法,而history.push
就交由不同路由模式下的子类来实现
而history.push
的作用也非常简单,仅仅只是将current
进行结构并重新命名,然后调用HashHistory
实例核心方法transitionTo
,将路径传入,并将回调函数作为第二个参数,第三个参数为失败函数
//hash.js
function push (location, onComplete , onAbort) {
const { current: fromRoute } = this
this.transitionTo(
location,
//在下面有对该函数调用时机的解释
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
transitionTo
//base.js
function transitionTo (location,onComplete,onAbort) {
//匹配到的路由,可以通过this.$route拿到
let route = this.router.match(location, this.current)
const prev = this.current
this.confirmTransition(
route,
() => {
//对此回调下面有详解
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
},
)
}
调用transitionTo
,获取到route
对象,这个对象有以下参数
键名 | 类型 | 定义 |
---|---|---|
fullPath | string | 匹配到的完整路径 |
hash | string | |
matched | Array | 重要的参数,记录着从一级路由到要跳转的路由中所有的定义在routes里的内容 |
meta | Object | 路由中的元数据,如做面包屑导航时,通过meta里定义的title,就可以拿到从一级路由到跳转路由中所有的title信息 |
name | string | |
params | Object | 动态路由时获取的参数,也可以push跳转时传入,需要注意的是push跳转 params不能与path同时使用 |
path | string | 传入的路径 |
query | Object | path后面拼接?a=1 ,query里就有{a=1} |
接着调用confirmTransition
方法
confirmTransition
confirmTransition
方法主要做了4件事,以下将一一说明
function confirmTransition(route, onComplete){
//此处current是要离开的路由
const current = this.current
...
}
1. 比对
进入路由与离开路由中matched
数组中内容进行比对,拿到updated
、deactivated
、activated
如:/foo/nav -> /foo/bar
-
updated
: 可复用的配置,定义在routes
里的/foo
配置就是可以复用的 -
deactivated
:即将离开的配置,/nav
就是即将离开的配置 -
activated
: 需要更新的配置,/bar
就是需要更新的配置 -
//resolveQueue函数非常简单,就是找到两者最大长度,然后遍历从0开始找相同,然后找不同 const { updated, deactivated, activated } = resolveQueue( this.current.matched, route.matched )
2. 获取导航守卫
通过updated
、deactivated
、activated
,创建导航守卫队列,队列中有以下守卫
-
通过
deactivated
得到的beforeRouteLeave
守卫数组,如果``deactivated长度大于1,那么
beforeRouteLeave`数组顺序是从子到父 -
拿到全局调用的
beforeEach
的回调函数数组 -
通过
updated
得到的beforeRouteUpdate
守卫数组 -
拿到
activated
路由配置中的beforeEnter
方法 -
对
activated
中所有路由组件进行解析,确保所有的异步组件解析完成后在执行接下来的守卫 -
//拍平为一维数组 const queue = [].concat( //beforeRouteLeave数组 extractLeaveGuards(deactivated), //beforeEach数组 this.router.beforeHooks, //beforeRouteUpdate数组 extractUpdateHooks(updated), //beforeEnter数组 activated.map(m => m.beforeEnter), // 解析异步组件 resolveAsyncComponents(activated) )
3. 创建迭代器函数
该函数接收两个参数,一个是当前的守卫hook,一个是回调函数next
-
主要功能:对hook进行调用,传入要跳转路由,要离开的路由,以及一个回调函数
-
hook使用如:
beforeEach
-
to
就是要进入的路由 -
from
就是要离开的路由 -
next
是一个回调函数,必须在此处调用next
,否则将会暂停路由切换,具体原因下面将会分析 -
router.beforeEach((to, from, next) => { console.log('beforeEach全局前置守卫', to, from) next() })
-
-
迭代器具体实现
-
const iterator = (hook, next) => { //hook就是导航 //回调函数中to就是守卫中next(...)传入的参数 hook(route, current, (to) => { //如果to符合以下预期,将尝试跳转 if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string')) ) { if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { //否则对iterator的next回调调用,注意:此处传入to参数无任何意义 next(to) } }) }
-
4. 执行队列
使用递归方式创建类似generator函数将队列执行
先将该函数贴下面,大致知道通过递归使用即可,下面将一一说明使用
function runQueue (queue, fn, cb) {
//创建递归函数
const step = index => {
//当index大于queue,说明守卫队列全部被执行完毕
if (index >= queue.length) {
//此处cb()就是执行步骤2或步骤4
cb()
} else {
//判断是否有守卫,可能存在是undefined
if (queue[index]) {
//fn是iterator,queue[index]是守卫,回调函数是iterator中的next
fn(queue[index], () => {
step(index + 1)
})
} else {
//如果是undefined,就直接进行下一个守卫的调用
step(index + 1)
}
}
}
step(0)
}
**调用runQueue
时大致分类4个步骤
-
步骤1:将
queue
中的守卫 逐一调用 -
步骤2:守卫 全部调用完毕后,执行
runQueue
时传入的回调函数调用 -
步骤3:重复步骤1,不过
queue
中的守卫是beforeRouteEnter
和beforeResolve
-
步骤4:重复步骤2,此时回调函数调用,除
afterEach
外所有导航守卫都被调用完毕 -
//执行步骤1 runQueue(queue, iterator, //执行步骤2 () => { //提取`beforeRouteEnter` const enterGuards = extractEnterGuards(activated) //beforeRouteEnter和beforeResolve 拼接 const queue = enterGuards.concat(this.router.resolveHooks) //执行步骤3 runQueue(queue, iterator, //执行步骤4 () => { onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { handleRouteEntered(route) }) } }) })
大致执行流程如下,在守卫中必须调用
next()
,否则就会暂停路由导航,这也是runQueue
主要做的事情,保证守卫依次执行并且必须由用户手动确定何时执行
步骤4结束后就执行onComplete
函数,这个函数就是调用confirmTransition
时的第二个参数,是一个回调函数,主要做了以下事情
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
- 更新路由,并对
_route
执行赋值,触发set方法进行依赖收集,并在下次异步任务时进行更新视图 - 调用
hashHistory实例push
时传入的回调函数onComplete
,这个onComplete
和上面的是不同的回调函数,注意区分!- 这个
onComplete
回调函数主要做了如下三件事- 执行
pushHash(route.fullPath)
来更新浏览器会话的历史堆栈 - 处理滚动
- 执行用户传入的成功函数
- 执行
- 这个
- 确保路由是当前的路由
- 调用全局
afterEach
钩子
上面回调结束后会继续往下执行该判断,this.router.app
是根Vue
实例,在下次DOM更新后触发handleRouteEntered
方法
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
该方法主要是获取BeforeRouteEnter
中传入的回调,因为在BeforeRouteEnter
中无法获取this
,此时实例还没有被创建
beforeRouteEnter
守卫 不能 访问this
,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。不过,你可以通过传一个回调给
next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
此时一次由用户调用this.$router.push()
流程基本走完
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。- 调用全局的
beforeEach
守卫。- 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+)。- 在路由配置里调用
beforeEnter
。- 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。- 调用全局的
beforeResolve
守卫 (2.5+)。- 导航被确认。
- 调用全局的
afterEach
钩子。- 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。