從 Vue.use 開始分析 Vur-Router 的源碼實現

SPA 單頁應用

隨着前端框架(React/Vue/Angular)等漸進式框架發展,配合 webpack 等打包工具,完成單頁面的構建愈來愈簡單.javascript

對比傳統多頁面應用,單頁面應用優點:html

  • 更好的交互體驗
  • 更好的先後端分離開發

缺點:前端

  • 首屏加載資源大
  • 不利於SEO
  • 須要配合前前端路由系統實現跳轉

爲了解決單頁面系統中,頁面跳轉路由實現,和改變視圖的同時不會向後端發出請求。引入了前端路由系統 React-Router-Dom/vue-router 等前端路由庫.vue

經過瀏覽器地址欄的 hashChange 和 HTML5 提供的 History interface 實現的地址改變觸發視圖改變.html5

從vue-router看前端路由的兩種實現

這是一段簡單的示例程序, vue-router 在 vue 程序中的簡單應用java

示例

<html>
<head>
  <meta charset="utf-8">
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
</head>

<body>
  <div id="app">
    <h1>Hello App!</h1>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
    <router-view></router-view>
  </div>
  <div id="root">
    <h1>Hello root!</h1>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
    <router-view></router-view>
  </div>

  <script> const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } const routes = [{ path: '/foo', component: Foo }, { path: '/bar', component: Bar }] const router = new VueRouter({ routes }) new Vue({ router }).$mount('#app') new Vue({ router }).$mount('#root') </script>
</body>
</html>
複製代碼

Vue.use 註冊

script 加載

上面這段示例代碼使用了 umd 模塊的加載方式,直接 script 加載到 window 上webpack

在加載 router 代碼塊的時候內部會判斷加載方式,若是是 script 加載,會直接調用 Vue.use 方法初始化使用 Vue-router 插件git

// vue-router/src/index.js
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
複製代碼

ES Module 加載

若是是基於 webpack 的打包方式的程序,還須要在引入了 vue-router 以後使用如下代碼把 router 加載安裝到咱們的 vue 程序中,實際上這是一個 vue-router 集成的開始github

Vue.use(Router)
複製代碼

Vue.use 會調用 Router 內部實現的 install 方法,這是使用router 的入口web

install 實現

首先貼上刪除了部分不作分析的部分的源代碼

import View from './components/view'
import Link from './components/link'

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  const isDef = v => v !== undefined

  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
      }
    }
  })

  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)
}

複製代碼

這裏的 install 中註冊 router 到 Vue 的過程當中,作了這幾件事情

  • 保證只註冊一次 router 到 Vue
  • 混入了核心的 router 實現開始入口
  • 定義了了 r o u t e r router route 的 getter
  • 全局註冊了 Link 和 View 組件

install 核心

核心的混入部分經過混入 beforeCreate 鉤子中,實現了在每一個組件中對根示例 _routerRoot 的訪問.

isDef 方法判斷了 Vue 實例的配置中是否有 router 定義,而 router 只在根示例中有定義,也就是:

new Vue({ router }).$mount('#app')
複製代碼

進入根實例的條件以後,在根實例上定義了 _routerRoot 保持對自己的訪問地址.在後面的全部組件中,給組件共享根組件的訪問.

而後把 Router 實例掛載到根 Vue 實例上,保持 Router 實例的訪問.

執行 Router 實例的init 方法, 該方法定義在 Router 的類定義中是 vue-router 的核心初始化流程,入參根組件.

調用 Vue.util.defineReactive 定義響應式對象,後續的組件更新依賴於 Vue 的響應式原理, 經過響應式對象的依賴收集,派發更新流程通知視圖的更新

vue-router 核心

vue-router 的核心實現是在 src/index.js 中定義的 VueRouter 類,類中實現了初始化邏輯,定義了實如下的例的屬性和方法:

VueRouter 構造器

