vue-router原始碼分析-整體流程

滴滴出行·DDFE發表於2016-12-01

作者:滴滴公共前端團隊 - dolymood

在現在單頁應用這麼火爆的年代,路由已經成為了我們開發應用必不可少的利器;而縱觀各大框架,都會有對應的強大路由支援。Vue.js 因其效能、通用、易用、體積、學習成本低等特點已經成為了廣大前端們的新寵,而其對應的路由 vue-router 也是設計的簡單好用,功能強大。本文就從原始碼來分析下 Vue.js 官方路由 vue-router 的整體流程。

本文主要以 vue-router 的 2.0.3 版本來進行分析。

首先來張整體的圖:

vue-router原始碼分析-整體流程
vue-router.js流程圖

先對整體有個大概的印象,下邊就以官方倉庫下 examples/basic 基礎例子來一點點具體分析整個流程。

目錄結構

先來看看整體的目錄結構:

vue-router原始碼分析-整體流程
vue-router 目錄結構圖

和流程相關的主要需要關注點的就是 componentshistory 目錄以及 create-matcher.jscreate-route-map.jsindex.jsinstall.js。下面就從 basic 應用入口開始來分析 vue-router 的整個流程。

入口

首先看應用入口的程式碼部分:

import Vue from 'vue'
import VueRouter from 'vue-router'

// 1. 外掛
// 安裝 <router-view> and <router-link> 元件
// 且給當前應用下所有的元件都注入 $router and $route 物件
Vue.use(VueRouter)

// 2. 定義各個路由下使用的元件,簡稱路由元件
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 3. 建立 VueRouter 例項 router
const router = new VueRouter({
  mode: 'history',
  base: __dirname,
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
})

// 4. 建立 啟動應用
// 一定要確認注入了 router 
// 在 <router-view> 中將會渲染路由元件
new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <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>
        <router-link tag="li" to="/bar">/bar</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')複製程式碼

作為外掛

上邊程式碼中關鍵的第 1 步,利用 Vue.js 提供的外掛機制 .use(plugin) 來安裝 VueRouter,而這個外掛機制則會呼叫該 plugin 物件的 install 方法(當然如果該 plugin 沒有該方法的話會把 plugin 自身作為函式來呼叫);下邊來看下 vue-router 這個外掛具體的實現部分。

VueRouter 物件是在 src/index.js 中暴露出來的,這個物件有一個靜態的 install 方法:

/* @flow */
// 匯入 install 模組
import { install } from './install'
// ...
import { inBrowser, supportsHistory } from './util/dom'
// ...

export default class VueRouter {
// ...
}

// 賦值 install
VueRouter.install = install

// 自動使用外掛
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}複製程式碼

可以看到這是一個 Vue.js 外掛的經典寫法,給外掛物件增加 install 方法用來安裝外掛具體邏輯,同時在最後判斷下如果是在瀏覽器環境且存在 window.Vue 的話就會自動使用外掛。

install 在這裡是一個單獨的模組,繼續來看同級下的 src/install.js 的主要邏輯:

// router-view router-link 元件
import View from './components/view'
import Link from './components/link'

// export 一個 Vue 引用
export let _Vue

// 安裝函式
export function install (Vue) {
  if (install.installed) return
  install.installed = true

  // 賦值私有 Vue 引用
  _Vue = Vue

  // 注入 $router $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this.$root._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this.$root._route }
  })
  // beforeCreate mixin
  Vue.mixin({
    beforeCreate () {
      // 判斷是否有 router
      if (this.$options.router) {
          // 賦值 _router
        this._router = this.$options.router
        // 初始化 init
        this._router.init(this)
        // 定義響應式的 _route 物件
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
    }
  })

  // 註冊元件
  Vue.component('router-view', View)
  Vue.component('router-link', Link)
// ...
}複製程式碼

這裡就會有一些疑問了?

  • 為啥要 export 一個 Vue 引用?

