【vue-系列】vue-router源碼分析

歷史回顧:javascript

這是一篇集合了從如何查看 vue-router源碼(v3.1.3),到 vue-router源碼解析,以及擴展了相關涉及的知識點,科普了完整的導航解析流程圖,一時讀不完,建議收藏。html

如何查看vue-router源碼

查看源碼的方法有不少,下面是我本身讀vue-router源碼的兩種方法,你們都是怎麼查看源碼的,歡迎在評論區留言。前端

查看vue-router源碼 方法一:
  1. 下載好 vue-router 源碼,安裝好依賴。
  2. 找到 build/config.js 修改 module.exports,只保留 es,其它的註釋。
module.exports = [
    {
        file: resolve('dist/vue-router.esm.js'),
        format: 'es'
    }
].map(genConfig)
複製代碼
  1. 在根目錄下建立一個 auto-running.js 文件,用於監聽src文件的改變的腳本,監聽到vue-router 源碼變動就重新構建vue-router執行 node auto-running.js 命令。auto-running.js的代碼以下:
const { exec } = require('child_process')
const fs = require('fs')

let building = false

fs.watch('./src', {
  recursive: true
}, (event, filename) => {
  if (building) {
    return
  } else {
    building = true
    console.log('start: building ...')
    exec('npm run build', (err, stdout, stderr) => {
      if (err) {
        console.log(err)
      } else {
        console.log('end: building: ', stdout)
      }
      building = false
    })
  }
})
複製代碼

4.執行 npm run dev命令,將 vue-router 跑起來vue

查看vue-router源碼方法二:

通常項目中的node_modules的vue-router的src不全 不方便查看源碼;html5

因此須要本身下載一個vue-router的完整版,看到哪裏不清楚了,就去vue-router的node_modules的 dist>vue-router.esm.js 文件裏去打debugger。java

爲何要在vue-router.esm.js文件裏打點而不是vue-router.js;是由於webpack在進行打包的時候用的是esm.js文件。node

爲何要在esm.js文件中打debugger

在vue-router源碼的 dist/目錄,有不少不一樣的構建版本。webpack

版本 UMD Common JS ES Module(基於構建工具使用) ES Modules(直接用於瀏覽器)
完整版 vue-router.js vue-router.common.js vue-router.esm.js vue-router.esm.browser.js
完整版(生產環境) vue-router.min.js vue-router.esm.browser.min.js
  • 完整版:同時包含編譯器和運行時的版本
  • UMD:UMD版本能夠經過 <script> 標籤直接用在瀏覽器中。
  • CommonJS: CommonJS版本用來配合老的打包工具好比webpack1。
  • ES Module: 有兩個ES Modules構建文件:
    1. 爲打包工具提供的ESM,ESM被設計爲能夠被靜態分析,打包工具能夠利用這一點來進行「tree-shaking」。
    2. 爲瀏覽器提供的ESM,在現代瀏覽器中經過 <script type="module"> 直接導入

如今清楚爲何要在esm.js文件中打點,由於esm文件爲打包工具提供的esm,打包工具能夠進行「tree-shaking」。web

vue-router項目src的目錄樹

.
├── components
│   ├── link.js
│   └── view.js
├── create-matcher.js
├── create-route-map.js
├── history
│   ├── abstract.js
│   ├── base.js
│   ├── errors.js
│   ├── hash.js
│   └── html5.js
├── index.js
├── install.js
└── util
    ├── async.js
    ├── dom.js
    ├── location.js
    ├── misc.js
    ├── params.js
    ├── path.js
    ├── push-state.js
    ├── query.js
    ├── resolve-components.js
    ├── route.js
    ├── scroll.js
    ├── state-key.js
    └── warn.js
複製代碼

vue-router的使用

vue-router 是vue的插件,其使用方式跟普通的vue插件相似都須要按照、插件和註冊。 vue-router的基礎使用在 vue-router 項目中 examples/basic,注意代碼註釋。算法

