天天看點

從源碼了解 Vue-Router 實作原理

在使用 vue 開發單頁面(SPA)應用時,vue-router 是必不可少的技術;它的本質是監聽 URL 的變化然後比對路由規則顯示相應的頁面,并且不重新整理頁面;簡單的說就是,更新視圖但不重新請求頁面。

下面通過源碼來看一下 vue-router 的整個實作流程:

一、vue-router 源碼目錄結構

github位址:https://github.com/vuejs/vue-router

目前版本:

vue 2.6.11

vue-router 3.5.1

從源碼了解 Vue-Router 實作原理

components:這裡面是兩個元件 router-view 和 router-link

history:這個是路由模式(mode),有三種方式

util:這裡是路由的功能函數和類

create-matcher 和 create-router-map 是路由解析和生成配置表

index:VueRouter類,也是整個插件的入口

install:提供插件安裝方法

二、Vue.use() 注冊插件

vue 在使用路由時需要調用 Vue.use(plugin) 方法進行注冊,代碼在 vue 源碼裡面 src/core/global-api/use.js檔案裡主要作用兩個:

1、緩存判斷是否已經注冊過,避免重複注冊

2、使用插件的 install 方法或者直接運作插件來注冊

3、這裡使用了 flow 的文法,在編譯時對 js 變量進行類型檢查,縮短調式時間減少類型錯誤引起的 bug

// 初始化use
export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 檢測插件是否已經被安裝
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    // 調用插件的 install 方法或者直接運作插件,以實作插件的 install
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}
           

三、路由安裝 install

注冊路由的時候需要調用路由的 install 方法,代碼在 vue-router 源碼的 src/install.js 檔案裡,是 vue-router 的安裝程式,該方法做了下面四件事:

1、緩存判斷是否已經安裝過,避免重複安裝

2、使用 Vue.mixin 混入 beforeCreate 和 destroyed 鈎子函數,這樣在 Vue 生命周期階段就會被調用

3、通過 Vue.prototype 定義 router 和 route 屬性,友善所有元件使用

4、全局注冊 router-view 和 router-link 元件;router-link 用于觸發路由的變化,router-view 用于觸發對應路由視圖的變化