這是 VueRouter 的構造函數.

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}`)
        }
    }
  }
複製代碼

構造函數裏面作的事情很簡單:

  • 對參數進行了保存
  • 定義了保存 Vue 根實例的 app apps
  • 定義了鉤子函數的保存地址
  • 構造出 matcher 路由匹配器
  • 判斷環境配置,絕對最後使用的路由模式
  • 根據路由模式生成路由 History 對象

構造器的核心就是根據環境和配置生成路由模式

這裏能夠看到優先使用配置項中的 mode 若是沒有配置則使用 hash

let mode = options.mode || 'hash'
複製代碼

而後當配置中使用了 history 模式的時候,判斷是否支持 history ,不支持則降級使用 hash

this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
  mode = 'hash'
}
複製代碼

不是在瀏覽器環境中則使用本身構造的路由事件系統來實現 History

if (!inBrowser) {
  mode = 'abstract'
}
複製代碼

VueRouter init 初始化方法

init (app: any /* Vue component instance */) {
    process.env.NODE_ENV !== 'production' &&
      assert(
        install.installed,
        `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
          `before creating root instance.`
      )
    // 對照本文檔開始示例,一個 router 對象可能被多個 app 所使用,在後續的路由變動的時候,經過更改 apps 中全部 app 的響應式路由數據,觸發視圖變動.
    this.apps.push(app)
    
    // this.app 保存了是否還有在使用該 router 實例的 app ,也就是 VUE 應用
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    // 針對 hash 和 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
      })
    })
  }
複製代碼

小結 這裏初始化函數作了幾件事情:

  • 保證在創造 VUE 實例以前安裝了 router 也就是 Vue.use(Router)
  • 記錄調用了 router 實例的 init 的 vue 實例
  • 開始初始化路由變化監聽器
  • 初始化變化監聽回調,以觸發響應視圖
  • 調用第一次跳轉當前路由,初始化視圖.

VueRouter 實例屬性

  • router.app
  • router.mode
  • router.currentRoute

VueRouter 實例API

這裏的 API 一部分對路由操做的都是對 History 對象上具體的方法的代理.

  • router.beforeEach
  • router.beforeResolve
  • router.afterEach
  • router.push
  • router.replace
  • router.go
  • router.back
  • router.forward
  • router.getMatchedComponents
  • router.resolve
  • router.addRoutes
  • router.onReady
  • router.onError

HTML5History

這是基於原生的 HTML5 History interface 的路由監聽器實現(刪減不作分析部分)

這裏 HTML5History 派生自 History

History 類實現了路由的核心跳轉處理.後面會作分析

HTML5History類實現了:

  • 初始化 HTML5History 監聽的方法
  • 路由跳轉操做方法
  • history 模式下的獲取完整路由方法

其實就是對 各類mode 之間的不一樣點提取到這裏進行特殊處理,基礎能力都定義在基類 History 中

/* @flow */

import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'

export class HTML5History extends History {
  constructor (router: Router, base: ?string) {
    super(router, base)
  }

  // 定義了初始化監聽路由變化的方法
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    // 滾動行爲處理
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    // 路由變化響應函數,調用核心跳轉實現 transitionTo
    const handleRoutingEvent = () => {
      const current = this.current

      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    //監聽 popstate ⌚事件
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }

  go (n: number) {
    window.history.go(n)
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  // 肯定路由是否正確,不正確向state 裏面推入正確路由
  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }

  getCurrentLocation (): string {
    return getLocation(this.base)
  }
}

export function getLocation (base: string): string {
  let path = decodeURI(window.location.pathname)
  if (base && path.toLowerCase().indexOf(base.toLowerCase()) === 0) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

複製代碼

HashHistory

HashHistory 實現中實現的方法實際上與 HTML5History 中實現的是一致的,只是在路由操做中 添加了對 hash 標識符 # 的判斷,跳轉路由的生成不同,要多一些反作用的操做 hash

這裏不作過多的分析.

/* @flow */

import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { getLocation } from './html5'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }

  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
    )
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  go (n: number) {
    window.history.go(n)
  }

  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }

  getCurrentLocation () {
    return getHash()
  }
}

function checkFallback (base) {
  const location = getLocation(base)
  if (!/^\/#/.test(location)) {
    window.location.replace(cleanPath(base + '/#' + location))
    return true
  }
}

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf('#')
  // empty path
  if (index < 0) return ''

  href = href.slice(index + 1)
  // decode the hash but not the search or hash
  // as search(query) is already decoded
  // https://github.com/vuejs/vue-router/issues/2708
  const searchIndex = href.indexOf('?')
  if (searchIndex < 0) {
    const hashIndex = href.indexOf('#')
    if (hashIndex > -1) {
      href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex)
    } else href = decodeURI(href)
  } else {
    href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex)
  }

  return href
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

