[精讀原始碼系列]前端路由Vue-Router

前端古力士發表於2019-09-07

前言

相信用vue開發過專案的小夥伴都不會陌生,vue-router模組幫助我們處理單頁面應用的理由跳轉的,我們只需要將不同path對應的元件資訊傳給vue-router,就可以在頁面區域性重新整理的情況下實現路由跳轉了,你有沒有覺得對這一處理過程感到很好奇,想要揭開這一操作的神祕面紗?來吧,讓我們一起開啟探索之旅~

vue-router的使用

import VueRouter from 'vue-router'
Vue.use(VueRouter)

const router = new VueRouter({
    mode: 'history',
    routes: [...]
})

new Vue({
    router
    ...
})
複製程式碼

我們看到在使用路由之前需要呼叫Vue.use(VueRouter),這一操作就好像給一個拼裝玩具的安裝核心部件的過程.這使得VueRouter可以使用Vue.下面簡單介紹下Vue.use方法.

export function initUse(Vue: GlobalAPI) {
    Vue.use = function(plugin) {
        // 獲取當前外掛列表
        const installedPlugins = this._installedPlugins || (this._installedPlugins = []) if(this.installedPlugins.indexOf(plugin) > -1) {
            return this
        }
        const args = toArray(arguments, 1)
        // 插入vue
        args.unshift(this)
        // 一般外掛都有一個install函式
        // 通過該函式可以讓外掛使用Vue
        if(typeof plugin.install === 'function') {
            plugin.install.apply(plugin, args) // this指向plugin
        } else {
            plugin.apply(null, args)  // this指向window
        }
    }
}
複製程式碼

整體概覽

vue-router整體結構思維導圖

[精讀原始碼系列]前端路由Vue-Router

深入原始碼

接下來我會和你一起一步步深入原始碼,探究整個路由切換的實現邏輯.

目錄結構

拿到一個專案的原始碼的時候,我們首先要去觀察他的檔案結構,對專案的整體結構有一個大致的瞭解.主要的原始碼邏輯都在src目錄下:

|—— index.js
|—— install.js
└── create-matcher.js
└── create-route-map.js
|—— components
    |—— link.js
    |—— view.js
|—— history
    |—— abstract.js
    |—— base.js
    |—— errors.js
    |—— hash.js
    |—— html5.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
    |—— warn.js
複製程式碼
  • index.js整個專案的入口檔案,用來定義VueRouter這個類
  • install.js定義vue-router外掛的掛載邏輯
  • create-matcher.js是我們建立matcher的入口檔案
  • create-route-map.js用於建立路由對映表的js, pathMap,nameMap等
  • components資料夾存放的是vue-router的兩個元件,RouterView和RouterLink
  • history資料夾,存放三種路由模式的處理邏輯,hash模式,history模式,abstract模式以及路由跳轉錯誤的處理邏輯
  • util資料夾,顧名思義存放工具函式的地方

入口檔案