外掛在打包的時候是肯定不希望把 vue 作為一個依賴包打進去的,但是呢又希望使用 Vue 物件本身的一些方法,此時就可以採用上邊類似的做法,在 install 的時候把這個變數賦值 Vue ,這樣就可以在其他地方使用 Vue 的一些方法而不必引入 vue 依賴包(前提是保證 install 後才會使用)。

  • 通過給 Vue.prototype 定義 $router$route 屬性就可以把他們注入到所有元件中嗎?

在 Vue.js 中所有的元件都是被擴充套件的 Vue 例項,也就意味著所有的元件都可以訪問到這個例項原型上定義的屬性。

beforeCreate mixin 這個在後邊建立 Vue 例項的時候再細說。

例項化 VueRouter

在入口檔案中,首先要例項化一個 VueRouter ,然後將其傳入 Vue 例項的 options 中。現在繼續來看在 src/index.js 中暴露出來的 VueRouter 類:

// ...
import { createMatcher } from './create-matcher'
// ...
export default class VueRouter {
// ...
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.options = options
    this.beforeHooks = []
    this.afterHooks = []
    // 建立 match 匹配函式
    this.match = createMatcher(options.routes || [])
    // 根據 mode 例項化具體的 History
    let mode = options.mode || 'hash'
    this.fallback = mode === 'history' && !supportsHistory
    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)
        break
      default:
        assert(false, `invalid mode: ${mode}`)
    }
  }
// ...
}複製程式碼

裡邊包含了重要的一步:建立 match 匹配函式。

match 匹配函式

匹配函式是由 src/create-matcher.js 中的 createMatcher 建立的:

/* @flow */

import Regexp from 'path-to-regexp'
// ...
import { createRouteMap } from './create-route-map'
// ...

export function createMatcher (routes: Array<RouteConfig>): Matcher {
  // 建立路由 map
  const { pathMap, nameMap } = createRouteMap(routes)
  // 匹配函式
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
// ...
  }

  function redirect (
    record: RouteRecord,
    location: Location
  ): Route {
// ...
  }

  function alias (
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {
// ...
  }

  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)
  }
  // 返回
  return match
}
// ...複製程式碼

具體邏輯後續再具體分析,現在只需要理解為根據傳入的 routes 配置生成對應的路由 map,然後直接返回了 match 匹配函式。

繼續來看 src/create-route-map.js 中的 createRouteMap 函式:

/* @flow */

import { assert, warn } from './util/warn'
import { cleanPath } from './util/path'

// 建立路由 map
export function createRouteMap (routes: Array<RouteConfig>): {
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // path 路由 map
  const pathMap: Dictionary<RouteRecord> = Object.create(null)
  // name 路由 map
  const nameMap: Dictionary<RouteRecord> = Object.create(null)
  // 遍歷路由配置物件 增加 路由記錄
  routes.forEach(route => {
    addRouteRecord(pathMap, nameMap, route)
  })

  return {
    pathMap,
    nameMap
  }
}

// 增加 路由記錄 函式
function addRouteRecord (
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  // 獲取 path 、name
  const { path, name } = route
  assert(path != null, `"path" is required in a route configuration.`)
  // 路由記錄 物件
  const record: RouteRecord = {
    path: normalizePath(path, parent),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {}
  }
  // 巢狀子路由 則遞迴增加 記錄
  if (route.children) {
// ...
    route.children.forEach(child => {
      addRouteRecord(pathMap, nameMap, child, record)
    })
  }
  // 處理別名 alias 邏輯 增加對應的 記錄
  if (route.alias !== undefined) {
    if (Array.isArray(route.alias)) {
      route.alias.forEach(alias => {
        addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path)
      })
    } else {
      addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path)
    }
  }
  // 更新 path map
  pathMap[record.path] = record
  // 更新 name map
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else {
      warn(false, `Duplicate named routes definition: { name: "${name}", path: "${record.path}" }`)
    }
  }
}