複製代碼

History 核心

上面提到的兩個 HTML5History 和 HashHistory 實際上都是派生自 History 基類,在基類上定義了 路由監聽的核心邏輯,接下來咱們來分析這部分的核心代碼

因爲這部分代碼輔助方法較多,不展現過多的代碼,只摘錄部分核心邏輯代碼展現:

/* @flow */

import { _Vue } from '../install'
import type Router from '../index'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { warn } from '../util/warn'
import { START, isSameRoute } from '../util/route'
import {
  flatten,
  flatMapComponents,
  resolveAsyncComponents
} from '../util/resolve-components'
import {
  createNavigationDuplicatedError,
  createNavigationCancelledError,
  createNavigationRedirectedError,
  createNavigationAbortedError,
  isError,
  isNavigationFailure,
  NavigationFailureType
} from '../util/errors'

export class History {
  constructor (router: Router, base: ?string) {
    ...
  }

  // 外部經過 listen 註冊路由變化回調到這裏,當路由跳轉觸發回調函數通知外部執行對應方法,入參跳轉的 route 對象.
  listen (cb: Function) {
    this.cb = cb
  }

  onReady (cb: Function, errorCb: ?Function) { ... } 

  onError (errorCb: Function) { ... }

  // 路由跳轉函數
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    try {
      //調用 match方法獲得匹配的 route對象
      route = this.router.match(location, this.current)
    } catch (e) {...}
    // 核心跳轉邏輯,會處理路由守衛鉤子方法,生成鉤子任務隊列,處理過渡等.
    this.confirmTransition(
      route,
      () => {
        // 跳轉處理完成回調中,調用 updateRoute 實現跳轉,觸發視圖更新
        const prev = this.current
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })

        // fire ready cbs once
        if (!this.ready) { ... }
      },
      err => { ... }
    )
  }

  // 路由跳轉前處理函數,處理過渡,鉤子函數隊列
  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    const abort = err => {...}
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1

     // 若是當前路由和以前路由相同 確認url 直接return
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      return abort(createNavigationDuplicatedError(current, route))
    }

    // 經過異步隊列來交叉對比當前路由的路由記錄和如今的這個路由的路由記錄 
    // 爲了能準確獲得父子路由更新的狀況下能夠確切的知道 哪些組件須要更新 哪些不須要更新
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )

    // 在異步隊列中執行響應的勾子函數
    // 經過 queue 這個數組保存相應的路由鉤子函數
    const queue: Array<?NavigationGuard> = [].concat(
      /// leave 的勾子
      extractLeaveGuards(deactivated),
      // 全局的 before 的勾子
      this.router.beforeHooks,
      // in-component update hooks
      extractUpdateHooks(updated),
      // 將要更新的路由的 beforeEnter勾子
      activated.map(m => m.beforeEnter),
      // 異步組件
      resolveAsyncComponents(activated)
    )

    this.pending = route

    // 隊列執行的 iterator 遍歷函數 
    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false) {
            // next(false) -> abort navigation, ensure current URL
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) {
            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(createNavigationRedirectedError(current, route))
            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)
      }
    }

    // 遞歸回調方式運行隊列函數
    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(createNavigationCancelledError(current, route))
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => {
              cb()
            })
          })
        }
      })
    })
  }

  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }
}


function resolveQueue ( current: Array<RouteRecord>, next: Array<RouteRecord> ): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}

複製代碼

小結

這裏核心實現了路徑切換的邏輯,是整個router 路由切換跳轉的實現.主要實現瞭如下功能

  • 註冊跳轉完成回調,以觸發外部視圖更新,入參 路由切換的 路由對象
  • 實現了路由跳轉函數 transitionTo ,在 transitionTo 完成回調中調用 updatRoute 觸發 listen 註冊的回調執行.
  • 路由跳轉前處理函數,處理過渡,鉤子函數隊列,運行鉤子隊列,遞歸判斷路由改變等方法

路由更新流程

history.listen(callback) ==> $router.push() ==> HashHistory.push() ==> History.transitionTo() ==>
History.confirmTransition() ==> History.updateRoute() ==> {app._route = route} ==> vm.render()
複製代碼
相關文章
相關標籤/搜索