前言
看了这篇文章后:juejin.cn/post/699164…,作者写得很不错,由浅入深,但因为确实很详细,导致如果不是一次性静下心来研究的话,容易看了下面,忘了上面,这里通过一个跳转例子来看看路由跳转的过程中发生了哪些事,这里使用的例子是vue-routergithub源码里的example/hash-mode🌰。
起步
先看看这个例子里的代码
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 1. 定义路由组建
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 2. 创建路由
const router = new VueRouter({
mode: 'hash',
base: __dirname,
routes: [
{ path: '/', component: Home, name: 'Home' }, // all paths are defined without the hash.
{ path: '/foo', component: Foo, name: 'Foo' },
{ path: '/bar', component: Bar },
]
})
const vueInstance = new Vue({
router,
template: `
<div id="app">
<h1>Mode: 'hash'</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<router-link tag="li" to="/bar">/bar</router-link>
</ul>
<router-view class="view"></router-view>
</div>
`,
methods: {}
}).$mount('#app')
document.getElementById('unmount').addEventListener('click', () => {
vueInstance.$destroy()
vueInstance.$el.innerHTML = ''
})
这里对源码进了一下删减,主要是去除一些跟路由跳转无关的展示性的代码,可以看到,代码就是很常规的定义路由信息跟创建路由,接着使用<router-link>跟<router-vue>来进行路由跳转跟路由内容展示。
既然是使用<router-link>来出发跳转逻辑,那么跳转的开始也就是在定义<router-link>组件的link.js中,先看看触发的逻辑
const handler = e => { // 触发的路由跳转逻辑
if (guardEvent(e)) { // 这里是去除默认的点击行为
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}
const on = { click: guardEvent } // 为点击时间添加清除默认行为的逻辑
if (Array.isArray(this.event)) { // event是我们传进来的,用于指定支持跳转逻辑的事件,默认是click
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
if (this.tag === 'a') { // 这段逻辑是找到触发跳转事件的html标签,默认是用<a>标签
data.on = on
data.attrs = { href, 'aria-current': ariaCurrentValue }
} else {
// find the first <a> child and apply listener and href
const a = findAnchor(this.$slots.default)
if (a) {
} else {
// doesn't have <a> child, apply listener to self
data.on = on
}
}
return h(this.tag, data, this.$slots.default) // 生成触发跳转逻辑的vNode
这里主要做两件事:
- 找到真正出发跳转逻辑的节点,默认是
<a>标签,否则查找子节点,有<a>标签则使用这个<a>,否则将事件添加到tag属性传入的标签。 - 添加跳转逻辑:跳转逻辑默认是由click事件触发,但我们也可以通过传入event来决定哪些可以触发跳转逻辑,跳转逻辑实际上就是调用
router抛出的方法,既然是我们改写了跳转逻辑,那么就得调用guardEvent来清楚默认的跳转行为。
所以点击节点后,触发的就是handler()函数,接下来看看handler()函数的定义,其实就是调用router,而router其实就是VueRouter实例。
VueRouter
上面的handler()函数里调用的router方法是在VueRouter类定义的实例方法,这里以router.push()举例,router.push()的源码如下在VueRouter类中:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
push方法接收三个参数,第一个是跳转的路由信息,其他两个是在导航成功完成 (在所有的异步钩子被解析之后) 或终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用。 这里调用的是this.history.push,而this.history是由我们路由配置信息里的mode决定的,在VueRouter类的构造函数里定义的
constructor (options: RouterOptions = {}) {
...
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
既然我们使用的是examples/hash-mode那么就是走到case 'hash'里了,这样就走到HashHistory。
HashHistory
由上面可知,this.history.push()实际上调用的是HashHistory类的push()方法,源码如下:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
可以看到,主要就是两部分:获取跳转源和执行this.transitionTo()方法,transitonTo()方法并不是在HashHistory类里定义的,是通过继承History类而来的,所以得去看下base.js里的定义
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
route = this.router.match(location, 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)
})
}
},
...
)
}
先看参数:
- location:目标信息对象,就是我们目标路由信息,我们经常调用路由的
push方法就是传loaction对象,只不过这里是通过<router-link>触发push方法的。 - onComplete: 将会在导航成功完成 (在所有的异步钩子被解析之后) 执行
- onAbort: 终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用
接下来首先通过location跟this.current(当前路由信息)来调用路由的match方法得出目标路由,所以这里看下match方法
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router) // 格式化路由信息
const { name } = location
if (name) { // 通过name匹配
const record = nameMap[name]
...
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
} else if (location.path) { // 通过path匹配
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
可以看到,这里就是通过两种匹配方式来创建最终路由,主要是通过我们路由配置信息的name或者path,之所以能通过这两个匹配,是因为源码会将我们传为vue-router的所有路由配置信息转成由path/name组成的映射对象,匹配这里的_createRoute是用来统一创建路由的,让所有路由具有相同的属性、方法。
ok,继续看transitionTo方法,接下来调用了this.confirmTransiton方法,这里使用this.confirmTransiton的目的是:
- 对于跳转同一个路由直接
abort掉 - 执行生命周期,因为在发生路由跳转的前后,我们需要进行执行对应的生命周期,所以这里会对执行进行编排,先执行路由跳转前的生命周期,再执行
onComplete,接着执行路由跳转后的生命周期。(这里可以通过在组件内部打debugger来看调用栈)
接下来看看onComplete的逻辑:
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}
onComplete就是一开始传进去的这段代码,这里我们看下pushHash就行
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
很简单就是进行导航了hash的变更。
到了这里,导航拦的hash就变更成最终路由的hash了,但页面并没有更新,因为这块更新逻辑还未执行。
怎么让<router-view>更新?需要做到
- 知道路由变更
- 获取最终的路由信息,然后调用渲染函数
响应路由变更
可以通过监听$route对象。这个就是当前路由信息,因为我们是进行路由的修改,所以这个对象更新时,就是代表需要路由更新了,所以得先对其赋值并设置为响应式,这样他一改变,<router-view>就能收到通知,如下:
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
}
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
可以看到,是调用Vue的defineReactive来将_route设置为响应数据,_route就是指向当前的路由,而$route也就是指向_route。所以我们需要做两件事:
- 更新_route,这样监听他的就会收到响应,执行更新逻辑
- 订阅_route
更新_route
上面代码调用了init()函数,这个函数代码是VueRouter实例的一个方法,主要做如下事情:
init (app: any /* Vue component instance */) {
...
app.$once('hook:destroyed', () => { // 监听根实例销毁,执行卸载逻辑
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
if (!this.app) this.history.teardown()
})
if (this.app) { // 避免重复注册
return
}
...
history.listen(route => { // 这里就是更新_route的逻辑
this.apps.forEach(app => {
app._route = route
})
})
}
可以看到,更行_route的逻辑是通过history.listen触发,但这里的触发只是在路由初始化时触发,所以需要将传给history.listen的回调保存下来,在后续路由更新时进行调用,这里的history.listen调用的是Histor类的listen方法
listen (cb: Function) {
this.cb = cb
}
就是将回调保存在this.cb上,那么接下来就是路由更新时调用this.cb即可,路由的更新时发生在History里的transitionTo里的,这个我们上面分析过,再简单看下
transitionTo (
...
) {
...
route = this.router.match(location, this.current) // 获取得到最新的路由信息
...
this.confirmTransition(
route,
() => {
this.updateRoute(route)
...
},
可以看到,我们先通过this.router.match获取得到最新路由信息,然后调用了this.updateRoute
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
在updateRoute里调用了之前保存的cb,从而更新了_route,这样监听了_route的<router-view>就能收到更新了,接下来看看做了啥:
- 获取最行路由信息
- 判断是否keep-alive:如果是keep-alive就去根节点的
_routerViewCache里找是否有匹配的 - 匹配路由组件:通过
route的match函数匹配需要渲染的组件 - 执行路由生命周期函数
- 调用渲染函数并返回结果
至此,结束。