import View from './components/view'
import Link from './components/link'
export let _Vue
// Vue.use安裝插件時候需要暴露的install方法 
export function install (Vue) {
  // 判斷是否已安裝過,安裝過直接 return 出來,沒安裝執行安裝程式 
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 把Vue指派給全局變量 
  _Vue = Vue
  // 判斷是否已定義 
  const isDef = v => v !== undefined
  //通過registerRouteInstance方法注冊router執行個體 
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 混淆進Vue執行個體,在boforeCreate與destroyed鈎子上混淆 
  Vue.mixin({
    beforeCreate () {
      // 在option上面存在router則代表是根元件 
      if (isDef(this.$options.router)) {
        // 根路由設定為自己
        this._routerRoot = this
        // 儲存router
        this._router = this.$options.router
        // VueRouter對象的init方法 
        this._router.init(this)
        // Vue内部方法,為對象defineProperty上在變化時通知的屬性,實作響應式
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 非根元件則直接從父元件中擷取,用于 router-view 層級判斷
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 通過registerRouteInstance方法注冊router執行個體
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  //在Vue.prototype挂載屬性,可以通過 this.$router、this.$route 來通路 Vue.prototype 上的 _router、_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)

  // 該對象儲存了兩個option合并的規則 
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
           

四、VueRouter 執行個體化

安裝路由插件之後,會對 VueRouter 進行執行個體化然後将其傳入 Vue 執行個體的 options 中,在 vue-router 源碼的 src/index.js 檔案裡;下面是 VueRouter 的構造函數。

constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 路由比對對象
    this.matcher = createMatcher(options.routes || [], this)
	// 根據 mode 采取不同的路由方式
    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}`)
        }
    }
  }
           

構造函數有兩個重要的東西:

1、建立路由比對對象 matcher(核心)

這裡用到的 createMatcher 函數在 src/create-matcher.js 檔案裡,其作用是建立路由映射表,然後使用閉包的方法讓 addRoutes 和 match 函數能夠使用路由映射表的幾個對象,最後傳回一個 Matcher 對象。

//Matcher 的資料結構
export type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
};
//createMatcher 具體實作
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  //建立路由映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  function addRoutes (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 {...}

  return {
    match,
    addRoutes
  }
}
           

createMatcher()有兩個參數routes表示建立VueRouter新對象傳入的routes配置資訊,router表示VueRouter執行個體。

createRouteMap() 的作用就是對目前開發者傳入的 options.routes 進行路由映射化處理,并得到了三個路由容器 pathList、pathMap、nameMap,方法在 src/create-route-map.js 檔案裡

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>;
  pathMap: Dictionary<RouteRecord>;
  nameMap: Dictionary<RouteRecord>;
} {
  // 建立映射表
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  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 函數傳回三個屬性 —— pathList、pathMap、nameMap。然後通過 addRouteRecord 函數去向這三個屬性中增添資料。

下面是 addRouteRecord 方法, 利用遞歸方式解析嵌套路由。

//添加路由記錄
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  //擷取路由配置下的屬性
  const { path, name } = route
  if (process.env.NODE_ENV !== 'production') {
    assert(path != null, `"path" is required in a route configuration.`)
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(path || name)} cannot be a ` +
      `string id. Use an actual component instead.`
    )
  }
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  //格式化url 替換 /
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  //生成記錄對象
  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 }
  }
  if (route.children) {
    // 遞歸路由配置的 children 屬性,添加路由記錄
    if (process.env.NODE_ENV !== 'production') {
      if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
          `When navigating to this named route (:to="{name: '${route.name}'"), ` +
          `the default child route will not be rendered. Remove the name from ` +
          `this route and use the name of the default child route for named ` +
          `links instead.`
        )
      }
    }
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  // 如果路由有别名的話,給别名也添加路由記錄
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias)
      ? route.alias
      : [route.alias]

    aliases.forEach(alias => {
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    })
  }
  //更新映射表,使用鍵值對對解析好的路由進行記錄,這樣配置相同的path隻有第一個會起作用,後面的都會忽略
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  //命名路由添加記錄
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
        `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}
           

2、根據 mode 采取不同的路由方式

1、vue-router 一共有三種路由模式(mode):hash、history、abstract,其中 abstract 是在非浏覽器環境下使用的路由模式,如 weex

2、預設是 hash 模式,如果傳入的是 history 模式,但是目前環境不支援則會降級為 hash 模式

3、如果目前環境是非浏覽器環境,則強制使用 abstract 模式

4、模式比對成功則會進行對應的初始化操作

五、路由初始化

當根元件調用 beforeCreate 鈎子函數的時候會執行路由初始化代碼,代碼在 src/index 檔案下面,是路由執行個體提供的一個方法。

/* 初始化 */
  init (app: any /* Vue component instance */) {
    /* 未安裝就調用init會抛出異常 */
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )
    /* 将目前vm執行個體儲存在app中 */
    this.apps.push(app)
    // main app already initialized.
    /* 已存在說明已經被init過了,直接傳回 */
    if (this.app) {
      return
    }
    /* this.app儲存目前vm執行個體 */
    this.app = app
    const history = this.history
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }
           

路由初始化會進行路由跳轉,改變 URL 後渲染對應的元件。路由跳轉的核心的 history 的 transitionTo 方法。

六、路由切換

在 src/history/base 檔案下 History 執行個體提供的一個方法。

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // 根據跳轉的 location 得到新的route
    const route = this.router.match(location, this.current)
    //确認切換路由
    this.confirmTransition(route, () => {
      //更新 route
      this.updateRoute(route)
      //添加 hashChange 監聽
      onComplete && onComplete(route)
      //更新 URL
      this.ensureURL()
	  //隻執行一次 ready 回掉
      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 方法中先調用match方法得到新的路由對象,然後調用 confirmTransition 方法是處理導航守衛的邏輯。

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    //中斷跳轉路由函數
    const abort = err => {
      if (isError(err)) {
        if (this.errorCbs.length) {
          this.errorCbs.forEach(cb => { cb(err) })
        } else {
          warn(false, 'uncaught error during route navigation:')
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    //判斷路由是否相同,相同不跳轉
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort()
    }
	//對比路由,解析出可複用的元件、失活的元件、需要渲染的元件
    const {
      updated,
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)
	//導航守衛數組
    const queue: Array<?NavigationGuard> = [].concat(
      //失活的元件鈎子
      extractLeaveGuards(deactivated),
      //全局的 beforeEach 鈎子
      this.router.beforeHooks,
      //在目前路由改變,但是該元件被複用時調用
      extractUpdateHooks(updated),
      //需要渲染元件 enter 守衛鈎子
      activated.map(m => m.beforeEnter),
      //解析異步路由元件
      resolveAsyncComponents(activated)
    )
	//儲存路由
    this.pending = route
    //疊代器,用于執行 queue 裡面的導航守衛鈎子
    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: '/' }) -> 重定向
            abort()
            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
      //上一次隊列執行完成之後再執行元件内的鈎子
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      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() })
          })
        }
      })
    })
  }
           

通過 next 進行導航守衛的回調疊代,是以如果在代碼中使用了路由鈎子函數,那麼就必須在最後調用 next(),否則回調不執行,導航将無法繼續

七、路由同步(以hash模式為例)

在路由切換的時候,vue-router 會調用 push、go 等方法實作視圖與位址 url 的同步。

1、主動觸發

點選事件跳轉頁面,觸發 push 或 replace 方法,然後調用 transitionTo 方法裡面的 updateRoute 方法來更新 _route,進而觸發 router-view 的變化。

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

push 方法先檢測目前浏覽器是否支援 html5的 History API,如果支援則調用此 API進行 href的修改,否則直接對window.location.hash進行指派

2、改變位址欄 url,然後視圖同步

在路由初始化的時候會添加事件 setupHashListener 來監聽 hashchange 或 popstate;當路由變化時,會觸發對應的 push 或 replace 方法,然後調用 transitionTo 方法裡面的 updateRoute 方法來更新 _route,進而觸發 router-view 的變化。

// src/history/hash.js
  setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    if (supportsScroll) {
      setupScroll()
    }
    window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    })
  }
           

八、路由元件

在 src/components/ 檔案夾下

1、router-view:元件挂載

該元件是無狀态 (沒有 data ) 和無執行個體 (沒有 this 上下文)的(功能元件)函數式元件。其通過路由比對擷取到對應的元件執行個體,通過 h函數動态生成元件,如果目前路由沒有比對到任何元件,則渲染一個注釋節點。

export default {
  name: 'RouterView',
  /* 
    https://cn.vuejs.org/v2/api/#functional
    使元件無狀态 (沒有 data ) 和無執行個體 (沒有 this 上下文)。他們用一個簡單的 render 函數傳回虛拟節點使他們更容易渲染。
  */
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // 标記位,标記是route-view元件
    data.routerView = true
    // 直接使用父元件的createElement函數 
    const h = parent.$createElement
    // props的name,預設'default' 
    const name = props.name
    // option中的VueRouter對象 
    const route = parent.$route
    // 在parent上建立一個緩存對象 
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    // 記錄元件深度 
    let depth = 0
    // 标記是否是待用(非alive狀态)
    let inactive = false
    // _routerRoot中中存放了根元件的執行個體,這邊循環向上級通路,直到通路到根元件,得到depth深度 
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      // 如果_inactive為true,代表是在keep-alive中且是待用(非alive狀态)
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    // 存放route-view元件的深度 
    data.routerViewDepth = depth
    // 如果inactive為true說明在keep-alive元件中,直接從緩存中取 
    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]
    // 注冊執行個體的registration鈎子,這個函數将在執行個體被注入的加入到元件的生命鈎子(beforeCreate與destroyed)中被調用 
    data.registerRouteInstance = (vm, val) => {  
      // 第二個值不存在的時候為登出 
      // 擷取元件執行個體 
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        //這裡有兩種情況,一種是val存在,則用val替換目前元件執行個體,另一種則是val不存在,則直接将val賦給instances 
        matched.instances[name] = val
      }
    }
    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    // resolve props
    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass)
      // pass non-declared props as attrs
      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)
  }
}
           

主要作用就是拿到比對的元件進行渲染。

2、router-link:路由跳轉

router-link在執行 render函數的時候,會根據目前的路由狀态,給渲染出來的active元素添加 class,是以你可以借助此給active路由元素設定樣式等

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
    // Support global empty active class
    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
    }
	// 渲染出 <a> 标簽,然後添加 href 屬性和點選事件
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // find the first <a> child and apply listener and 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 {
        // doesn't have <a> child, apply listener to self
        data.on = on
      }
    }
    return h(this.tag, data, this.$slots.default)
  }
}
           

最後、路由實作的過程簡化版

頁面渲染

1、Vue.use(Router) 注冊

2、注冊時調用 install 方法混入生命周期,定義 router 和 route 屬性,注冊 router-view 和 router-link 元件

3、生成 router 執行個體,根據配置數組(傳入的routes)生成路由配置記錄表,根據不同模式生成監控路由變化的History對象

4、生成 vue 執行個體,将 router 執行個體挂載到 vue 執行個體上面,挂載的時候 router 會執行最開始混入的生命周期函數

5、初始化結束,顯示預設頁面

路由點選更新

1、 router-link 綁定 click 方法,觸發 history.push 或 history.replace ,進而觸發 history.transitionTo 方法

2、ransitionTo 用于處理路由轉換,其中包含了 updateRoute 用于更新 _route

3、在 beforeCreate 中有劫持 _route 的方法,當 _route 變化後,觸發 router-view 的變化

位址變化路由更新

1、HashHistory 和 HTML5History 會分别監控 hashchange 和 popstate 來對路由變化作對用的處理

2、HashHistory 和 HTML5History 捕獲到變化後會對應執行 push 或 replace 方法,進而調用 transitionTo

3、然後更新 _route 觸發 router-view 的變化