原文链接:www.yuque.com/wuhaosky/vu…
Vue-router是Vue官方的路由管理库。本文基于Vue-router@3.1.3版本分析Vue-router的技术原理。
一 install函数
和Vuex一样,Vue-router也是作为Vue的插件使用。当使用Vue.use(Vue-router)时,会执行Vue-router的install函数。
1.1 install函数的作用:
1.1.1 为所有Vue组件混合beforeCreate和destoryed钩子方法
在beforeCreate钩子里做了如下事情:
1.为所有Vue组件实例设置_routerRoot实例属性,指向Vue根实例;
2.为Vue根实例设置_router实例属性,指向this.$options.router,也就是Vue-router实例对象;
3.只在Vue根实例的beforeCreate钩子里,调用_router的init方法;
4.设置Vue根实例的_route实例属性为响应式,指向当前路由对象;
5.调用registerInstance方法,在此方法里判断,如果当前Vue组件实例对应的VNode对象,有定义data.registerRouteInstance方法,则执行data.registerRouteInstance方法。
在destroyed钩子里:
1.调用registerInstance方法,在此方法里判断,如果当前Vue组件实例对应的VNode对象,有定义data.registerRouteInstance方法,则执行data.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 () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// _router是当前路由,把_router设置为响应式,当RouterView组件执行render方法时,触发收集渲染函数的观察者;当_router变化时,通知渲染函数的观察者,触发重新渲染。
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 设置当前路由的vue组件实例为this
registerInstance(this, this)
},
destroyed () {
// 设置当前路由的vue组件实例为undefined
registerInstance(this)
}
})
1.1.2 为Vue原型增加属性route
给Vue增加只读的原型属性$router,指向Vue-router实例对象;
给Vue增加只读的原型属性$route,指向当前路由对象;
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
1.1.3 设置Vue全局组件RouterView和RouterLink
RouterView组件比较重要,它是路由出口,在RouterView组件中根据当前路由渲染相应的Vue组件。
1.1.4 设置Vue组件导航钩子的合并策略
设置Vue组件导航钩子(beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave)的合并策略与Vue生命周期钩子的合并策略相同。
1.2 思考:
1.2.1 设置Vue根实例的_router实例属性为响应式的目的是什么?
通过defineReactive对_route属性进行响应式处理,这一步是路由变更时视图可以重渲染的关键。
Vue应用初始化时,会执行RouterView组件的render方法,render方法里有这样一句代码const route = parent.route求值,就会进入route就会被重新赋值,就会进入$route的set方法,在set方法里,调用所有观察者的update方法,包括渲染函数的观察者,触发重渲染。这就是路由变更时视图可以重渲染的原理。
总结:当前路由引用发生改变,就会重新执行****RouterView组件render方法,挂载当前路由对应的Vue组件,从而实现视图更新。
1.2.2 哪些Vue组件实例的VNode的data属性,有定义data.registerRouteInstance方法?
定义data.registerRouteInstance方法的是RouterView组件的直接子组件。也就是当前路由对应的组件。
1.2.3 Vue实例的route属性有什么区别?
route指向当前路由对象。
二 RouterView组件
RouterView组件比较重要,它是路由出口,在RouterView组件中根据当前路由渲染相应的Vue组件。
2.1 RouterView组件做了如下几件事情:
2.1.1 根据当前路由挂载相应的Vue组件
RouterView组件根据当前路由挂载相应的Vue组件,这是最重要的工作。RouterView组件是Vue函数组件,它直接提供了render方法,在render方法使用h(component, data, children)挂载Vue组件。
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
const h = parent.$createElement
const name = props.name
const route = parent.$route
// 省略
const matched = route.matched
const component = matched.components[name]
// 省略
return h(component, data, children)
}
}
2.1.2 当路由引用变更时,更新视图
上文也有介绍,RouterView组件的render方法里通过const route = parent.route是响应式的,所以会触发依赖收集器收集RouterView组件的渲染函数的观察者。当$route引用变更时,通知RouterView组件的渲染函数的观察者,重新执行RouterView组件的render方法。从而挂载当前路由对应的Vue组件,实现视图更新。
2.1.3 设置当前路由对应的Vue组件的实例
目的是为了在导航守卫时,可以拿到当前路由和下一个路由对应的Vue组件实例。
三 VueRouter类
VueRouter构造函数中,根据路由配置参数(options.routes),创建路由匹配器matcher;根据路由模式参数(options.mode),创建历史记录器history。
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
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
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}`)
}
}
}
3.1 路由匹配器matcher
matcher是由createMatcher函数生成,matcher含有两个方法,分别是match方法和addRoutes方法。match方法是返回当前的路由对象,addRoutes方法是往路由映射表里增加路由。路由匹配器主要负责路由匹配和路由映射。
3.1.1 路由匹配
路由匹配是通过path-to-regexp实现的。
3.1.2 路由映射
路由映射是通过nameMap、pathMap实现的,通过path/name找到对应的路由记录。
3.2 历史记录器history
Vue-router的构造函数里,会根据路由模式(默认是hash模式),创建对应的历史记录器。历史记录器主要负责路由跳转、路由回退监听、导航守卫。本文介绍的是hash模式。
3.2.1 路由跳转
路由跳转是通过window.history.pushState实现的。
具体的说,当使用this.$router.push({ name: 'xxx'})方式跳转到新的路由时,会执行历史记录器的transitionTo/confirmTransition方法,更新当前路由,并执行window.history.pushState方法,把新的URL添加到history栈中。
pushState() 方法与设置hash值的区别
HTML5开始提供了对history栈中内容的操作,通过history.pushState/replaceState实现添加URL到history栈中。在某种意义上,调用 pushState() 与 设置 window.location = "#foo" 类似,二者都会在当前页面创建并激活新的历史记录。但 pushState() 具有如下几条优点:
1.新的 URL 可以是与当前URL同源的任意URL 。而设置 window.location 仅当你只修改了哈希值时才保持同一个文件。
2.如果需要,可以不必改变URL就能创建一条历史记录。而设置 window.location = "#foo",只有在当前哈希不是 #foo 的情况下, 才会创建一个新的历史记录项。
3.我们可以为新的历史记录项关联任意数据。而基于哈希值的方式,则必须将所有相关数据编码到一个短字符串里。
3.2.2 路由回退监听
路由回退监听是通过监听popstate事件实现的。对于hash路由,如果不支持popstate事件,则监听hashchange事件。当监听到路由回退,调用历史记录器的transitionTo/confirmTransition方法更新当前路由,从而触发视图更新,更新为当前路由对应的Vue组件。
3.2.3 导航守卫
上文介绍过,不论是路由跳转,还是路由回退,都会执行历史记录器的transitionTo/confirmTransition方法,由此可以transitionTo/confirmTransition方法是非常重要的。在transitionTo/confirmTransition方法里,除了负责更新当前路由和添加新的URL到history栈中,它还负责导航守卫。
Vue-router提供了三种类型的导航守卫:
1.全局路由守卫
全局路由守卫有3个钩子方法,分别是:beforeHooks、beforeResolve、afterHooks。
2.组件内守卫
组件内的守卫有3个钩子方法,分别是:beforeRouteLeave、beforeRouteUpdate、beforeRouteEnter。
3.路由独享的守卫
路由独享的守卫有1个钩子方法,它是:beforeEnter,这个钩子方法在定义路由配置时直接定义。
Vue-router完整的导航解析流程:
-
导航被触发;
-
在失活的组件里调用组件内的守卫 beforeRouteLeave;
-
调用全局路由守卫 beforeHooks;
-
在重用的组件里调用组件内的守卫 beforeRouteUpdate;
-
在被激活的组件里调用路由独享的守卫 beforeEnter;
-
解析异步路由组件。
-
在被激活的组件里调用组件内的守卫 beforeRouteEnter;
-
调用全局路由守卫 beforeResolve;
-
导航被确认,更新当前路由;
-
调用全局路由守卫 afterHooks;
-
触发 DOM 更新。
-
用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
3.3 思考:
3.3.1.如何监听params的变化?
params的变化并没有改变当前路由的引用,因此并不会触发进入/离开的导航守卫。你可以通过对 $route 对象进行深度观察来应对这些变化,或使用组件内守卫的beforeRouteUpdate钩子。
四 参考文章
【vue-进阶】之vue-router源码分析:juejin.im/post/684490…
vue-router从源码到实践到进阶:juejin.im/post/684490…
简单聊聊H5的pushState与replaceState:juejin.im/post/684490…
需要技术交流可以加微信。