function normalizePath (path: string, parent?: RouteRecord): string {
  path = path.replace(/\/$/, '')
  if (path[0] === '/') return path
  if (parent == null) return path
  return cleanPath(`${parent.path}/${path}`)
}複製程式碼

可以看出主要做的事情就是根據使用者路由配置物件生成普通的根據 path 來對應的路由記錄以及根據 name 來對應的路由記錄的 map,方便後續匹配對應。

例項化 History

這也是很重要的一步,所有的 History 類都是在 src/history/ 目錄下,現在呢不需要關心具體的每種 History 的具體實現上差異,只需要知道他們都是繼承自 src/history/base.js 中的 History 類的:

/* @flow */

// ...
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
// 這裡從之前分析過的 install.js 中 export _Vue
import { _Vue } from '../install'

export class History {
// ...
  constructor (router: VueRouter, base: ?string) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
  }
// ...
}

// 得到 base 值
function normalizeBase (base: ?string): string {
  if (!base) {
    if (inBrowser) {
      // respect <base> tag
      const baseEl = document.querySelector('base')
      base = baseEl ? baseEl.getAttribute('href') : '/'
    } else {
      base = '/'
    }
  }
  // make sure there's the starting slash
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  // remove trailing slash
  return base.replace(/\/$/, '')
}
// ...複製程式碼

例項化完了 VueRouter,下邊就該看看 Vue 例項了。

例項化 Vue

例項化很簡單:

new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <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>
        <router-link tag="li" to="/bar">/bar</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')複製程式碼

options 中傳入了 router,以及模板;還記得上邊沒具體分析的 beforeCreate mixin 嗎,此時建立一個 Vue 例項,對應的 beforeCreate 鉤子就會被呼叫:

// ...
  Vue.mixin({
    beforeCreate () {
      // 判斷是否有 router
      if (this.$options.router) {
          // 賦值 _router
        this._router = this.$options.router
        // 初始化 init
        this._router.init(this)
        // 定義響應式的 _route 物件
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
    }
  })複製程式碼

具體來說,首先判斷例項化時 options 是否包含 router,如果包含也就意味著是一個帶有路由配置的例項被建立了,此時才有必要繼續初始化路由相關邏輯。然後給當前例項賦值 _router,這樣在訪問原型上的 $router 的時候就可以得到 router 了。

下邊來看裡邊兩個關鍵:router.init 和 定義響應式的 _route 物件。

router.init

然後來看 routerinit 方法就幹了哪些事情,依舊是在 src/index.js 中:

/* @flow */

import { install } from './install'
import { createMatcher } from './create-matcher'
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
import { inBrowser, supportsHistory } from './util/dom'
import { assert } from './util/warn'

export default class VueRouter {
// ...
  init (app: any /* Vue component instance */) {
// ...
    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(getLocation(history.base))
    } else if (history instanceof HashHistory) {
      history.transitionTo(getHash(), () => {
        window.addEventListener('hashchange', () => {
          history.onHashChange()
        })
      })
    }

    history.listen(route => {
      this.app._route = route
    })
  }
// ...
}
// ...複製程式碼

可以看到初始化主要就是給 app 賦值,針對於 HTML5HistoryHashHistory 特殊處理,因為在這兩種模式下才有可能存在進入時候的不是預設頁,需要根據當前瀏覽器位址列裡的 path 或者 hash 來啟用對應的路由,此時就是通過呼叫 transitionTo 來達到目的;而且此時還有個注意點是針對於 HashHistory 有特殊處理,為什麼不直接在初始化 HashHistory 的時候監聽 hashchange 事件呢?這個是為了修復github.com/vuejs/vue-r…這個 bug 而這樣做的,簡要來說就是說如果在 beforeEnter 這樣的鉤子函式中是非同步的話,beforeEnter 鉤子就會被觸發兩次,原因是因為在初始化的時候如果此時的 hash 值不是以 / 開頭的話就會補上 #/,這個過程會觸發 hashchange 事件,所以會再走一次生命週期鉤子,也就意味著會再次呼叫 beforeEnter 鉤子函式。

