前言
在看 HashHistory 和 HTML5History 的实现时,涉及到父类 History
与其 transitionTo
方法。
在路由发生跳转的时候,需要调用 transitionTo
方法,其中里面便实现了导航守卫。
History
HashHistory 和 HTML5History 都是继承于 History。在调用它们构造函数的时候通过 super 也调用了 History 的构造函数,同时也用到了父类的一些方法,比如 listen 和 transitionTo。
主要来看构造函数 constructor
与 transitionTo
方法的实现。
constructor
History 的构造函数里定义了四个属性。
- router
- base
- current
- pending
constructor (router: VueRouter, base: ?string) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
}
复制代码
router 保存了 Router 实例,方便接下来使用它的属性和方法。
base 是保存则应用的基路径。其中通过 normalizeBase
函数来判断处理,默认为 '/'。
current 和 pending 都是用来保存一个 Route 对象。
Route 对象在 flow 里是这样定义的:
declare type Route = {
path: string;
name: ?string;
hash: string;
query: Dictionary<string>;
params: Dictionary<string>;
fullPath: string;
matched: Array<RouteRecord>;
redirectedFrom?: string;
meta?: any;
}
复制代码
一个 Route 对象将包含这样一些属性,一个页面可以用一个 Route 对象来表示。
pending 保存着当前正在进行导航守卫的 Route 对象,current 则保存着完成导航守卫的当前 Route 对象。
current 初始化是一个 START
。START 的定义如下:
// the starting route that represents the initial state
export const START = createRoute(null, {
path: '/'
})
复制代码
通过 createRoute
函数创建的 START
,是一个除了 path
为 '/' 其它属性都为空的 Route 对象。
START = {
path: '/',
hash: '',
query: {},
params: {},
...
}
复制代码
transitionTo
来看下之前的代码里是怎么地使用 transitionTo:
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
复制代码
可以看到它接收两个参数,一个是路径,另一个是执行完导航守卫的回调函数。
接下来开始深入了解这个函数。
transitionTo (location: RawLocation, cb?: Function) {
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
this.updateRoute(route)
cb && cb(route)
this.ensureURL()
})
}
复制代码
match 方法先暂时放一放(因为这又是一大块),这里我们只要知道通过它能得到即将调整的路由对象即可。
主要关注的是这两个方法,confirmTransition
和 updateRoute
。
updateRoute
我们先从简单的 updateRoute
开始吧,免得到后面忘了它。首先可以猜出它是在 confirmTransition
的回调函数里面执行的,说明是在它之后才会执行。
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
复制代码
updateRoute
主要做了三件事情:
- 更新保存 current 属性。
- 执行 cb 回调,即之前我们用
listen
监听的 cb,从而进一步更新 router-view 组件。 - 最后执行 afterHooks 数组里保存的全局
afterEach
钩子。
剩下的钩子触发都是发生在 confirmTransition
里面。
confirmTransition
confirmTransition
里的代码有点多,且有点复杂。
先让我大致说一下它做了哪些实现,再来结合看源代码吧。
- 首先会判断传入的路由对象跟当前是否是同一个,是的话,则不需要跳转页面。
- 通过
resolveQueue
函数找到即将要被销毁的组件,和即将要被激活的组件。 - 将即将销毁的组件和即将激活的组件的导航守卫收集到一个数组
queue
中。 - 接着定义一个迭代函数
iterator
,用来在迭代queue
里每一个导航守卫时做些事情。 - 调用
runQueue
开始迭代调用导航守卫,最后执行回调cb
。
接下来不会逐句代码解释(添加少量注释),我们的目的是了解上面的大概实现。
实现 1
实现 1 里先是判断要跳转的页面:
const current = this.current
if (isSameRoute(route, current)) {
this.ensureURL()
return
}
复制代码
isSameRoute
方法判断了两个 Route 对象的 name、hash、query 和 params 属性是否一致,是的话,则表示是同个路由,直接 return,否则就继续往下走。
实现 2
实现 2 里做的是找到即将销毁和即将激活的组件:
const {
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
复制代码
Route 对象的 matched 属性包括着每一层路由的 RouteRecord 对象(跟 Route 属性差不多,主要多了 beforeEnter 方法)。
resolveQueue
方法通过 matched 属性来来逐个遍历,找到哪一层级开始的路由是不一样的,接下来就从这一层级来更新路由组件。
实现 3
实现 3 里收集了组件的导航守卫:
const queue: Array<?NavigationGuard> = [].concat(
// 即将销毁的组件的 beforeRouteLeave 钩子
extractLeaveGuards(deactivated),
// 全局的 beforeEach 钩子
this.router.beforeHooks,
// enter guards
// 即将激活的组件的 beforeEnter 钩子
activated.map(m => m.beforeEnter),
// 解析异步路由组件
resolveAsyncComponents(activated)
)
复制代码
这里需要细读一下 flatMapComponents
函数:
function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
return Array.prototype.concat.apply(
[],
matched.map(m => {
// 通过遍历 matched 对应的组件
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
复制代码
flatMapComponents
函数实现的功能是,遍历传入的 matched 里的 components 里的每一个组件(即我们在 new VueRouter 传入的 components),每一个组件都调用一遍 fn 回调函数。
extractLeaveGuards
就通过了 flatMapComponents
函数来收集即将销毁的组件的 beforeRouteLeave 钩子。而 resolveAsyncComponents
也是通过它来收集异步组件。
所以实现 3 里将所有即将要执行的钩子(导航守卫)都收集到了 queue
数组里,用来接下来的遍历钩子。
实现 4
实现 4 定义一个迭代函数 iterator
,到时每一次触发钩子的时候,会先执行一遍 iterator 函数,并将钩子函数传入。
iterator 函数会执行钩子函数,并实现了 next
的使用。next 的使用具体参考 vue-router 全局守卫。
实现 5
前面的实现都是为了这里的调用。
runQueue(queue, iterator, () => {
// ...
})
复制代码
这里用到了 runQueue 函数,接收 3 个参数,runQueue 会迭代 queue 数组,每一次都先执行一遍 iterator,iterator 里才是真正地触发钩子。当全部钩子触发完毕,则调用第 3 个参数,即一个回调函数。
queue 里所有钩子触发完后,还需要触发激活组件的 beforeRouteEnter 钩子。
所以需要收集激活组件的 beforeRouteEnter 钩子,再执行一遍 runQueue 函数。以下代码在执行完第一个 runQueue 函数后的回调函数里执行:
// 激活的组件 beforeRouteEnter 钩子
const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
return this.current === route
})
// wait until async components are resolved before
// extracting in-component enter guards
runQueue(enterGuards, iterator, () => {
// ...
})
复制代码
extractEnterGuards
专门收集了激活组件的 beforeRouteEnter 钩子。
当所有 beforeRouteEnter 钩子执行完毕后,就会调用 confirmTransition 的 cb 了,并将当前 Route 对象作为参数传入。
if (this.pending === route) {
this.pending = null
cb(route)
// 解决 <transition> 问题
// 等 DOM 更新后再调用 beforeRouteEnter 守卫中传给 next 的回调函数
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => cb())
})
}
复制代码
时光倒流,再回到刚开始调用 confirmTransition
的时候,来瞧瞧当时传了神马作为回调函数。
this.confirmTransition(route, () => {
this.updateRoute(route)
cb && cb(route)
this.ensureURL()
})
复制代码
这代码应该很熟悉了,updateRoute
方法前面已经了解过了,作用是更新 router-view 组件。
至此,一套导航守卫的代码就粗略地解读结束,完整的导航解析流程可以参考 官方文档。
心得
导航守卫的源码解读可以说是看 vue-router 源码中最困难的一部分,代码量不少,函数跳来跳去,刚开始确实看得我晕头转向的。
后来多看了几遍,抓住几个重点的函数,其它的暂时放一边,慢慢地也总算是掌握了思路。所以看源码不要指望自己第一遍就能够全部看明白。
以上的解读中会有许多函数没有讲解出来,一方面我觉得没必要太多细致地纠结每一行代码,另一方面函数名基本已经说明其功能。若对它们的实现感兴趣的话,可以打开 vue-router 的源码自己看下。
下期预告
还记得曾经在 Router 调用 constructor 构造函数的时候,我们暂时放一边的 createMatcher
函数吗?
constructor (options: RouterOptions = {}) {
this.match = createMatcher(options.routes || [])
// ...
}
复制代码
在深入了解 transitionTo 的时候,我们就用到了这个 match
方法。
transitionTo (location: RawLocation, cb?: Function) {
const route = this.router.match(location, this.current)
// ...
}
复制代码
到底 createMatcher
做了什么操作?使用了 match
方法又有什么特效?下期我们就来深入了解 match 吧。