// ./index.js
import { install } from './install
import { START } from './util/route'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
import type { Matcher } from './create-matcher'
// ...
// VueRouter類
export default class VueRouter {
    constructor (options: RouterOptions = {}) {
        // 根例項
        this.app = null
        // 元件例項陣列
        this.apps = []
        // vue-router的鉤子函式
        this.beforeHooks = []
        this.resolveHooks = []
        this.afterHooks = []
        // 建立路由匹配例項,傳入我們定義的routes
        this.matcher = createMatcher(options.routes || [], this)
        // 判斷模式
        let mode = options.mode || 'hash'
        // fallback不等於false,且mode傳入history但是不支援pushState api的時候調整路由模式為hash
        this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
        if (this.fallback) {
            mode = 'hash'
        }
        // 非瀏覽器環境mode='abstract'
        if (!inBrowser) {
            mode = 'abstract'
        }
        this.mode = mode
        // 根據不同的模式建立對應的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}`)
            }
        }
    }
    // vue-router 初始化函式
    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.`
        )
        this.apps.push(app)
        // 建立一個元件銷燬的處理程式
        // https://github.com/vuejs/vue-router/issues/2639
        app.$once('hook:destroyed', () => {
            // 如果元件陣列中存在某個對應元件的例項,則清除
            const index = this.apps.indexOf(app)
            if (index > -1) this.apps.splice(index, 1)
            // 確保我們有一個根元件或者null如果沒有元件的情況下
            // we do not release the router so it can be reused
            if (this.app === app) this.app = this.apps[0] || null
        })
        // 根元件如果已經建立,則直接返回,我們不需要在建立一個新的history 監聽
        if (this.app) {
            return
        }
        // 否則建立根元件
        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物件的監聽的邏輯// cb函式賦值給history物件的cb屬性,以便在路由更新的時候呼叫
        history.listen(route => {
            this.apps.forEach((app) => {
                app._route = route
            })
        })
    }
    /*
        下面是
        vue-router的一系列api
        ...
    */
    VueRouter.install = install           // 掛載install函式
    VueRouter.version = '__VERSION__'     // 定義版本號
    // 判斷如果window上掛載了Vue則自動使用外掛
    if (inBrowser && window.Vue) {
        window.Vue.use(VueRouter)
    }
}
複製程式碼

模組安裝--install函式

import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
    // 如果已經安裝過,則直接返回
    if (install.installed && _Vue === Vue) return
    install.installed = true
    // 獲取Vue例項
    _Vue = 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, c   allVal)
        }
      }
  // 給Vue例項的鉤子函式混入一些屬性,並新增_route響應式物件
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        // 將根元件的_routerRoot屬相指向Vue例項
        this._routerRoot = this
        // 將根元件_router屬性指向傳入的router物件
        this._router = this.$options.router
        // router初始化,呼叫vueRouter的init方法
        this._router.init(this)
        // 呼叫Vue的defineReactive增加_route的響應式物件
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 將每個元件的_routerRoot屬性都指向根Vue例項
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 註冊vueComponent進行Observer處理
      registerInstance(this, this)
    },
    destroyed () {
      // 登出VueComponent
      registerInstance(this)
    }
  })
 // 給Vue例項新增 $router屬性,指向 _router 為VueRouter的例項
 // _route為一個存數量路有資料的物件
  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)
  // vue鉤子合併策略
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
複製程式碼

這一操作主要是:

  • 讓VueRouter可以使用Vue,

  • 給Vue例項的beforeCreate鉤子函式中混入以下邏輯:

    (1)加上_routerRoot, _router 屬性

    (2)初始化vueRouter

    (3) 新增_router的響應式物件,這樣Vue例項可以監聽到路由的變化

  • 全域性註冊兩個路由元件, RouterView和RouterLink

建立Matcher

還記得入口檔案裡有這樣一步操作嗎?

this.matcher = createMatcher(options.routes || [], this)
複製程式碼

下面讓我們一起來看看createMatcher函式.

// ./create-matcher.js
/*
    建立matcher
    @params { Array } routes 初始化的時候傳進來的路由配置
    @params { Object } router vueRouter的例項
 */
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // 建立路由對映表, pathList,路由path組成的陣列,
  // pathMap路由path和routeRecord組成的對映表
  // nameMap路由name和routeRecord組成的對映表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location
    if (name) {
      // 當前路由對應的name存在則在nameMap中查詢對應的record
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      // 如果沒找到對應的record則建立對應的record
      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 = {}
      }
      // 複製路由的引數到location中
      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)
    } 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)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }
  // 處理重定向
  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, router)
  }
  return {
    match,
    addRoutes
  }
}
function matchRoute (
  regex: RouteRegExp,
  path: string,
  params: Object
): boolean {
  const m = path.match(regex)
  if (!m) {
    return false
  } else if (!params) {
    return true
  }
  for (let i = 1, len = m.length; i < len; ++i) {
    const key = regex.keys[i - 1]
    const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
    if (key) {
      // Fix #1994: using * with props: true generates a param named 0
      params[key.name || 'pathMatch'] = val
    }
  }
  return true
}
// ...
複製程式碼

匹配路由之前會為每個路由建立對應的路由對映表

// ./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>
} {
    // pathList被建立用來控制path匹配優先順序
    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 => {
        // 為每個路由對映一個組裝好的record物件
        addRouteRecord(pathList, pathMap, nameMap, route)
    })
    // ensure wildcard routes are always at the end
    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
    }
}
// 為每個路由對映一個record物件
function addRouteRecord (
    pathList: Array<string>,
    pathMap: Dictionary<RouteRecord>,
    nameMap: Dictionary<RouteRecord>,
    route: RouteConfig,
    parent?: RouteRecord,
    matchAs?: string
) {
    // 從每個路由配置物件中解構出path和name
    const { path, name } = route
    if (process.env.NODE_ENV !== 'production') {
        // ...
    }
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  } 
  const record: RouteRecord = {
    path: normalizedPath,  // path
    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) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    if (process.env.NODE_ENV !== 'production') {
      if (
        route.name &&
        !route.redirect &&
        route.children.some(child => /^\/?$/.test(child.path))
      ) {
        // ...
      }
    }
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })  
}
if (!pathMap[record.path]) {
  pathList.push(record.path)
  // 建立path到record的對映
  pathMap[record.path] = record
}
if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if (process.env.NODE_ENV !== 'production' && alias === path) {
        // ...
      }
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    }
  }
  if (name) {
    if (!nameMap[name]) {
      // 建立name到record的對映
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      // ..
    }
  }
}
複製程式碼

這個檔案主要建立了三個變數:

  • pathList: 用來存放處理過的所有路由的path, 被建立用來控制path匹配的優先順序
  • pathMap: 路由的path與record物件的對映
  • nameMap: 路由的name與record物件的對映 record物件存放了完整的路由資訊,可以通過path或者name來對映出路由的完整資訊,來進行相關的路有操作.

三種路由模式

vur-router為我們提供了三種路由模式,對應的就是history資料夾裡存放的內容,base.js建立了history的基類,hash.js,html5.js和abstract.js則在基類的基礎上,擴充套件自身模式對應的屬性和方法.

  • hash模式
  • history模式
  • abstract模式

abstract模式是Node.js中使用的路由模式,主要原理是用陣列來模擬瀏覽器記錄,然後通過對陣列的操作來模擬路由的前進後退.這裡我們主要介紹瀏覽器中使用的兩種路由模式,hash模式和history模式.

base.js

export class History {
  router: Router          // vueRouter類
  base: string            // 基礎路徑
  current: Route          // 當前路由 
  pending: ?Route
  cb: (r: Route) => void  // 路由切換的回撥
  ready: boolean          //  
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  // implemented by sub-classes
  +go: (n: number) => void
  +push: (loc: RawLocation) => void
  +replace: (loc: RawLocation) => void
  +ensureURL: (push?: boolean) => void  
  +getCurrentLocation: () => string 
  constructor (router: Router, base: ?string) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
}
// 監聽路由切換
listen (cb: Function) {
  this.cb = cb
}
// 路由切換
onReady (cb: Function, errorCb: ?Function) {
    // 路由切換完成則執行回撥,否則push進readyCbs陣列中
    if (this.ready) {
      cb()
    } else {
      this.readyCbs.push(cb)
      if (errorCb) {
        this.readyErrorCbs.push(errorCb)
      }
    }
}
// 把發生錯誤時需要執行的回撥收集起來
onError (errorCb: Function) {
    this.errorCbs.push(errorCb)
}
// 核心函式,控制路由跳轉
transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
) {   
    // 獲取匹配的路由資訊
    const route = this.router.match(location, this.current)
    // 判斷是否跳轉
    this.confirmTransition(
      route,
      () => {
        // 更新路由
        this.updateRoute(route)
        // 執行跳轉完成的回撥
        onComplete && onComplete(route)
        // 暫且理解成修改瀏覽器地址
        this.ensureURL()
        // 保證readyCbs陣列中的回撥函式製備呼叫一次
        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)
          })
        }
      }
    )
}
/*
  * @mathods 判斷是否跳轉
  * @params { Route } route 匹配的路由物件
  * @params { Function } onComplete 跳轉完成時的回撥
  * @params { Function } onAbort 取消跳轉時的回撥
*/
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    const abort = err => {
      // after merging https://github.com/vuejs/vue-router/pull/2771 we
      // When the user navigates through history through back/forward buttons
      // we do not want to throw the error. We only throw it if directly calling
      // push/replace. That's why it's not included in isError
      // 當使用者通過瀏覽器操作前進後退按鈕的時候,我們不想丟擲錯誤,
      // 我們僅僅會在直接呼叫push和replace方法的時候丟擲錯誤
      if (!isExtendedError(NavigationDuplicated, err) && 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(new NavigationDuplicated(route))
    }
    // 下面是跳轉的邏輯

    // 通過對比解析出可複用的元件,失活的元件, 需要渲染的元件,
    // matched裡存放的是路由記錄的陣列
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
    // 切換路由要做的一系列任務佇列
    const queue: Array<?NavigationGuard> = [].concat(
      // 清除失活的元件, 通過觸發beforeRouteLeave導航鉤子,執行清除對應元件的路由記錄等邏輯
      extractLeaveGuards(deactivated),
      // global before hooks
      this.router.beforeHooks,
      // 可複用的元件, 通過觸發 beforeRouteUpdate 導航鉤子,來做一些更新邏輯
      extractUpdateHooks(updated),
      // in-config enter guards
      activated.map(m => m.beforeEnter),
      // 解析要啟用的非同步元件, 也有對應的 beforeRouteEnter導航鉤子
      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: '/' }) -> redirect
            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()
            })
          })
        }
      })
    })
    }
    // 更新路由
    updateRoute (route: Route) {
        const prev = this.current
        this.current = route
        this.cb && this.cb(route)
        this.router.afterHooks.forEach(hook => {
            hook && hook(route, prev)
        })
    }
}
複製程式碼

history物件的基礎類主要處理了路由跳轉的邏輯,在路由跳轉過程中,先獲取路由的匹配資訊,未找到匹配的路由資訊則建立新的路由,然後判斷是否跳轉.跳轉則比較跳轉前後的路由資訊,解析出失活的元件,可複用的元件和需要啟用的元件,並呼叫對應路由導航鉤子函式,從而更新路由資訊.

hash.js 和 html5.js

// hash.js
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()
  }

  // 這是一個延遲,知道app掛載完成,以免hashChange的監聽被過早的觸發
  setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    if (supportsScroll) {
      setupScroll()
    }
    // 開啟路由切換的監聽,如果支援pushState api,
    // 則監聽popState事件,不支援,則監聽hashChange事件
    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)
          }
        })
    }
    )
}
  // 跳轉到新的路由
  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)
  }
  // 更新url
  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
        push ? pushHash(current) : replaceHash(current)
    }
  }
  getCurrentLocation () {
    return getHash()
  }
}
複製程式碼

html5.js的內容和hash.js的內容大同小異,不同的就是hash.js中會去判斷瀏覽器是否支援pushState api,支援的話監聽popState事件,不支援的話監聽hashChange事件,而html5.js是直接監聽popState事件.

base基類擴充套件的HashHistory類和HTML5History類,主要增加了以下內容:

  • 監聽路由切換,保證在app掛載之後開啟監聽,index.js中相關程式碼

      // 否則建立根元件
      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物件擴充套件push.replace,go,ensureURL,getCurrentLocation等方法.

全域性路由元件

RouterView

export default {
  // 元件名稱
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // used by devtools to display a router-view badge
    data.routerView = true
    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    // 父元素的createElement方法
    const h = parent.$createElement
    const name = props.name
    // 獲取當前路由物件
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    // 獲取元件層級,知道 _routerRoot 指向Vue例項時終止迴圈
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode && parent.$vnode.data
      if (vnodeData) {
            if (vnodeData.routerView) {
              depth++
            }
            if (vnodeData.keepAlive && parent._inactive) {
              inactive = true
            }
        }
        parent = parent.$parent
    }
    data.routerViewDepth = depth
    // render previous view if the tree is inactive and kept-alive
    // 如果元件被快取,則渲染快取的元件
    if (inactive) {
      return h(cache[name], data, children)
    }
    // 根據元件層級去查詢route路由物件中匹配的元件
    const matched = route.matched[depth]
    // 如果沒找到匹配的元件,則渲染空節點
    if (!matched) {
      cache[name] = null
      return h()
    }
    // 將查詢出來的元件也賦值給cache[name]
    const component = cache[name] = matched.components[name]
    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    // 新增註冊鉤子, 鉤子會被注入到元件的生命週期鉤子中
    // 這會在install.js中給Vue中元件的生命週期混入鉤子中呼叫
    data.registerRouteInstance = (vm, val) => {
      // val could be undefined for unregistration
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
    // 給prepatch的鉤子函式也註冊該例項, 為了同一個元件可以在不同的路由下複用
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    //  給初始化的鉤子函式中也註冊該例項,以便路由發生變哈的時候啟用快取的元件
    data.hook.init = (vnode) => {
      if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
      ) {
        matched.instances[name] = vnode.componentInstance
      }
    }
    // 處理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]
        }
      }
    }
    // 對應當前route物件的元件存在, 且沒有在快取, 執行渲染操作
    return h(component, data, children)
  }
}
複製程式碼

RouterLink

// ./components/link.js
export default {
  // 元件名稱
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a' // 預設建立a標籤
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    // 獲取掛載的VueRouter例項
    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 = route.redirectedFrom
      ? createRoute(null, normalizeLocation(route.redirectedFrom), 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 }
    const scopedSlot =
      !this.$scopedSlots.$hasNormal &&
      this.$scopedSlots.default &&
      this.$scopedSlots.default({
        href,
        route,
        navigate: handler,
        isActive: classes[activeClass],
        isExactActive: classes[exactActiveClass]
      })
    if (scopedSlot) {
      if (scopedSlot.length === 1) {
        return scopedSlot[0]
      } else if (scopedSlot.length > 1 || !scopedSlot.length) {
        if (process.env.NODE_ENV !== 'production') {
          warn(
            false,
            `RouterLink with to="${
              this.props.to
            }" is trying to use a scoped slot but it didn't provide exactly one child.`
          )
        }
        return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
      }
    }
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // 從子元素中找到第一個a標籤,給他繫結事件監聽和設定href屬性
      // 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 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
        // 沒有找到a標籤,則繫結當前元素自身
        data.on = on
      }
    }
    return h(this.tag, data, this.$slots.default)
    }
}
複製程式碼

總結

vue-router的原始碼解讀到此就告一段落了,我的github倉庫有完整的程式碼註解和部分模組的思維導圖倉庫地址,大家如果對此有興趣,想要學習的同學一定先把vue-router git倉庫的程式碼克隆下來,對照我的解讀來看,或者可以先自己試著去讀,當然直白的讀原始碼會顯得有些枯燥,你可以嘗試帶著自己的問題或者疑惑去讀,有目的性的閱讀更容易堅持.不明白的地方可以多讀幾遍,原始碼中函式的邏輯往往用到了不同js檔案中的函式,要順著思路去往下捋.我沒有對util檔案裡用到的一些鉤子函式做過多的解讀,但是希望大家都可以去仔細研究下,可以收穫到更多的設計思路和程式設計技巧.剛入原始碼坑不久,如果有不對的或者解釋不到位的地方歡迎指出,有什麼建議或想法,歡迎留言或者加微信lj_de_wei_xin與我交流~

擴充套件閱讀

相關文章