來看看這個具體的 transitionTo 方法的大概邏輯,在 src/history/base.js 中:

/* @flow */

import type VueRouter from '../index'
import { warn } from '../util/warn'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
import { _Vue } from '../install'

export class History {
// ...
  transitionTo (location: RawLocation, cb?: Function) {
      // 呼叫 match 得到匹配的 route 物件
    const route = this.router.match(location, this.current)
    // 確認過渡
    this.confirmTransition(route, () => {
      // 更新當前 route 物件
      this.updateRoute(route)
      cb && cb(route)
      // 子類實現的更新url地址
      // 對於 hash 模式的話 就是更新 hash 的值
      // 對於 history 模式的話 就是利用 pushstate / replacestate 來更新
      // 瀏覽器地址
      this.ensureURL()
    })
  }
  // 確認過渡
  confirmTransition (route: Route, cb: Function) {
    const current = this.current
    // 如果是相同 直接返回
    if (isSameRoute(route, current)) {
      this.ensureURL()
      return
    }
    // 交叉比對當前路由的路由記錄和現在的這個路由的路由記錄
    // 以便能準確得到父子路由更新的情況下可以確切的知道
    // 哪些元件需要更新 哪些不需要更新
    const {
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)

    // 整個切換週期的佇列
    const queue: Array<?NavigationGuard> = [].concat(
      // leave 的鉤子
      extractLeaveGuards(deactivated),
      // 全域性 router before hooks
      this.router.beforeHooks,
      // 將要更新的路由的 beforeEnter 鉤子
      activated.map(m => m.beforeEnter),
      // 非同步元件
      resolveAsyncComponents(activated)
    )

    this.pending = route
    每一個佇列執行的 iterator 函式
    const iterator = (hook: NavigationGuard, next) => {
      // 確保期間還是當前路由
      if (this.pending !== route) return
      hook(route, current, (to: any) => {
        if (to === false) {
          // next(false) -> abort navigation, ensure current URL
          this.ensureURL(true)
        } else if (typeof to === 'string' || typeof to === 'object') {
          // next('/') or next({ path: '/' }) -> redirect
          this.push(to)
        } else {
          // confirm transition and pass on the value
          next(to)
        }
      })
    }
    // 執行佇列
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      // 元件內的鉤子
      const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
        return this.current === route
      })
      // 在上次的佇列執行完成後再執行元件內的鉤子
      // 因為需要等非同步元件以及是OK的情況下才能執行
      runQueue(enterGuards, iterator, () => {
          // 確保期間還是當前路由
        if (this.pending === route) {
          this.pending = null
          cb(route)
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => cb())
          })
        }
      })
    })
  }
  // 更新當前 route 物件
  updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    // 注意 cb 的值 
    // 每次更新都會呼叫 下邊需要用到!
    this.cb && this.cb(route)
    // 執行 after hooks 回撥
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
}
// ...複製程式碼

可以看到整個過程就是執行約定的各種鉤子以及處理非同步元件問題,這裡有一些具體函式具體細節被忽略掉了(後續會具體分析)但是不影響具體理解這個流程。但是需要注意一個概念:路由記錄,每一個路由 route 物件都對應有一個 matched 屬性,它對應的就是路由記錄,他的具體含義在呼叫 match() 中有處理;通過之前的分析可以知道這個 match 是在 src/create-matcher.js 中的:

// ...
import { createRoute } from './util/route'
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher (routes: Array<RouteConfig>): Matcher {
  const { pathMap, nameMap } = createRouteMap(routes)
  // 關鍵的 match
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute)
    const { name } = location

    // 命名路由處理
    if (name) {
      // nameMap[name] = 路由記錄
      const record = nameMap[name]
      const paramNames = getParams(record.path)
// ...
      if (record) {
        location.path = fillParams(record.path, location.params, `named route "${name}"`)
        // 建立 route
        return _createRoute(record, location, redirectedFrom)
      }
    } else if (location.path) {
      // 普通路由處理
      location.params = {}
      for (const path in pathMap) {
        if (matchRoute(path, location.params, location.path)) {
          // 匹配成功 建立route
          // pathMap[path] = 路由記錄
          return _createRoute(pathMap[path], location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }
// ...
  // 建立路由
  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)
  }

  return match
}
// ...複製程式碼

