这里大概分析一下vue router整体流程, push方法原理和如何监听用户修改url,然后进行视图渲染。
在一个项目中我们可能会这么去配置一个router
Vue.use(VueRouter) const routes: Array<RouteConfig> = [ { path: '/', redirect: '/login', component: login }, ] const router = new VueRouter({ base: process.env.BASE_URL, routes })
在这段代码中做了三件事情。第一:Vue.use 第二:设置配置项 第三:new 一个Router实例。下面分析下这第一和第三做了什么事情。
Vue.use
看上去只是简单的一句话。但是在这句话里面vue会加载它的插件,会执行插件定义的install方法。下面是Vue Router Install的源码
mport View from './components/view' import Link from './components/link' export let _Vue export function install (Vue) { if (install.installed && _Vue === Vue) return install.installed = true _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 () { 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) const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }
这段代码的执行时期是在Vue初始化全局API的时候执行,即Vue源码中一个叫做initGlobalAPI的方法吗,在这里Vue会主动去执行插件的install方法。对于Vue Router来说就是上面这段install的代码。
install代码主要做了几件关键的事情。关注这段代码(如下)。
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) } })
这段代码做了一件事情,在Vue组建中都加入一个混入beforeCreate生命钩子函数。这个函数里面有一句这样的话,这句话的作用就是在Vue上添加一个_route属性,并且他是响应式的,在我们使用push方法的时候会给这个属性重新赋值,从而触发拦截器的set方法然后去进行视图的更新(这部分需要对Vue响应式有所了解)
Vue.util.defineReactive(this, '_route', this._router.history.current)
在上面整个install方法还有一个地方值得关注。
this._router.init(this)
现在看看init的作用是什么
init (app: any /* Vue component instance */) { ...省略 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 ) }
history.listen(route => { this.apps.forEach(app => { app._route = route }) }) }
开头有一个if语句。if下有一个setupListeners的方法,在不同的路由模式这个方法作用有所区别。但是共同点就是用来监听用户修改url的时候,然后根据用户的修改的url进行路由匹配从而更新页面的渲染
这个if语句下有一段这样的话,这句话就是刚刚开始的时候讲的给_route属性重新赋值,从而触发拦截器set然后进行视图的更新
history.listen(route => { this.apps.forEach(app => { app._route = route }) })
现在就介绍完了在Vue.use的时候做得几件重要的事情
new Router
这个阶段做的事情不是很多,第一件事情,就是根据用户传入过来的router选项,然后根据这个选项整理为Vue Router里面所要使用的数据结构。 第二件事情就是根据不同的路由模式去创建不同的路由实例,代码如下:
constructor (options: RouterOptions = {}) { ....省略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}`) } } }
push方法
这里以hash模式的为例子,源码是这样的:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) }
push本质上执行的就是transitionTo方法,这个方法源码是这样的。
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) ... }) }
第一句话的match方法就是用来匹配当前路由对应的组件。然后在comfirmTransition有一个updateRoute的方法。这个方法做了一件事情就是触发一个cb,代码如下:
updateRoute (route: Route) { this.current = route this.cb && this.cb(route) }
那这个cb是什么,回到前面的init方法中:
history.listen(function (route) { this$1.apps.forEach(function (app) { app._route = route; }); });
//history的listen方法
listen (cb) {
this.cb = cb;
}
listen方法的cb其实就是上面的函数,所以执行cb本质就是在执行函数参数,执行函数参数就会执行对_route属性的赋值,。从而触发set,然后完成视图更新。
现在整个push如何触发视图的重新渲染已经清晰了,所以这也解释了为什么vue是一个单页面,本质上就是在对dom的替换,而不是在请求页面。
如何监听url改变
我们知道vue路由其中的两种模式是hash和history。这里只介绍如何监听hash。hash有一个典型的标志就是#符号。假如用户改变#后面的url,浏览器是不会向后台发送请求的。并且会在浏览器种产生一条记录,就是说用户点击浏览器前进或者后退是可以追溯到你的hash改变记录。假如#后面的值发生改变浏览器提供了特定的事件hashchange可以对这个行为进行监听。
基于这个特点,那么vue就利用这个监听的机制。每次用户改变url的时候,就会触发监听方法。然后vue就拿取用户改变的url后缀。然后去匹配相应的组件,然后去完成视图的更新。这个就是改变url的渲染原理。
我们回到init方法上来。
init (app /* Vue component instance */) {
...省略
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 ); } }
关注有一个这段
history.setupListeners()
然后在hashHistory这个方法是这样的:
setupListeners () { ...省略 const eventType = supportsPushState ? 'popstate' : 'hashchange' window.addEventListener( eventType, handleRoutingEvent ) this.listeners.push(() => { window.removeEventListener(eventType, handleRoutingEvent) }) }
看到这段代码。本质上hash模式在浏览器支持的情况下优先使用popstate,其次才是使用hashchange监听。所以就是,在beforeCreate生命周期中,就已经开始了这个监听。从而实现用户在修改url的时候进行
视图的更新。
上面的内容大致vue router原理的简单介绍。