Vue-Router 源码解析
Vue-Router
Vue-Router是vue的路由插件,能够方便我们进行路由管理,是单页面开发不可或缺的一部分。vue-router使用相对简单,编码格式化合理,能够极大地帮助我们开发单页面应用。
tips:本文不讲解vue-router的基本使用,只对源码进行解析
vue-router官方指导网站:点我
源码结构以及状态图
vue-router源码文件较多,但有部分是功能性模块,层次分明
关键文件解析:
源码结构:
components:
link.js:RouterView组件定义
view.js:RouterLink组件定义
history:
abstrct.js:AbstractHistory类定义
hash.js:HashHistory类定义
html5.js:Html5History类定义
base.js:History类定义
errors.js
utils:
route.js:createRouter路由生产者
其他
index.js:VueRouter类定义
create-matcher.js:Matcher生产者
create-router-map.js:创建路由映射表
install.js:vue插件安装调用
原理结构图:
install.js
对于Vue插件,都是通过Vue实例创建调用install.js文件进行安装,而vue-router的install主要完成了5件事:
* Vue.use(插件)来注册插件的时候会找到插件的install方法进行执行
* install主要有以下几个目的:
* (1)子组件通过从父组件获取router实例,从而可以在组件内进行路由切换、在路由钩子内设置callback、获取当前route等操作,所有router提供的接口均可以调用
* (2)设置代理,组件内使用this.router就等同于获得router实例,使用this._route就等同于获取当前的_route
* (3)设置_route设置数据劫持,也就是在数值变化时,可以notify到watcher
* (4)注册路由实例,router-view中注册route
* (5)注册全局组件RouterView、RouterLink
源码如下:
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install(Vue) { // 当用户执行 Vue.use(VueRouter) 的时候,实际上就是在执行 install 函数
// 避免重复安装
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue // 用一个全局的 _Vue 来接收参数 Vue, 通过这种方式拿到 Vue,不用单独 import
const isDef = v => v !== undefined // 判断参数是否为 undefined
// 从父节点拿到registerRouteInstance,注册路由实例
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 和 destroyed 钩子函数注入到每一个组件中,每个组件都会在对应生命周期阶段执行
Vue.mixin({
// 对于根 Vue 实例而言,执行beforeCreate时定义了 this._routerRoot 表示它自身
beforeCreate() {
// 验证vue是否有router对象了,如果有,就不再初始化了
if (isDef(this.$options.router)) {
this._routerRoot = this
// 将_router对象挂载到根组件元素_router上
this._router = this.$options.router // this._router 表示 VueRouter 的实例 router,它是在 new Vue 的时候传入的
// 调用初始化init方法,建立路由监控 index.js中定义了
this._router.init(this)
// 设置 _route 为 响应式的 把 this._route 变成响应式对象
// 劫持数据_route,一旦_route数据发生变化,通知router-view执行render方法
Vue.util.defineReactive(this, '_route', this._router.history.current) // defineReactive的作用就是给对象的属性进行简单的数据观测,一旦值获取或者设置就会触发一些行为.
} else {
// 子组件从父组件获取routeRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed() {
registerInstance(this)
}
})
// 给 Vue 原型上定义了 $router 和 $route 2 个属性的 get 方法
// 设置代理 this.$router === this._routerRoot._router,组件内部通过
// this.$router调用_router
Object.defineProperty(Vue.prototype, '$router', {
get() { return this._routerRoot._router }
})
// 组件内部通过this.$route调用_route
Object.defineProperty(Vue.prototype, '$route', {
get() { return this._routerRoot._route }
})
// 注册RouterView、RouterLink组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 定义了路由中的钩子函数的合并策略,和普通的钩子函数一样
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
可以看到,install时运用了Vue的响应式劫持_route,以确保路由变化时能够调用render更新router-view组件,同时再通过Object.defineProperty在Vue的原型上设置router和route,使得所有vue组件可以访问。而在混入的beforeCreate一开始对options.router进行判断,判断是否是根Vue实例,如果不是则去根vue实例中获取routeRoot,确保所有vue实例共用一个路由器。
class VueRouter
接着,我们看看VueRouter内部是怎样实现路由管理的。
首先,Vue.use()会调用构造函数,生成必须的变量,并且根据mode选项生成对应的History管理器:
// 构造函数
constructor(options: RouterOptions = {}) {
this.app = null // 根vue实例
this.apps = [] // 存放正在被使用的组件(vue实例) 保存持有 $options.router 属性的 Vue 实例
this.options = options // vueRouter实例化时传来的参数,即 router.js 中配置的内容,mode,routes等 传入的路由配置
this.beforeHooks = [] // 存放各组件的全局beforeEach钩子
this.resolveHooks = [] // 存放各组件的全局beforeResolve钩子
this.afterHooks = [] // 存放各组件的全局afterEach钩子
this.matcher = createMatcher(options.routes || [], this) // 路由匹配器 由createMatcher生成的matcher,里面有一些匹配相关方法
let mode = options.mode || 'hash' // router 模式mode
// 模式的回退或者兼容方式,若设置的mode是history,而js运行平台不支持 supportsPushState 方法,自动回退到hash模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false // 判断条件
if (this.fallback) {
mode = 'hash' // 满足条件,回退到 hash 模式
}
// 若不在浏览器环境下,强制使用abstract模式
if (!inBrowser) {
mode = 'abstract'
}
// 赋值 mode
this.mode = mode
// 对于不同的模式mode ,使用对应的History管理器去管理history this.history 表示路由历史的具体的实现实例
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}`)
}
}
}
然后我们再看到install时调用的init方法,初始化需要通过这一方法第一次确定路由,渲染视图,因此我们可以通过它看到基本的过程:
// 传入的是Vue实例,然后存储到 this.apps 中;只有根 Vue 实例会保存到 this.app 中
init(app: any /* Vue component instance */) {
// assert是个断言,测试install.install是否为真,为真,则说明vueRouter已经安装了
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 将组件vue实例推入到apps中,install里面最初是将vue根实例推进去的
this.apps.push(app)
// app被destroyed时候,会$emit ‘hook:destroyed’事件,监听这个事件,执行下面方法
// 从apps 里将app移除
app.$once('hook:destroyed', () => {
// 找到apps中这个app的index
const index = this.apps.indexOf(app)
// 移除app
if (index > -1) this.apps.splice(index, 1)
// 确保 this.app 为一个实例或null 而不是undefined
if (this.app === app) this.app = this.apps[0] || null
})
// this.app 有指向,此实例不是根vue实例,返回
if (this.app) {
return
}
// 此实例是根vue实例
// 新增一个history,并添加route监听器
//并根据不同路由模式进行跳转。hashHistory需要监听hashchange和popshate两个事件,而html5History监听popstate事件
this.app = app
const history = this.history
if (history instanceof HTML5History) {
// 获取当前location并调用transitionTo做路由切换
history.transitionTo(history.getCurrentLocation()) //HTML5History在constructor中包含了监听方法,因此这里不需要像HashHistory那样setupListner
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
// 获取当前location并调用transitionTo做路由切换
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 将apps中的组件中的 _route 全部更新至最新的
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
可以看到,在init中,把所有调用它的app也就是vue实例,放到了this.apps数组中,并且监听他们的destroyed声明周期。然后判断是不是根vue实例,如果是,则根据前面构造函数确定的history,调用方法获取当前location,然后再调用transitionTo做路由切换。
base.js
那我们再看到 history/base.js 中定义的transitionTo,confirmTransition,updateRoute,是怎么做到路由的切换的。
// transitionTo 首先根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径
// 接下来就会执行 confirmTransition => updateRoute 方法去做真正的切换
transitionTo(
location: RawLocation,
onComplete ?: Function,
onAbort ?: Function
) {
// 调用match方法,找到匹配的新线路
const route = this.router.match(location, this.current) // 这里 this.current 是 history 维护的当前路径,它的初始值是在 history 的构造函数中初始化的:this.current = START
this.confirmTransition(
route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
// fire ready cbs once
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)
})
}
}
)
}
// 路由的切换
// 首先定义了 abort 函数,然后判断如果满足计算后的 route 和 current 是相同路径的话,则直接调用 this.ensureUrl 和 abort
// 接着又根据 current.matched 和 route.matched 执行了 resolveQueue 方法解析出 3 个队列
confirmTransition(route: Route, onComplete: Function, onAbort ?: Function) {
const current = this.current
const abort = err => {
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort(new NavigationDuplicated(route))
}
// 根据 current.matched 和 route.matched 执行了 resolveQueue 方法解析出 3 个队列
const { updated, deactivated, activated } = resolveQueue(
this.current.matched, // 一个 RouteRecord 的数组
route.matched
)
// 拿到 updated、activated、deactivated 3 个 RouteRecord 数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数
/**
* 首先构造一个队列 queue,依次序concat钩子函数数组
* 然后再定义一个迭代器函数 iterator;
* 最后再执行 runQueue 方法来执行这个队列
*/
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated), // 在失活的组件里调用离开守卫
// global before hooks
this.router.beforeHooks, // 调用全局的 beforeEach 守卫
// in-component update hooks
extractUpdateHooks(updated), // 在重用的组件里调用 beforeRouteUpdate 守卫
// in-config enter guards
activated.map(m => m.beforeEnter), // 在激活的路由配置里调用 beforeEnter
// async components
resolveAsyncComponents(activated) // 解析异步路由组件
)
this.pending = route
// 执行每一个 导航守卫 hook,并传入 route、current 和匿名函数
// 当执行了匿名函数,会根据一些条件执行 abort 或 next,只有执行 next 的时候,才会前进到下一个导航守卫钩子函数中
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 调用 util/async.js 中的runQueue
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
}
updateRoute(route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
transitionTo中调用的match方法定义在了index.js中
// 输入参数raw,current,redirectedForm,结果返回匹配 route
match(
raw: RawLocation, // /user/4739284722这种形式,类似route的path
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
可以看到,先通过match方法匹配出新路由的路径,然后在confirmTransition中与旧路径做对比,如果不同,则根据两个路径解析出三个队列分别表示更新路径,新增路径,消灭路径,对应方法如下:
/**
* 由于路径是由 current 变向 route,那么就遍历对比 2 边的 RouteRecord,找到一个不一样的位置 i
* 那么 next 中从 0 到 i 的 RouteRecord 是两边都一样,则为 updated 的部分;
* 从 i 到最后的 RouteRecord 是 next 独有的,为 activated 的部分;
* 而 current 中从 i 到最后的 RouteRecord 则没有了,为 deactivated 的部分
*
*/
function resolveQueue(
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
拿到 updated、activated、deactivated 3 个 RouteRecord 数组后,接下来就是执行一系列的钩子函数,最后再是调用updateRoute更新history.current属性,触发根组件的_route的变化,最后更新视图。
向外暴露的方法
vue-router同时向外部暴露一些方法供开发者扩展,比如添加钩子函数,主动跳转路由等,定义如下
/**
* 全局路由钩子:
* 在路由切换的时候被调用,可以自定义fn。可以在main.js中使用VueRoute实例router进行调用。比如:
* router.beforeEach((to,from,next)=>{})
*
*/
// 将回调方法fn注册到beforeHooks里。registerHook会返回fn执行后的callback方法,功能是将fn从beforeHooks删除
beforeEach(fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
// 将回调方法fn注册到resolveHooks里。registerHook会返回fn执行后的callback方法,功能是将fn从resolveHooks删除。
beforeResolve(fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
// 将回调方法fn注册到afterHooks里。registerHook会返回fn执行后要调用的callback方法,功能是将fn从afterHooks删除
afterEach(fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
// 首次路由跳转完成时被调用
onReady(cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
// 报错
onError(errorCb: Function) {
this.history.onError(errorCb)
}
// 新增路由跳转
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)
}
}
// 路由替换
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
// 前进n条路由
go(n: number) {
this.history.go(n)
}
// 后退一步
back() {
this.go(-1)
}
// 前进一步
forward() {
this.go(1)
}
// 根据路径或者路由获取匹配的组件,返回目标位置或是当前路由匹配的组件数组
getMatchedComponents(to?: RawLocation | Route): Array<any> {
const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if (!route) {
return []
}
return [].concat.apply([], route.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key]
})
}))
}
结语
小结:
路由更新方法:
(1)主动触发
router-link绑定了click方法,触发history.push或者history.replace,从而触发history.transitionTo
transitionTo用于处理路由的转换,其中包含了updateRoute用于更新_route
在beforeCreate中有劫持_route的方法,当_route变化后,触发router-view变化
(2)地址变化
HashHistory和HTML5History会分别监控hashchange和popstate来对路由变化作对应的处理
HashHistory和HTML5History捕获到变化后会执行push或replace方法,从而调用transitionTo,
然后就是transitionTo用于处理路由的转换,其中包含了updateRoute用于更新_route
在beforeCreate中有劫持_route的方法,当_route变化后,触发router-view变化
源码概述:
1 安装插件:
混入beforeCreate生命周期处理,初始化 _routerRoot,_router,_route等
全局设置vue静态访问route,方便在组件中调用方法改变_route
完成了router-link和 router-view 两个组件的注册,router-link用于触发路由的变化,router-view作 为功能组件,用于触发对应路由视图的变化
2 根据路由配置生成router实例:
根据配置数组routes生成路由配置记录表
根据mode生成监控路由变化的history对象
3 将router实例传入根vue实例
根据beforeCreate混入,为根vue对象设置了劫持字段_route,用户触发route-view的变化
调用init()函数,完成首次路由的渲染,首次渲染的调用路径是调用history.transitionTo方法,
根据router的match函数,生成一个新的route对象
接着通过confirmTransition对比一下新生成的route和当前的route对象是否改变,
改变的话触发updateRoute,更新history.current属性,触发根组件的_route的变化,
从而导致组件调用render函数,更新router-view
另一种更新路由的方式是主动触发
router-link绑定了click方法,触发history.push或history.replace,从而触发history.transitionTo,
同时会监控hashchange和popstate来对路由变化作对应的处理
本文仅分析了源码中最为核心的部分,很多如递归路由,不同路由模式history的异同等,都没有分析,也欢迎大家和我一起讨论学习
编者github地址:传送
qq:1073490398
wechat:carfiedfeifei