// 0.在模塊化工程中使用,導入Vue和VueRouter
import Vue from 'vue'
import VueRouter from 'vue-router'


// 1. 插件的使用,必須經過Vue.use()明確地安裝路由
// 在全局注入了兩個組件 <router-view> 和 <router-link>,
// 而且在全局注入 $router 和 $route,
// 能夠在實例化的全部的vue組件中使用 $router路由實例、$route當前路由對象
Vue.use(VueRouter)

// 2. 定義路由組件
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const Unicode = { template: '<div>unicode</div>' }

// 3. 建立路由實例 實例接收了一個對象參數,
// 參數mode:路由模式,
// 參數routes路由配置 將組件映射到路由
const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar },
    { path: '/é', component: Unicode }
  ]
})

// 4. 建立和掛載根實例
// 經過router參數注入到vue裏 讓整個應用都有路由參數
// 在應用中經過組件<router-view>,進行路由切換
// template裏有寫特殊用法 咱們晚點討論
new Vue({
  router,
  data: () => ({ n: 0 }),
  template: ` <div id="app"> <h1>Basic</h1> <ul> <!-- 使用 router-link 建立a標籤來定義導航連接. to屬性爲執行連接--> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <!-- 經過tag屬性能夠指定渲染的標籤 這裏是li標籤 event自定義了事件--> <router-link tag="li" to="/bar" :event="['mousedown', 'touchstart']"> <a>/bar</a> </router-link> <li><router-link to="/é">/é</router-link></li> <li><router-link to="/é?t=%25ñ">/é?t=%ñ</router-link></li> <li><router-link to="/é#%25ñ">/é#%25ñ</router-link></li> <!-- router-link能夠做爲slot,插入內容,若是內容中有a標籤,會把to屬性的連接給內部的a標籤 --> <router-link to="/foo" v-slot="props"> <li :class="[props.isActive && 'active', props.isExactActive && 'exact-active']"> <a :href="props.href" @click="props.navigate">{{ props.route.path }} (with v-slot).</a> </li> </router-link> </ul> <button id="navigate-btn" @click="navigateAndIncrement">On Success</button> <pre id="counter">{{ n }}</pre> <pre id="query-t">{{ $route.query.t }}</pre> <pre id="hash">{{ $route.hash }}</pre> <!-- 路由匹配到的組件將渲染在這裏 --> <router-view class="view"></router-view> </div> `,

  methods: {
    navigateAndIncrement () {
      const increment = () => this.n++
      // 路由註冊後,咱們能夠在Vue實例內部經過 this.$router 訪問路由實例,
      // 經過 this.$route 訪問當前路由
      if (this.$route.path === '/') {
        // this.$router.push 會向history棧添加一個新的記錄
        // <router-link>內部也是調用來 router.push,實現原理相同
        this.$router.push('/foo', increment)
      } else {
        this.$router.push('/', increment)
      }
    }
  }
}).$mount('#app')
複製代碼

使用 this.$router 的緣由是並不想用戶在每一個獨立須要封裝路由的組件中都導入路由。<router-view> 是最頂層的出口,渲染最高級路由匹配的組件,要在嵌套的出口中渲染組件,須要在 VueRouter 的參數中使用 children 配置。

注入路由和路由實例化都幹了點啥

Vue提供了插件註冊機制是,每一個插件都須要實現一個靜態的 install方法,當執行 Vue.use 註冊插件的時候,就會執行 install 方法,該方法執行的時候第一個參數強制是 Vue對象。

爲何install的插件方法第一個參數是Vue

Vue插件的策略,編寫插件的時候就不須要inport Vue了,在註冊插件的時候,給插件強制插入一個參數就是 Vue 實例。

install 爲何是 static 方法

類的靜態方法用 static 關鍵字定義,不能在類的實例上調用靜態方法,只可以經過類自己調用。這裏的 install 只能vue-router類調用,他的實例不能調用(防止vue-router的實例在 外部調用)。

vue-router注入的時候時候,install了什麼
// 引入install方法
import { install } from './install'

