# vue-router 源码解读
# vue.use(VueRouter)
VueRouter 作为 Vue 的一个插件,是通过 vue.use(VueRouter) 进行注册,调用 vue.use(VueRouter) 会给每个组件 mixin 两个钩子函数,分别是 beforeCreate 和 destroyed,并且在 vue.use(VueRouter) 时也会在 Vue.prototype 上定义 $router 和 $route,全局注册 RouterView 和 RouterLink。
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)
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
# matcher
在 VueRouter 的构造函数中会创建 matcher,由于 routes 是一个树状结构的数组,所有在创建 matcher 的过程中会递归遍历所有的 routes 和 routes.children,最终生成 nameMap,pathMap,pathList。VueRouter 中使用 path-to-regexp 将路径转成正则表达式
# 为什么嵌套路由的子路径的 path 可以不用 /
开头
因为在生成 matcher 时,在递归遍历的过程中,如果 child.path 不是以 /
开头,就会将 child.path 改成 parent.path + '/' + child.path
function normalizePath (
path: string,
parent?: RouteRecord,
strict?: boolean
): string {
if (!strict) path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)
}
# 路由切换
在 VueRouter 中有 location 的概念,在切换路由时会根据传入的 location 和当前的 route 计算出一个新的 route ,然后准备导航到这个新的 route,在导航到新的 route 之前,会计算出经过这次路由切换会让哪些路由记录 updated, deactivated, activated。然后得到这些路由的各个路由守卫,以队列的形式依次执行这些路由守卫,这也是为什么要在路由守卫中执行 next 方法的原因,执行 next 访问意味着从路由守卫队列中取出下一个路由守卫并执行它。
// 得到 updated, deactivated, activated 的路由记录
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 路由守卫队列
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
// 执行路由守卫
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)
}
}
// 依次取出路由守卫
function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
// 运行队列
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()
})
})
}
})
})
在根据新的 route.matched 和当前 route.matched 得到经过本次切换需要 updated, deactivated, activated 的路由记录,然后得到这些路由记录的需要执行的路由守卫,这些路由守卫会被包裹到一个包裹函数中,上述代码中的 hook 就是包裹函数,VueRouter 会在包裹函数中调用项目中定义的路由守卫,通过这种方式实现给路由守卫绑定上下文,也通过这种方式使得组件内部的 beforeRouteEnter 守卫的第三个参数 next 可以传递一个回调函数,在这个回调函数被调用的时候能够给它传当前组件的实例作为参数
# 浏览器地址栏 url
只讨论 hash 模式,并且浏览器不支持 pushState 的情况
在 VueRouter 中 history 对象,history 提供了一些路由切换方法(比如:push,replace 等) ,改变浏览器地址栏 url 的方法以及给 window 绑定 hashchange 事件。
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}`)
}
}
# 确保浏览器地址栏 url 带 #
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
在 new VueRouter() 就会生成 history 对象。并且 history 的构造函数中会执行 ensureSlash ,这个方法用来确保浏览器地址栏中有一个 #
,这也是为什么在打开一个页面没有带 #
,浏览器地址栏会替换成带 #
的原因.例如:
http://www.xxx.com/ => http://www.xxx.com/#/
# 监听浏览器地址栏 hash 的变化
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
}
在 router.init 中会有一次路由切换,在路由切换之后会 调用 setupListeners 方法去监听浏览器地址栏 hash 的变化
# router-view
router-view 可以嵌套,routes 也是可以嵌套的,router-view 的嵌套和 routes 的嵌套是对应的,利于这一规则,就可以根据 router-view 的嵌套深度,在 route.matched 上得到这个 router-view 对应的 component
router-view 的 render 简化版本
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
const h = parent.$createElement
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
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
//...something
const matched = route.matched[depth]
const component = matched && matched.components[name]
//...something
return h(component, data, children)
}
render 中的 parent 是 view-router 的父组件,render 中的 data 是通过 view-router 渲染出的组件的 data,给 data 设置 routerView 为 true,在 while 循环时就可以得到嵌套深度。
# 为什么浏览器地址栏上的路径变化之后,视图也会对应变化?
调用 vue.use(VueRouter) 会给组件 mixin beforeCreate 构造函数,在 beforeCreate 会将 _route 定义成响应式的。并且 Vue.prototype.$route 引用的就是 _route,在 view-router 的 render 中访问了 $route,所以只要 _route 的值发生变化,view-router 的 render 就会重新渲染。
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
在路由切换完之后会修改 _route 的值
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
history.listen (cb: Function) {
this.cb = cb
}
updateRoute (route: Route) {
const prev = this.current
this.current = route
// 修改 app._route,使 router-view 重新执行 render
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}