简介
路由的概念相信大部分同学并不陌生,我们在用 Vue
开发过实际项目的时候都会用到 Vue-Router
这个官方插件来帮我们解决路由的问题。它的作用就是根据不同的路径映射到不同的视图。本文不再讲述路由的基础使用和API
,不清楚的同学可以自行查阅官方文档vue-router3 对应 vue2 和 vue-router4 对应 vue3。今天我们从源码出发以vue-router 3.5.3
源码为例,一起来分析下Vue-Router
的具体实现。
由于篇幅原因,
vue-router
源码分析分上、中、下三篇文章讲解。
前面我们已经讲了路由的安装和实例化,下面我们来看看初始化流程。
初始化
前面我们说到,在安装的时候会混入全局mixin
。我们知道全局混入,会影响后续创建的所有Vue实例
。beforeCreate
首次触发是在Vue根实例实例化的时候
即new Vue({router})
时。
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)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
}
...
})
由于router
仅存在于Vue根实例
的$options
上,所以,整个初始化只会被调用一次。也就是这个if (isDef(this.$options.router))
只会执行一次。
在这里我们我们重点分析下init
方法。
分析init方法
// src/index.js
...
init (app: any /* Vue component instance */) {
process.env.NODE_ENV !== 'production' &&
assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app) // 保存实例
// 绑定destroyed hook,避免内存泄露
app.$once('hook:destroyed', () => {
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// 需要确保始终有个主应用
if (this.app === app) this.app = this.apps[0] || null
if (!this.app) this.history.teardown()
})
// main app已经存在,则不需要重复初始化history 的事件监听
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History || history instanceof HashHistory) {
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// 调用父类的listen方法,添加回调;
// 回调会在父类的updateRoute方法被调用时触发,重新为app._route赋值
// 由于app._route被定义为响应式,所以app._route发生变化,依赖app._route的组件(route-view组件)都会被重新渲染
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
可以看到,init
方法主要做了下面几件事
-
检查了
VueRouter
是否已经安装 -
保存了挂载
router实例
的vue实例
。VueRouter
支持多实例嵌套,所以存在this.apps
来保存持有router实例
的vue实例
-
注册了一个一次性钩子
destroyed
,在destroyed
时,卸载this.app
,避免内存泄露。 -
检查了
this.app
,避免重复事件监听。 -
根据
history
类型,调用transitionTo
跳转到初始页面,并调用setupListeners
函数初始化路由变化的监听。 -
注册
updateRoute
回调,在route
更新时,更新app._route
完成页面重新渲染。
我们重点看下transitionTo
相关逻辑
分析transitionTo方法
// src/history/base.js
transitionTo (
location: RawLocation, // 原始location,一个url或者是一个Location
onComplete?: Function, // 跳转成功回调
onAbort?: Function // 跳转失败回调
) {
let route
try {
// 传入需要跳转的location和当前路由对象,返回to的Route
route = this.router.match(location, this.current)
} catch (e) {
this.errorCbs.forEach(cb => {
cb(e)