My-Blog
My-Blog copied to clipboard
vue-router 源码分析
vue-router 源码分析
思考 🤔
1、vue-router 是怎么被初始化以及装载的
2、路由改变是怎么更新视图的

流程图
结合流程图来分析下面代码的执行流程
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
]
})
new Vue({
router,
template: `
<div id="app">
<router-view></router-view>
</div>
`
}).$mount('#app')
1、vue-router 初始化以及装载
vue-router 作为 Vue 的一个插件,通过全局方法 Vue.use() 的方式将 vue-router 挂载到 Vue 上。
对照流程图:
(1)、Vue.use(VueRouter) 首先会调用 install 方法,
(2)、利用 mixin 往 Vue 混入一个 beforeCreat 钩子,接着 beforeCreat 钩子做了哪些事情呢,
(3)、检测 Vue 的 options 有没有 router 实例,有的话会通过 Object.defineProperty 将 $router 和 $route 变成响应式对象,
(4)、并挂载到 Vue 的 prototype 上,
(5)、最后会往 Vue 的 $options.components 注册两个全局组件 router-view 和 router-link
vue-router 挂载完,new VueRouter 实例化 VueRouter 有做了哪些事情呢。
对照流程图:
(1)、刚开始当然执行构造函数做一些初始化操作(流程图没体现出来)
(2)、接着会执行 createMatcher(options, routers) 根据 VueRouter 传入的 routes 数组创建 path 和 component 的匹配规则
(3)、根据 VueRouter 传入的 mode 实例化 history 属性
(4)、定义一些钩子函数 beforeEach beforeResolve afterEach (流程图没体现出来)
(5)、定义一些路由方法 push、replace、go、back
(6)、定义 init() 方法,该方法是在Vue组件的 beforeCreat 钩子下会被调用,执行的时候会去匹配对应的 path 并找到对应的 component 再渲染到页面上(先有个印象就好)
2、路由改变是怎么触发匹配的组件更新的
前面做了这么一大波操作,那路由改变是怎么触发组件更新的呢,或者一个Vue项目初始化的时候是怎么加载路由组件的呢,具体流程是怎么样的。
如果你还没被流程图绕晕的话,再看一眼流程图的 黑色 线路。
前面我们提到了 Vue.use() 时会利用 mixin 往 Vue 混入一个 beforeCreat 钩子,注意这个钩子是全局钩子,意味着每个 Vue 组件都会调用这个钩子,包括根组件自身。
跟着黑色 线路走:
(1)、在根组件实例化后就会调用 beforeCreat 钩子,
(2)、接着会调用 route 实例的 init() 方法,init() 方法会执行 transitionTo 来改变对应的路由。
(3)、那对应的路由从哪里找呢,就是通过 match() 方法,去匹配我们上面有提到的 createMatcher 方法已经做好的 path 和 component 的匹配规则。
(4)、通过 path 找到对应的 component 后会再执行 confirmTransition 来确认改变路由,
(5)、接着 updateRoute 跟更新路由,最后将匹配的 component 给 current,
(6)、执行这些操作后其实是有个回调函数的,回调函数会跟新 _route 属性,该属性也是一个响应式对象,
(6)、_route 更新就会触发视图渲染,也就是调用 app 的 render() 函数,接着也触发了 router-view 组件的 render ,
(7)、router-view 组件的 render 会获取匹配的 component 渲染到视图上。
源码解析
通过流程图大致分析了 vue-router 的初始化和装载过程,以及路由跟新的流程。了解完大概思路下面进行源码层的分析就不会那么摸不着头绪了。
1、路由注册
故事的开端还是需要从路由的注册说起。
Vue.use(VueRouter)
vue-router 提供了一个 install 方法, Vue.use(VueRouter) 的时候会执行该方法。具体可看 vue 插件机制
import { install } from './install'
export default class VueRouter {
static install: () => void;
static version: string;
// ...
}
VueRouter.install = install
VueRouter.version = '__VERSION__'
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
install 方法里头做了这些事情:
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
// 确保 install 调用一次
if (install.installed && _Vue === Vue) return
install.installed = true
// 把 Vue 赋值给全局变量给 VueRouter 用
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
// 给每个组件混入 beforeCreate 钩子,注意,这意味着每个组件实例化时都会执行调用这个钩子
beforeCreate () {
// 判断是不是一个路由应用,有没有传入 router options
if (isDef(this.$options.router)) { // 根组件会执行
this._routerRoot = this
this._router = this.$options.router
// 初始化路由
this._router.init(this)
// 将 _route 属性实现双向绑定,改变时会触发组件渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else { // 其他子组件会执行
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 能够在组件上使用 this.$router 获取 _router 实例
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 能够在组件上使用 this.$route 获取 _router 实例
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 全局注册组件 router-link 和 router-view
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
}
2、VueRouter 实例
上面有提到 Vue 根实例会挂载 _router 属性,及 $options.router 实例,该实例就是 VueRouter 实例
this._router = this.$options.router
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
]
})
接下来分析 VueRouter 实例化做了哪些操作
export default class VueRouter {
constructor (options: RouterOptions = {}) {
//...
// 创建路由匹配对象
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根据 mode 采取不同的路由方式
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}`)
}
}
}
// ...
}
(1)、 createMatcher
初始化部分主要是 createMatcher 的功能, createMatcher 会创建路由匹配对象,及 path 和 component 对应
export function createMatcher (routes: Array<RouteConfig>,router: VueRouter): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
console.log('==pathMap==', pathMap)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function match (raw: RawLocation,currentRoute?: Route,redirectedFrom?: Location ): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
//...
return _createRoute(record, location, redirectedFrom)
}
// ...
return {
match,
addRoutes
}
}
createMatcher 会返回一个 match 和 addRoutes 方法, 调用 match 方法能根据 path 返回对应的组件。这里隐藏了一些细节的分析,直接
log pathMap 在控制台查看 createRouteMap 创建匹配的结果就好

可看到有这么一个结构
{
'': {
path: '',
components: {}
},
'/foo': {
path: '/foo',
components: {}
}
}
调用 match 方法能拿到对应的匹配内容了。
3、路由初始化
上面分析了 VueRouter 的实例化,那路由的初始化在哪里执行的呢,之前有提到 beforeCreate 会执行 this._router.init(this) ,这里就做了路由的初始化操作。
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)
},
destroyed () {
registerInstance(this)
}
})
来看 init() 做了哪些事情
init (app: any /* Vue component instance */) {
if (this.app) {
return
}
this.app = app
const history = this.history
// 判断路由模式
if (history instanceof HTML5History) {
// 路由跳转
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
// 添加 hashchange 监听
const setupHashListener = () => {
history.setupListeners()
}
// 路由跳转
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
//注册监听事件, transitionTo 执行完会执行该回调,回调里头对组件的 _route 属性进行赋值,触发组件渲染
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
transitionTo 执行完会执行回调,回调里头去更新 _route 从而跟新视图,所以先再看下 transitionTo 做了什么。
transitionTo (location: RawLocation,onComplete?: Function,onAbort?: Function) {
// 获取匹配的路由信息
const route = this.router.match(location, this.current)
// 确认切换路由
this.confirmTransition(
route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
// 执行回调
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
)
}
transitionTo 主要是去获取匹配的路由信息,这个匹配路由信息上面已经分析过了。接着调用 confirmTransition ,执行完会调用一个回调 cb(route),
这个就会触发之前的监听事件:
history.listen(route => {
this.apps.forEach((app) => {
app._route = route // 更新 _route 从而触发组件更新
})
})
4、vue-router
前面我们已经了解到了路由改变会经过一系列的操作,最终找到匹配的 component 从而跟新 route
app._route = route // 更新 _route 从而触发组件更新
而在讲解【1、路由注册】这一步我们有提到
Vue.util.defineReactive(this, '_route', this._router.history.current)
_route 是一个响应式属性,更新后会触发相应的视图更新,并且【1、路由注册】这一步我们还提到
// 全局注册组件 router-link 和 router-view
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
install 注册 vue-router 时顺带注册了一个全局组件 RouterView 。接下来我们来分析下 _route 属性的改变是如何触发视图更新的,也就是
<router-view class="view"></router-view>
是如何渲染匹配的 component 的
router-view 源码
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
data.routerView = true
const h = parent.$createElement
const name = props.name
const route = parent.$route // 获取 _router
const cache = parent._routerViewCache || (parent._routerViewCache = {})
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// ...
//获取到匹配的 component
const matched = route.matched[depth]
const component = matched && matched.components[name]
// ...
// 调用 createElement 函数 渲染匹配的组件
return h(component, data, children)
}
}
router-view 是一个函数组件, _router 改变触发 render 执行,render 内部获取到匹配的 component 进行渲染。
<router-view class="view"></router-view>
最后 router-view 被渲染成相应的组件。
小结
我们通过 vue-router 的挂载 -> 路由匹配 -> 组件渲染 的流程大致分析了 vue-router 的执行原理,我们隐藏和很多细节和功能没分析,比如
路由的 history 的 hash 模式、路由的具体匹配过程、路由守卫的功能等等。这些后续再具体分析,但了解了整体流程,接下来的细节功能就一步步拆开分析就清楚多了。
最后再放一张最开始的流程图
