Vue Router原理學習
這裡大概分析一下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
看上去只是簡單的一句話。下面是它的原始碼
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原理的簡單介紹。