路由記錄在分析 match 匹配函式那裡以及分析過了,這裡還需要了解下建立路由物件的 createRoute,存在於 src/util/route.js 中:

// ...
export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: Location
): Route {
  // 可以看到就是一個被凍結的普通物件
  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query: location.query || {},
    params: location.params || {},
    fullPath: getFullPath(location),
    // 根據記錄層級的得到所有匹配的 路由記錄
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom)
  }
  return Object.freeze(route)
}
// ...
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}
// ...複製程式碼

回到之前看的 init,最後呼叫了 history.listen 方法:

history.listen(route => {
  this.app._route = route
})複製程式碼

listen 方法很簡單就是設定下當前歷史物件的 cb 的值, 在之前分析 transitionTo 的時候已經知道在 history 更新完畢的時候呼叫下這個 cb。然後看這裡設定的這個函式的作用就是更新下當前應用例項的 _route 的值,更新這個有什麼用呢?請看下段落的分析。

defineReactive 定義 _route

繼續回到 beforeCreate 鉤子函式中,在最後通過 Vue 的工具方法給當前應用例項定義了一個響應式的 _route 屬性,值就是獲取的 this._router.history.current,也就是當前 history 例項的當前活動路由物件。給應用例項定義了這麼一個響應式的屬性值也就意味著如果該屬性值發生了變化,就會觸發更新機制,繼而呼叫應用例項的 render 重新渲染。還記得上一段結尾留下的疑問,也就是 history 每次更新成功後都會去更新應用例項的 _route 的值,也就意味著一旦 history 發生改變就會觸發更新機制呼叫應用例項的 render 方法進行重新渲染。

回到例項化應用例項的地方:

new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <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>
        <router-link tag="li" to="/bar">/bar</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')複製程式碼

可以看到這個例項的 template 中包含了兩個自定義元件:router-linkrouter-view

router-view 元件

router-view 元件比較簡單,所以這裡就先來分析它,他是在原始碼的 src/components/view.js 中定義的:

export default {
  name: 'router-view',
  functional: true, // 功能元件 純粹渲染
  props: {
    name: {
      type: String,
      default: 'default' // 預設default 預設命名檢視的name
    }
  },
  render (h, { props, children, parent, data }) {
    // 解決巢狀深度問題
    data.routerView = true
    // route 物件
    const route = parent.$route
    // 快取
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    let depth = 0
    let inactive = false
    // 當前元件的深度
    while (parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      處理 keepalive 邏輯
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }

    data.routerViewDepth = depth
    // 得到相匹配的當前元件層級的 路由記錄
    const matched = route.matched[depth]
    if (!matched) {
      return h()
    }
    // 得到要渲染元件
    const name = props.name
    const component = inactive
      ? cache[name]
      : (cache[name] = matched.components[name])

    if (!inactive) {
      // 非 keepalive 模式下 每次都需要設定鉤子
      // 進而更新(賦值&銷燬)匹配了的例項元素
      const hooks = data.hook || (data.hook = {})
      hooks.init = vnode => {
        matched.instances[name] = vnode.child
      }
      hooks.prepatch = (oldVnode, vnode) => {
        matched.instances[name] = vnode.child
      }
      hooks.destroy = vnode => {
        if (matched.instances[name] === vnode.child) {
          matched.instances[name] = undefined
        }
      }
    }
    // 呼叫 createElement 函式 渲染匹配的元件
    return h(component, data, children)
  }
}複製程式碼

可以看到邏輯還是比較簡單的,拿到匹配的元件進行渲染就可以了。

再來看看導航連結元件,他在原始碼的 src/components/link.js 中定義的:

// ...
import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
// ...
export default {
  name: 'router-link',
  props: {
    // 傳入的元件屬性們
    to: { // 目標路由的連結
      type: toTypes,
      required: true
    },
    // 建立的html標籤
    tag: {
      type: String,
      default: 'a'
    },
    // 完整模式,如果為 true 那麼也就意味著
    // 絕對相等的路由才會增加 activeClass
    // 否則是包含關係
    exact: Boolean,
    // 在當前(相對)路徑附加路徑
    append: Boolean,
    // 如果為 true 則呼叫 router.replace() 做替換歷史操作
    replace: Boolean,
    // 連結啟用時使用的 CSS 類名
    activeClass: String
  },
  render (h: Function) {
    // 得到 router 例項以及當前啟用的 route 物件
    const router = this.$router
    const current = this.$route
    const to = normalizeLocation(this.to, current, this.append)
    // 根據當前目標連結和當前啟用的 route匹配結果
    const resolved = router.match(to, current)
    const fullPath = resolved.redirectedFrom || resolved.fullPath
    const base = router.history.base
    // 建立的 href
    const href = createHref(base, fullPath, router.mode)
    const classes = {}
    // 啟用class 優先當前元件上獲取 要麼就是 router 配置的 linkActiveClass
    // 預設 router-link-active
    const activeClass = this.activeClass || router.options.linkActiveClass || 'router-link-active'
    // 相比較目標
    // 因為有命名路由 所有不一定有path
    const compareTarget = to.path ? createRoute(null, to) : resolved
    // 如果嚴格模式的話 就判斷是否是相同路由(path query params hash)
    // 否則就走包含邏輯(path包含,query包含 hash為空或者相同)
    classes[activeClass] = this.exact
      ? isSameRoute(current, compareTarget)
      : isIncludedRoute(current, compareTarget)

    // 事件繫結
    const on = {
      click: (e) => {
        // 忽略帶有功能鍵的點選
        if (e.metaKey || e.ctrlKey || e.shiftKey) return
        // 已阻止的返回
        if (e.defaultPrevented) return
        // 右擊
        if (e.button !== 0) return
        // `target="_blank"` 忽略
        const target = e.target.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
        // 阻止預設行為 防止跳轉
        e.preventDefault()
        if (this.replace) {
          // replace 邏輯
          router.replace(to)
        } else {
          // push 邏輯
          router.push(to)
        }
      }
    }
    // 建立元素需要附加的資料們
    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // 找到第一個 <a> 給予這個元素事件繫結和href屬性
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        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 {
        // 沒有 <a> 的話就給當前元素自身繫結時間
        data.on = on
      }
    }
    // 建立元素
    return h(this.tag, data, this.$slots.default)
  }
}

function findAnchor (children) {
  if (children) {
    let child
    for (let i = 0; i < children.length; i++) {
      child = children[i]
      if (child.tag === 'a') {
        return child
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child
      }
    }
  }
}

function createHref (base, fullPath, mode) {
  var path = mode === 'hash' ? '/#' + fullPath : fullPath
  return base ? cleanPath(base + path) : path
}複製程式碼

可以看出 router-link 元件就是在其點選的時候根據設定的 to 的值去呼叫 routerpush 或者 replace 來更新路由的,同時呢,會檢查自身是否和當前路由匹配(嚴格匹配和包含匹配)來決定自身的 activeClass 是否新增。

小結

整個流程的程式碼到這裡已經分析的差不多了,再來回顧下:

vue-router原始碼分析-整體流程
vue-router.js流程圖

相信整體看完後和最開始的時候看到這張圖的感覺是不一樣的,且對於 vue-router 的整體的流程瞭解的比較清楚了。當然由於篇幅有限,這裡還有很多細節的地方沒有細細分析,後續會根據模組來進行具體的分析。


歡迎關注DDFE
GITHUB:github.com/DDFE
微信公眾號:微信搜尋公眾號“DDFE”或掃描下面的二維碼

vue-router原始碼分析-整體流程

相關文章