1. 程式人生 > 實用技巧 >Vue Router原理學習

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原理的簡單介紹。