export default class VueRouter {
    // 在VueRouter類中定義install靜態方法
    static install: () => void;
}

// 給VueRouter.install複製
VueRouter.install = install

// 以連接的形式引入vue-router插件 直接註冊vue-router
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
複製代碼

vue-router源碼中,入口文件是 src/index.js,其中定義了 VueRouter 類,在VueRouter類中定義靜態方法 install,它定義在 src/install.js中。

src/install.js文件中路由註冊的時候install了什麼
import View from './components/view'
import Link from './components/link'

// 導出Vue實例
export let _Vue

// install 方法 當Vue.use(vueRouter)時 至關於 Vue.use(vueRouter.install())
export function install (Vue) {
  // vue-router註冊處理 只註冊一次便可
  if (install.installed && _Vue === Vue) return
  install.installed = true

  // 保存Vue實例,方便其它插件文件使用
  _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-router的時候,給全部的vue組件混入兩個生命週期beforeCreate、destroyed * 在beforeCreated中初始化vue-router,並將_route響應式 */
  Vue.mixin({
    beforeCreate () {
      // 若是vue的實例的自定義屬性有router的話,把vue實例掛在到vue實例的_routerRoot上
      if (isDef(this.$options.router)) {
        // 給大佬遞貓 把本身遞大佬
        this._routerRoot = this

        // 把VueRouter實例掛載到_router上
        this._router = this.$options.router

        // 初始化vue-router,init爲核心方法,init定義在src/index.js中,晚些再看
        this._router.init(this)

        // 將當前的route對象 隱式掛到當前組件的data上,使其成爲響應式變量。
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 找爸爸,自身沒有_routerRoot,找其父組件的_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  /** * 給Vue添加實例對象$router和$route * $router爲router實例 * $route爲當前的route */
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  /** * 注入兩個全局組件 * <router-view> * <router-link> */
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  /** * Vue.config 是一個對象,包含了Vue的全局配置 * 將vue-router的hook進行Vue的合併策略 */
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
複製代碼

爲了保證 VueRouter 只執行一次,當執行 install 邏輯的時候添加一個標識 installed。用一個全局變量保存Vue,方便VueRouter插件各處對Vue的使用。這個思想就很好,之後本身寫Vue插件的時候就能夠存一個全局的 _Vue

VueRouter安裝的核心是經過 mixin,嚮應用的全部組件混入 beforeCreatedestroyed鉤子函數。在beforeCreate鉤子函數中,定義了私有屬性_routerRoot_router

  • _routerRoot: 將Vue實例賦值給_routerRoot,至關於把Vue跟實例掛載到每一個組件的_routerRoot的屬性上,經過 $parent._routerRoot 的方式,讓全部組件都能擁有_routerRoot始終指向根Vue實例。
  • _router:經過 this.$options.router方式,讓每一個vue組件都能拿到VueRouter實例

用Vue的defineReactive方法把 _route 變成響應式對象。this._router.init() 初始化了router,init方法在 src/index.js中,init方法很重要,後面介紹。registerInstance 也是後面介紹。

而後給Vue的原型上掛載了兩個對象屬性 $router$route,在應用的全部組件實例上均可以訪問 this.$routerthis.$routethis.$router 是路由實例,對外暴露了像this.$router.pushthis.$router.replace等不少api方法,this.$route包含了當前路由的全部信息。是頗有用的兩個方法。

後面經過 Vue.component 方法定義了全局的 <router-link><router-view> 兩個組件。<router-link>相似於a標籤,<router-view> 是路由出口,在 <router-view> 切換路由渲染不一樣Vue組件。

最後定義了路由守衛的合併策略,採用了Vue的合併策略。

小結

Vue插件須要提供 install 方法,用於插件的注入。VueRouter安裝時會給應用的全部組件注入 beforeCreatedestoryed 鉤子函數。在 beforeCreate 中定義一些私有變量,初始化了路由。全局註冊了兩個組件和兩個api。

那麼問題來了,初始化路由都幹了啥

VueRouter類定義不少屬性和方法,咱們先看看初始化路由方法 init。初始化路由的代碼是 this._router.init(this),init接收了Vue實例,下面的app就是Vue實例。註釋寫的很詳細了,這裏就不文字敘述了。

init (app: any /* Vue component instance */) {
    // vueRouter可能會實例化屢次 apps用於存放多個vueRouter實例
    this.apps.push(app)

    // 保證VueRouter只初始化一次,若是初始化了就終止後續邏輯
    if (this.app) {
      return
    }

    // 將vue實例掛載到vueRouter上,router掛載到Vue實例上,哈 給大佬遞貓
    this.app = app

    // history是vueRouter維護的全局變量,很重要
    const history = this.history

    // 針對不一樣路由模式作不一樣的處理 transitionTo是history的核心方法,後面再細看
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    // 路由全局監聽,維護當前的route
    // 由於_route在install執行時定義爲響應式屬性,
    // 當route變動時_route更新,後面的視圖更新渲染就是依賴於_route
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }
複製代碼

history

接下來看看 new VueRouter 時constructor作了什麼。

constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 建立 matcher 匹配函數,createMatcher函數返回一個對象 {match, addRoutes} 很重要
    this.matcher = createMatcher(options.routes || [], this)

    // 默認hash模式
    let mode = options.mode || 'hash'

    // h5的history有兼容性 對history作降級處理
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    // 不一樣的mode,實例化不一樣的History類, 後面的this.history就是History的實例
    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}`)
        }
    }
}
複製代碼

constructoroptions 是實例化路由是的傳參,一般是一個對象 {routes, mode: 'history'}, routes是必傳參數,mode默認是hash模式。vueRouter還定義了哪些東西呢。

...

match (
    raw: RawLocation,
    current?: Route,
    redirectedFrom?: Location
  ): Route {
    return this.matcher.match(raw, current, redirectedFrom)
}

// 獲取當前的路由
get currentRoute (): ?Route {
    return this.history && this.history.current
}
  
init(options) { ... }

beforeEach(fn) { ... }
beforeResolve(fn) { ... }
afterEach(fn) { ... }
onReady(cb) { ... }

push(location) { ... }
replace(location) { ... }
back() { ... }
go(n) { ... }
forward() { ... }

// 獲取匹配到的路由組件
getMatchedComponents (to?: RawLocation | Route): Array<any> {
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if (!route) {
      return []
    }
    return [].concat.apply([], route.matched.map(m => {
      return Object.keys(m.components).map(key => {
        return m.components[key]
      })
    }))
}

addRoutes (routes: Array<RouteConfig>) {
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
}
複製代碼

在實例化的時候,vueRouter仿照history定義了一些api:pushreplacebackgoforward,還定義了路由匹配器、添加router動態更新方法等。

小結

install的時候先執行init方法,而後實例化vueRouter的時候定義一些屬性和方法。init執行的時候經過 history.transitionTo 作路由過渡。matcher 路由匹配器是後面路由切換,路由和組件匹配的核心函數。因此...en

matcher瞭解一下吧

在VueRouter對象中有如下代碼:

// 路由匹配器,createMatcher函數返回一個對象 {match, addRoutes}
this.matcher = createMatcher(options.routes || [], this)

...

match (
    raw: RawLocation,
    current?: Route,
    redirectedFrom?: Location
): Route {
    return this.matcher.match(raw, current, redirectedFrom)
}

...

const route = this.match(location, current)
複製代碼

咱們能夠觀察到 route 對象經過 this.match() 獲取,match 又是經過 this.matcher.match(),而 this.matcher 是經過 createMatcher 函數處理。接下來咱們去看看createMatcher函數的實現。

createMatcher

createMatcher 相關的實現都在 src/create-matcher.js中。

/** * 建立createMatcher * @param {*} routes 路由配置 * @param {*} router 路由實例 * * 返回一個對象 { * match, // 當前路由的match * addRoutes // 更新路由配置 * } */
export function createMatcher ( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

  function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route {

  ...

  return {
    match,
    addRoutes
  }
}
複製代碼

createMatcher 接收2個參數,routes 是用戶定義的路由配置,routernew VueRouter 返回的實例。routes 是一個定義了路由配置的數組,經過 createRouteMap 函數處理爲 pathList, pathMap, nameMap,返回了一個對象 {match, addRoutes} 。也就是說 matcher 是一個對象,它對外暴露了 matchaddRoutes 方法。

一會咱們先了解下 pathList, pathMap, nameMap分別是什麼,稍後在來看createRouteMap的實現。

  • pathList:路由路徑數組,存儲全部的path
  • pathMap:路由路徑與路由記錄的映射表,表示一個path到RouteRecord的映射關係
  • nameMap:路由名稱與路由記錄的映射表,表示name到RouteRecord的映射關係
RouteRecord

那麼路由記錄是什麼樣子的?

const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
}
複製代碼

RouteRecord 是一個對象,包含了一條路由的全部信息: 路徑、路由正則、路徑對應的組件數組、組件實例、路由名稱等等。

router對象

createRouteMap

createRouteMap 函數的實如今 src/create-route-map中:

/** * * @param {*} routes 用戶路由配置 * @param {*} oldPathList 老pathList * @param {*} oldPathMap 老pathMap * @param {*} oldNameMap 老nameMap */
export function createRouteMap ( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // pathList被用於控制路由匹配優先級
  const pathList: Array<string> = oldPathList || []
  // 路徑路由映射表
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // 路由名稱路由映射表
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // 確保通配符路由老是在最後
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }

  ...

  return {
    pathList,
    pathMap,
    nameMap
  }
}
複製代碼

createRouteMap 函數主要是把用戶的路由匹配轉換成一張路由映射表,後面路由切換就是依據這幾個映射表。routes 爲每個 route 執行 addRouteRecord 方法生成一條記錄,記錄在上面展現過了,咱們來看看是如何生成一條記錄的。

addRouteRecord
function addRouteRecord ( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) {

  //...
  // 先建立一條路由記錄
  const record: RouteRecord = { ... }

  // 若是該路由記錄 嵌套路由的話 就循環遍歷解析嵌套路由
  if (route.children) {
    // ...
    // 經過遞歸的方式來深度遍歷,並把當前的record做爲parent傳入
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 若是有多個相同的路徑,只有第一個起做用,後面的會被忽略
  // 對解析好的路由進行記錄,爲pathList、pathMap添加一條記錄
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  // ...
}
複製代碼

addRouteRecord 函數,先建立一條路由記錄對象。若是當前的路由記錄有嵌套路由的話,就循環遍歷繼續建立路由記錄,並按照路徑和路由名稱進行路由記錄映射。這樣全部的路由記錄都被記錄了。整個RouteRecord就是一個樹型結構,其中 parent 表示父的 RouteRecord

if (name) {
  if (!nameMap[name]) {
    nameMap[name] = record
  }
  // ...
}
複製代碼

若是咱們在路由配置中設置了 name,會給 nameMap添加一條記錄。createRouteMap 方法執行後,咱們就能夠獲得路由的完整記錄,而且獲得path、name對應的路由映射。經過pathname 能在 pathMapnameMap快速查到對應的 RouteRecord

export function createMatcher ( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
  //...
  return {
    match,
    addRoutes
  }
}
複製代碼

還記得 createMatcher 的返回值中有個 match,接下里咱們看 match的實現。

match
/** * * @param {*} raw 是RawLocation類型 是個url字符串或者RawLocation對象 * @param {*} currentRoute 當前的route * @param {*} redirectedFrom 重定向 (不是重要,可忽略) */
function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route {

    // location 是一個對象相似於
    // {"_normalized":true,"path":"/","query":{},"hash":""}
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    // 若是有路由名稱 就進行nameMap映射 
    // 獲取到路由記錄 處理路由params 返回一個_createRoute處理的東西
    if (name) {
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

      if (typeof location.params !== 'object') {
        location.params = {}
      }

      if (currentRoute && typeof currentRoute.params === 'object') {
        for (const key in currentRoute.params) {
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key]
          }
        }
      }

      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    
    // 若是路由配置了 path,到pathList和PathMap裏匹配到路由記錄 
    // 若是符合matchRoute 就返回_createRoute處理的東西
    } else if (location.path) {
      location.params = {}
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // 經過_createRoute返回一個東西
    return _createRoute(null, location)
}
複製代碼

match 方法接收路徑、但前路由、重定向,主要是根據傳入的rawcurrentRoute處理,返回的是 _createRoute()。來看看 _createRoute返回了什麼,就知道 match返回了什麼了。

function _createRoute ( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
}
複製代碼

_createRoute 函數根據有是否有路由重定向、路由重命名作不一樣的處理。其中redirect 函數和 alias 函數最後仍是調用了 _createRoute,最後都是調用了 createRoute。而來自於 util/route

/**
 * 
 * @param {*} record 通常爲null
 * @param {*} location 路由對象
 * @param {*} redirectedFrom 重定向
 * @param {*} router vueRouter實例
 */
export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  // 凍結route 一旦建立不可改變
  return Object.freeze(route)
}
複製代碼

createRoute 能夠根據 recordlocation 建立出來最終返回 Route 對象,而且外部不能夠修改,只能訪問。Route 對象中有一個很是重要的屬性是 matched,它是經過 formatMatch(record) 計算的:

function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}
複製代碼

經過 record 循環向上找 parent,直到找到最外層,並把全部的 record 都push到一個數組中,最終飯後就是一個 record 數組,這個 matched 爲後面的渲染組件提供了重要的做用。

小結

matcher的主流程就是經過createMatcher 返回一個對象 {match, addRoutes}, addRoutes 是動態添加路由用的,平時使用頻率比較低,match 很重要,返回一個路由對象,這個路由對象上記錄當前路由的基本信息,以及路徑匹配的路由記錄,爲路徑切換、組件渲染提供了依據。那路徑是怎麼切換的,又是怎麼渲染組件的呢。喝杯誰,咱們繼續繼續往下看。

路徑切換

還記得 vue-router 初始化的時候,調用了 init 方法,在 init方法裏針對不一樣的路由模式最後都調用了 history.transitionTo,進行路由初始化匹配。包括 history.pushhistory.replace的底層都是調用了它。它就是路由切換的方法,很重要。它的實如今 src/history/base.js,咱們來看看。

transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
) {
    // 調用 match方法獲得匹配的 route對象
    const route = this.router.match(location, this.current)
    
    // 過渡處理
    this.confirmTransition(
        route,
        () => {
            // 更新當前的 route 對象
            this.updateRoute(route)
            onComplete && onComplete(route)
            
            // 更新url地址 hash模式更新hash值 history模式經過pushState/replaceState來更新
            this.ensureURL()
    
            // fire ready cbs once
            if (!this.ready) {
                this.ready = true
                this.readyCbs.forEach(cb => {
                cb(route)
                })
            }
        },
        err => {
            if (onAbort) {
                onAbort(err)
            }
            if (err && !this.ready) {
                this.ready = true
                this.readyErrorCbs.forEach(cb => {
                cb(err)
                })
            }
        }
    )
}
複製代碼

transitionTo 能夠接收三個參數 locationonCompleteonAbort,分別是目標路徑、路經切換成功的回調、路徑切換失敗的回調。transitionTo 函數主要作了兩件事:首先根據目標路徑 location 和當前的路由對象經過 this.router.match方法去匹配到目標 route 對象。route是這個樣子的:

const route = {
    fullPath: "/detail/394"
    hash: ""
    matched: [{…}]
    meta: {title: "工單詳情"}
    name: "detail"
    params: {id: "394"}
    path: "/detail/394"
    query: {}
}
複製代碼

一個包含了目標路由基本信息的對象。而後執行 confirmTransition方法進行真正的路由切換。由於有一些異步組件,因此回有一些異步操做。具體的實現:

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    const abort = err => {
      // ...
      onAbort && onAbort(err)
    }
    
    // 若是當前路由和以前路由相同 確認url 直接return
    if (
      isSameRoute(route, current) &&
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort(new NavigationDuplicated(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()
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false || isError(to)) {
            // next(false) -> abort navigation, ensure current URL
            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()
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // confirm transition and pass on the value
            // 若是有導航鉤子,就須要調用next(),不然回調不執行,導航將沒法繼續
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }

    // runQueue 執行隊列 以一種遞歸回調的方式來啓動異步函數隊列的執行
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      const isValid = () => this.current === route

      // 組件內的鉤子
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      // 在上次的隊列執行完成後再執行組件內的鉤子
      // 由於須要等異步組件以及是否OK的狀況下才能執行
      runQueue(queue, iterator, () => {
        // 確保期間仍是當前路由
        if (this.pending !== route) {
          return abort()
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => {
              cb()
            })
          })
        }
      })
    })
}
複製代碼

查看目標路由 route 和當前前路由 current 是否相同,若是相同就調用 this.ensureUrlabort

// ensureUrl todo

接下來執行了 resolveQueue函數,這個函數要好好看看:

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

resolveQueue函數接收兩個參數:當前路由的 matched 和目標路由的 matchedmatched 是個數組。經過遍歷對比兩遍的路由記錄數組,當有一個路由記錄不同的時候就記錄這個位置,並終止遍歷。對於 next 從0到i和current都是同樣的,從i口開始不一樣,next 從i以後爲 activated部分,current從i以後爲 deactivated部分,相同部分爲 updated,由 resolveQueue 處理以後就能獲得路由變動須要更改的部分。緊接着就能夠根據路由的變動執行一系列的鉤子函數。完整的導航解析流程有12步,後面會出一篇vue-router路由切換的內部實現文章。盡情期待 !

路由改變路由組件是如何渲染的

路由的變動以後,路由組件隨之的渲染都是在 <router-view> 組件,它的定義在 src/components/view.js中。

router-view 組件
export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth
    if (inactive) {
      return h(cache[name], data, children)
    }
    const matched = route.matched[depth]
    if (!matched) {
      cache[name] = null
      return h()
    }
    const component = cache[name] = matched.components[name]
    data.registerRouteInstance = (vm, val) => {     
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      propsToPass = data.props = extend({}, propsToPass)
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }
    return h(component, data, children)
  }
}
複製代碼

<router-view>是一個渲染函數,它的渲染是用了Vue的 render 函數,它接收兩個參數,第一個是Vue實例,第二個是一個context,經過對象解析的方式能夠拿到 props、children、parent、data,供建立 <router-view> 使用。

router-link 組件

支持用戶在具備路由功能的組件裏使用,經過使用 to 屬性指定目標地址,默認渲染成 <a>標籤,支持經過 tag 自定義標籤和插槽。

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(this.to, current, this.append)
    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    const activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass
    const activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route
    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)
    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }
    const data: any = {
      class: classes
    }
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      const a = findAnchor(this.$slots.default)
      if (a) {
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        data.on = on
      }
    }
    return h(this.tag, data, this.$slots.default)
  }
}
複製代碼

<router-link>的特色:

  • history 模式和 hash 模式的標籤一致,針對不支持 history的模式會自動降級爲 hash 模式。
  • 可進行路由守衛,不重新加載頁面

<router-link> 的實現也是基於 render 函數。內部實現也是經過 history.push()history.replace() 實現的。

路徑變化是路由中最重要的功能:路由始終會維護當前的線路,;欲嘔切換的時候會把當前線路切換到目標線路,切換過程當中會執行一些列的導航守衛鉤子函數,會更改url, 渲染對應的組件,切換完畢後會把目標線路更新替換爲當前線路,做爲下一次路徑切換的依據。

知識補充

hash模式和history模式的區別

vue-router 默認是hash模式,使用hash模式時,變動URL,頁面不會從新加載,這種模式從ie6就有了,是一種很穩定的路由模式。可是hash的URL上有個 # 號,看上去很醜,後來HTML5出來後,有了history模式。

history 模式經過 history.pushState來完成url的跳轉而無須從新加載頁面,解決了hash模式很臭的問題。可是老瀏覽器不兼容history模式,有些時候咱們不得不使用hash模式,來作向下兼容。

history 模式,若是訪問一個不存在的頁面時就會返回 404,爲了解決這個問題,須要後臺作配置支持:當URL匹配不到任何靜態資源的時候,返回一個index.html頁面。或者在路由配置裏添加一個統一配置的錯誤頁。

爲何會history會出現這個問題,hash模式不會呢?

hash 模式下,僅 hash 符號以前的內容會被包含在請求中,如 www.abc.com,所以對於後端來講,即便沒有作到對路由的全覆蓋,也不會返回 404 錯誤

history 模式下,前端的 URL 必須和實際向後端發起請求的 URL 一致,如 www.abc.com/book/id。若是後… /book/id 的路由處理,將返回 404 錯誤。

const router = new VueRouter({
    mode: 'history',
    routes: [
        {
            path: '*',
            component: NotFoundComponent
        }
    ]
})
複製代碼

Vue Router 的 queryparams 的使用和區別

vue-router中有兩個概念 queryparams,一開始的時候我對它們分不清,相信也有人分不清。這裏作個彙總,方便記憶理解。

  • query的使用
// 帶查詢參數,變成 /register?plan=private
router.push({ path: 'register', query: {plan: 'private'}})
複製代碼
  • params的配置和調用
  • 路由配置,使用params傳參數,使用name
{
    path: '/detail/:id',
    name: 'detail',
    component: Detail,
}
複製代碼
  • 調用 this.$router.push 進行params傳參,使用name,前提須要在路由配置裏設置過名稱。
this.$router.push({
    name: 'detail',
    params: {
        id: '2019'
    }
})
複製代碼
  • params接收參數
const { id } = this.$route.params
複製代碼

query一般與path使用。query帶查詢參數,params路徑參數。若是提供了path,params會被忽略。

// params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
複製代碼

導航守衛

導航 表示路由正在發生變化,vue-router 提供的導航守衛主要用來經過跳轉或者取消的方式守衛導航。導航守衛分爲三種:全局守衛、單個路由守衛和組件內的守衛。

導航守衛

全局守衛:

  • 全局前置守衛 beforeEach (to, from, next)
  • 全局解析守衛 beforeResolve (to, from, next)
  • 全局後置鉤子 afterEach (to, from)

單個路由守衛:

  • 路由前置守衛 beforeEnter (to, from, next)

組件內的守衛:

  • 渲染組件的對應路由被confirm前 beforeRouterEnter (to, from, next) next能夠是函數,由於該守衛不能獲取組件實例,新組件還沒被建立
  • 路由改變,該組件被複用時調用 (to, from, next)
  • 導航離開該組件對應路由時調用 beforeRouteLeave
完整的導航解析流程圖

導航解析流程

  1. 導航被觸發
  2. 在失活的組件裏調用離開守衛 beforeRouteLeave
  3. 調用全局的 beforeEach 守衛
  4. 在重用的組件裏調用 beforeRouteUpdate 守衛(2.2+)
  5. 在路由配置裏調用 beforeEnter
  6. 解析異步路由組件
  7. 在被激活的組件裏調用 beforeRouteEnter
  8. 調用全局的 beforeResolve守衛
  9. 導航被確認
  10. 調用全局的 afterEach鉤子
  11. 觸發DOM更新
  12. 用建立好的實例調用 beforeRouterEnter 守衛中傳給next的回調函數
相關文章
相關標籤/搜索