VueRouter源碼分析

image

image

感謝

funfish, 玩弄心裏的鬼, Vue.js 技術揭祕的文章,對我的幫助

前言

vue-router的源碼不算很多, 但是內容也不算少。本文談不上逐行分析, 但是會盡量詳盡的說明主流程和原理。對一些工具函數和邊緣條件的處理會略過,因爲我也沒有逐行去了解它們,請見諒。

前置基礎知識

我們在學習VueRouter源碼前,先來複習下hash以及histroy相關的知識。更多細節請參考mdn文檔,本節內容節選自mdn文檔。

hash

onhashchange

當URL的片段標識符更改時,將觸發hashchange事件 (跟在#符號後面的URL部分,包括#符號)。注意 histroy.pushState() 絕對不會觸發 hashchange 事件,即使新的URL與舊的URL僅哈希不同也是如此。

histroy

pushState

pushState()需要三個參數: 一個狀態對象, 一個標題(目前被忽略), 和一個URL。

  • state, 狀態對象state是一個JavaScript對象,popstate事件觸發時,該對象會傳入回調函數
  • title, 目前所有瀏覽器忽略
  • url, 新的url記錄

replaceState

history.replaceState()的使用與history.pushState()非常相似,區別在於replaceState()是修改了當前的歷史記錄項而不是新建一個。

onpopstate

調用history.pushState()或者history.replaceState()不會觸發popstate事件. popstate事件只會在瀏覽器某些行爲下觸發, 比如點擊後退、前進按鈕(或者在JavaScript中調用history.back()、history.forward()、history.go()方法)。

如果當前處於激活狀態的歷史記錄條目是由history.pushState()方法創建, 或者由history.replaceState()方法修改過的, 則popstate事件對象的state屬性包含了這個歷史記錄條目的state對象的一個拷貝。

應用初始化

通常構建一個Vue應用的時候, 我們會使用Vue.use以插件的形式安裝VueRouter。同時會在Vue的實例上掛載router的實例。

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

let a = new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
    }
  ]
})

插件的安裝

在Vue的文檔中指出Vue.js 的插件應該有一個公開方法 install。這個方法的第一個參數是 Vue 構造器,第二個參數是一個可選的選項對象, 我們首先查看源碼中install.js的文件。

在install文件中, 我們在Vue的實例上初始化了一些私有屬性

  • _routerRoot, 指向了Vue的實例
  • _router, 指向了VueRouter的實例

在Vue的prototype上初始化了一些getter

  • $router, 當前Router的實例
  • $route, 當前Router的信息

並且在全局混入了mixin, 已經全局註冊了RouterView, RouterLink組件.


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

  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.mixin({
    beforeCreate () {
      // 判斷是否實例是否掛載了router
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // _router, 劫持的是當前的路由
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

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

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

  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

Vue.util.defineReactive, 這是Vue裏面觀察者劫持數據的方法,劫持_route,當_route觸發setter方法的時候,則會通知到依賴的組件。而RouterView, 需要訪問parent.$route所以形成了依賴(我們在後面會看到)

👀我們到Vue中看一下defineReactive的源碼, 在defineReactive, 會對_route使用Object.defineProperty劫持setter方法。set時會通知觀察者。



Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    // ...
  },
  set: function reactiveSetter (newVal) {
    // ...
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})

VueRouter實例


export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'
    // fallback會在不支持history環境的情況下, 回退到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}`)
        }
    }
  }
}

matcher

matcher對象中包含了兩個屬性, addRoutes, match。

pathList, pathMap, nameMap

pathList, pathMap, nameMap分別是路徑的列表, 路徑和路由對象的映射, 路由名稱和路由對象的映射。vue-router目標支持動態路由, pathList, pathMap, nameMap可以在初始化後動態的被修改。它們由createRouteMap方法創建, 我們來看看createRouteMap的源碼。


export function createRouteMap (
  routes,
  oldPathList,
  oldPathMap,
  oldNameMap
) {
  // pathList,pathMap,nameMap支持後續的動態添加
  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)
  })

  // 將通配符的路徑, push到pathList的末尾
  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
  }
}

routes爲一組路由, 所以我們循環routes, 但是route可能存在children所以我們通過遞歸的形式創建route。返回一個route的樹🌲


function addRouteRecord (
  pathList,
  pathMap,
  nameMap,
  route,
  parent,
  matchAs
) {
  const { path, name } = route
 
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}

  // normalizePath, 會對path進行格式化
  // 會刪除末尾的/,如果route是子級,會連接父級和子級的path,形成一個完整的path
  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 }
  }

  // 如果route存在children, 我們會遞歸的創建路由對象
  // 遞歸的創建route對象
  if (route.children) {
    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
      )
    })
  }

  // 填充pathMap,nameMap,pathList
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    }
  }
}

addRoutes

動態添加更多的路由規則, 並動態的修改pathList,pathMap,nameMap

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

match

match方法根據參數raw(可以是字符串也可以Location對象), 以及currentRoute(當前的路由對象返回Route對象),在nameMap中查找對應的Route,並返回。

如果location包含name, 我通過nameMap找到了對應的Route, 但是此時path中可能包含params, 所以我們會通過fillParams函數將params填充到patch,返回一個真實的路徑path。


function match (
  raw,
  currentRoute,
  redirectedFrom
) {
  // 會對raw,currentRoute處理,返回格式化後path, hash, 以及params
  const location = normalizeLocation(raw, currentRoute, false, router)

  const { name } = location

  if (name) {
    const record = nameMap[name]
    if (!record) return _createRoute(null, location)
    
    // 獲取所有必須的params。如果optional爲true說明params不是必須的
    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]
        }
      }
    }

    if (record) {
      // 使用params對path進行填充返回一個真實的路徑
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      // 創建Route對象
      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]
      // 使用pathList中的每一個regex,對path進行匹配
      if (matchRoute(record.regex, location.path, location.params)) {
        return _createRoute(record, location, redirectedFrom)
      }
    }
  }
  return _createRoute(null, location)
}

我們接下來繼續看看_createRoute中做了什麼。


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

其中redirect,alias最終都會調用createRoute方法。我們再將視角轉向createRoute函數。createRoute函數會返回一個凍結的Router對象。

其中matched屬性爲一個數組,包含當前路由的所有嵌套路徑片段的路由記錄。數組的順序爲從外向裏(樹的外層到內層)。


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)
  }
  return Object.freeze(route)
}

init

init中。會掛載cb的回調,這關乎到RouteView的渲染。我們根據當前的url,在Vue根實例的beforeCreate生命週期鉤子中完成路由的初始化,完成第一次的路由導航。


init (app) {

  // app爲Vue的實例
  this.apps.push(app)

  if (this.app) {
    return
  }

  // 在VueRouter上掛載app屬性
  this.app = app

  const history = this.history

  // 初始化當前的路由,完成第一次導航,在hash模式下會在transitionTo的回調中調用setupListeners
  // setupListeners裏會對hashchange事件進行監聽
  // transitionTo是進行路由導航的函數,我們將會在下面介紹
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }

  // 掛載了回調的cb, 每次更新路由更好更新_route
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

history

history一共有三個模式hash, histroy, abstract, 這三個類都繼承至base類

base

我們首先看下base的構造函數, 其中router是VueRouter的實例, base是路由的基礎路徑。current是當前的路由默認爲"/", ready是路由的狀態, readyCbs是ready的回調的集合, readyErrorCbs是raday失敗的回調。errorCbs導航出錯的回調的集合。


export class History {
  constructor (router: Router, base: ?string) {
    this.router = router
    // normalizeBase會對base路徑做出格式化的處理,會爲base開頭自動添加‘/’,刪除結尾的‘/’,默認返回’/‘
    this.base = normalizeBase(base)
    // 初始化的當前路由對象
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
}

export const START = createRoute(null, {
  path: '/'
})

function normalizeBase (base: ?string): string {
  if (!base) {
    // inBrowser判斷是否爲瀏覽器環境
    if (inBrowser) {
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      base = base.replace(/^https?:\/\/[^\/]+/, '')
    } else {
      base = '/'
    }
  }
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  return base.replace(/\/$/, '')
}

base中的listen的方法,會在VueRouter的init方法中使用到,listen會給每一次的路由的更新,添加回調


listen (cb: Function) {
  this.cb = cb
}   

base類中還有一些其他方法比如,transitionTo,confirmTransition,updateRoute它們在base子類中被使用。我們馬上在hashrouter中再看看它們的具體實現。

HashRouter

構造函數

在HashHistory的構造函數中。我們會判斷當前的fallback是否爲true。如果爲true,使用checkFallback,添加’#‘,並使用window.location.replace替換文檔。

如果fallback爲false,我們會調用ensureSlash,ensureSlash會爲沒有“#”的url,添加“#”,並且使用histroy的API或者replace替換文檔。

所以我們在訪問127.0.0.1的時候,會自動替換爲127.0.0.1/#/


export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // 如果是回退hash的情況,並且判斷當前路徑是否有/#/。如果沒有將會添加'/#/'
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
}

checkFallback


// 檢查url是否包含‘/#/’
function checkFallback (base) {
  // 獲取hash值
  const location = getLocation(base)
  // 如果location不是以/#,開頭。添加/#,使用window.location.replace替換文檔
  if (!/^\/#/.test(location)) {
    window.location.replace(
      cleanPath(base + '/#' + location)
    )
    return true
  }
}
// 返回hash
export function getLocation (base) {
  let path = decodeURI(window.location.pathname)
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

// 刪除 //, 替換爲 /
export function cleanPath (path) {
  return path.replace(/\/\//g, '/')
}

ensureSlash


function ensureSlash (): boolean {
  // 判斷是否包含#,並獲取hash值。如果url沒有#,則返回‘’
  const path = getHash()
  // 判斷path是否以/開頭
  if (path.charAt(0) === '/') {
    return true
  }
  // 如果開頭不是‘/’, 則添加/
  replaceHash('/' + path)
  return false
}
// 獲取“#”後面的hash
export function getHash (): string {
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : decodeURI(href.slice(index + 1))
}
function replaceHash (path) {
  // supportsPushState判斷是否存在history的API
  // 使用replaceState或者window.location.replace替換文檔
  // getUrl獲取完整的url
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
// getUrl返回了完整了路徑,並且會添加#, 確保存在/#/
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

在replaceHash中,我們調用了replaceState方法,在replaceState方法中,又調用了pushState方法。在pushState中我們會調用saveScrollPosition方法,它會記錄當前的滾動的位置信息。然後使用histroyAPI,或者window.location.replace完成文檔的更新。


export function replaceState (url?: string) {
  pushState(url, true)
}

export function pushState (url?: string, replace?: boolean) {
  // 記錄當前的x軸和y軸,以發生導航的時間爲key,位置信息記錄在positionStore中
  saveScrollPosition()
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

push, replace,

我們把push,replace放在一起說,因爲它們實現的源碼都是類似的。在push和replace中,調用transitionTo方法,transitionTo方法在基類base中,我們現在轉過頭來看看transitionTo的源碼(👇往下兩節,代碼不是很難,但是callback嵌套callback, 如蜜傳如蜜,看起來還是比較噁心的)


push (location, onComplete, onAbort) {
  const { current: fromRoute } = this
  this.transitionTo(
    location,
    route => {
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    },
    onAbort
  )
}

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

transitionTo, confirmTransition, updateRoute

image

transitionTo的location參數是我們的目標路徑, 可以是string或者RawLocation對象。我們通過router.match方法(我們在在matcher介紹過),router.match會返回我們的目標路由對象。緊接着我們會調用confirmTransition函數。


transitionTo (location, onComplete, onAbort) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(
    route,
    () => {
      // ...
    },
    err => {
      // ...
    }
  )
}

confirmTransition函數中會使用,isSameRoute會檢測是否導航到相同的路由,如果導航到相同的路由會停止🤚導航,並執行終止導航的回調。


if (
  isSameRoute(route, current) &&
  route.matched.length === current.matched.length
) {
  this.ensureURL()
  return abort()
}

接着我們調用resolveQueue方法,resolveQueue接受當前的路由和目標的路由的matched屬性作爲參數,resolveQueue的工作方式可以如下圖所示。我們會逐一比較兩個數組的路由,尋找出需要銷燬的,需要更新的,需要激活的路由,並返回它們(因爲我們需要執行它們不同的路由守衛)

image

function resolveQueue (
  current
  next
) {
  let i
  // 依次比對當前的路由和目標的路由的matched屬性中的每一個路由
  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)
  }
}

下一步,我們會逐一提取出,所有要執行的路由守衛,將它們concat到隊列queue。queue裏存放裏所有需要在這次路由更新中執行的路由守衛。

第一步,我們使用extractLeaveGuards函數,提取出deactivated中所有需要銷燬的組件內的“beforeRouteLeave”的守衛。extractLeaveGuards函數中會調用extractGuards函數,extractGuards函數,會調用flatMapComponents函數,flatMapComponents函數會遍歷records(resolveQueue返回deactivated), 在遍歷過程中我們將組件,組件的實例,route對象,傳入了fn(extractGuards中傳入flatMapComponents的回調), 在fn中我們會獲取組件中beforeRouteLeave守衛。


// 返回每一個組件中導航的集合
function extractLeaveGuards (deactivated) {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractGuards (
  records,
  name,
  bind,
  reverse?
) {
  const guards = flatMapComponents(
    records,
    // def爲組件
    // instance爲組件的實例
    (def, instance, match, key) => {
      // 返回每一個組件中定義的路由守衛
      const guard = extractGuard(def, name)
      if (guard) {
        // bindGuard函數確保了guard(路由守衛)的this指向的是Component中的實例
        return Array.isArray(guard)
          ? guard.map(guard => bind(guard, instance, match, key))
          : bind(guard, instance, match, key)
      }
    }
  )
  // 返回導航的集合
  return flatten(reverse ? guards.reverse() : guards)
}

export function flatMapComponents (
  matched,
  fn
) {
  // 遍歷matched,並返回matched中每一個route中的每一個Component
  return flatten(matched.map(m => {
    // 如果沒有設置components則默認是components{ default: YouComponent },可以從addRouteRecord函數中看到
    // 將每一個matched中所有的component傳入fn中
    // m.components[key]爲components中的key鍵對應的組件
    // m.instances[key]爲組件的實例,這個屬性是在routerview組件中beforecreated中被賦值的
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m,
      key
    ))
  }))
}

// 返回一個新數組
export function flatten (arr) {
  return Array.prototype.concat.apply([], arr)
}

// 獲取組件中的屬性
function extractGuard (def, key) {
  if (typeof def !== 'function') {
    def = _Vue.extend(def)
  }
  return def.options[key]
}

// 修正函數的this指向
function bindGuard (guard, instance) {
  if (instance) {
    return function boundRouteGuard () {
      return guard.apply(instance, arguments)
    }
  }
}

第二步,獲取全局VueRouter對象beforeEach的守衛

第三步, 使用extractUpdateHooks函數,提取出update組件中所有的beforeRouteUpdate的守衛。過程同第一步類似。

第四步, 獲取activated的options配置中beforeEach守衛

第五部, 獲取所有的異步組件


在獲取所有的路由守衛後我們定義了一個迭代器iterator。接着我們使用runQueue遍歷queue隊列。將queue隊列中每一個元素傳入fn(迭代器iterator)中,在迭代器中會執行路由守衛,並且路由守衛中必須明確的調用next方法纔會進入下一個管道,進入下一次迭代。迭代完成後,會執行runQueue的callback。

在runQueue的callback中,我們獲取激活組件內的beforeRouteEnter的守衛,並且將beforeRouteEnter守衛中next的回調存入postEnterCbs中,在導航被確認後遍歷postEnterCbs執行next的回調。

在queue隊列執行完成後,confirmTransition函數會執行transitionTo傳入的onComplete的回調。往下看👇

// queue爲路由守衛的隊列
// fn爲定義的迭代器
export function runQueue (queue, fn, cb) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        // 使用迭代器處理每一個鉤子
        // fn是迭代器
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

// 迭代器
const iterator = (hook, next) => {
  if (this.pending !== route) {
    return abort()
  }
  try {
    // 傳入路由守衛三個參數,分別分別對應to,from,next
    hook(route, current, (to: any) => {
      if (to === false || isError(to)) {
        // 如果next的參數爲false
        this.ensureURL(true)
        abort(to)
      } else if (
        // 如果next需要重定向到其他路由
        typeof to === 'string' ||
        (typeof to === 'object' && (
          typeof to.path === 'string' ||
          typeof to.name === 'string'
        ))
      ) {
        abort()
        if (typeof to === 'object' && to.replace) {
          this.replace(to)
        } else {
          this.push(to)
        }
      } else {
        // 進入下個管道
        next(to)
      }
    })
  } catch (e) {
    abort(e)
  }
}

runQueue(
  queue,
  iterator,
  () => {
    const postEnterCbs = []
    const isValid = () => this.current === route
    // 獲取所有激活組件內部的路由守衛beforeRouteEnter,組件內的beforeRouteEnter守衛,是無法獲取this實例的
    // 因爲這時激活的組件還沒有創建,但是我們可以通過傳一個回調給next來訪問組件實例。
    // beforeRouteEnter (to, from, next) {
    //   next(vm => {
    //     // 通過 `vm` 訪問組件實例
    //   })
    // }
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    // 獲取全局的beforeResolve的路由守衛
    const queue = enterGuards.concat(this.router.resolveHooks)
    // 再一次遍歷queue
    runQueue(queue, iterator, () => {
      // 完成過渡
      if (this.pending !== route) {
        return abort()
      }
      // 正在過渡的路由設置爲null
      this.pending = null
      // 
      onComplete(route)
      // 導航被確認後,我們執行beforeRouteEnter守衛中,next的回調
      if (this.router.app) {
        this.router.app.$nextTick(() => {
          postEnterCbs.forEach(cb => { cb() })
        })
      }
    }
  )
})

// 獲取組件中的beforeRouteEnter守衛
function extractEnterGuards (
  activated,
  cbs,
  isValid
) {
  return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
    // 這裏沒有修改guard(守衛)中this的指向
    return bindEnterGuard(guard, match, key, cbs, isValid)
  })
}

// 將beforeRouteEnter守衛中next的回調push到postEnterCbs中
function bindEnterGuard (
  guard,
  match,
  key,
  cbs,
  isValid
) {
  // 這裏的next參數是迭代器中傳入的參數
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      // 執行迭代器中傳入的next,進入下一個管道
      next(cb)
      if (typeof cb === 'function') {
        // 我們將next的回調包裝後保存到cbs中,next的回調會在導航被確認的時候執行回調
        cbs.push(() => {
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}

在confirmTransition的onComplete回調中,我們調用updateRoute方法, 參數是導航的路由。在updateRoute中我們會更新當前的路由(history.current), 並執行cb(更新Vue實例上的_route屬性,🌟這會觸發RouterView的重新渲染


updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  // 執行after的鉤子
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}

接着我們執行transitionTo的回調函數onComplete。在回調中會調用replaceHash或者pushHash方法。它們會更新location的hash值。如果兼容historyAPI,會使用history.replaceState或者history.pushState。如果不兼容historyAPI會使用window.location.replace或者window.location.hash。而handleScroll方法則是會更新我們的滾動條的位置我們這裏就不在細說了。


// replaceHash方法
(route) => {
  replaceHash(route.fullPath)
  handleScroll(this.router, route, fromRoute, false)
  onComplete && onComplete(route)
}

// push方法
route => {
  pushHash(route.fullPath)
  handleScroll(this.router, route, fromRoute, false)
  onComplete && onComplete(route)
}

好了,現在我們就把,replace或者push方法的流程說完了。

🎉🎉🎉🎉🎉🎉 以下是transitionTo,confirmTransition中完整的代碼。 🎉🎉🎉🎉🎉🎉


// onComplete 導航成功的回調
// onAbort 導航終止的回調
transitionTo (location, onComplete, onAbort) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(route,
    () => {
      this.updateRoute(route)
      onComplete && onComplete(route)
      this.ensureURL()
      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) })
      }
    }
  )
}

// onComplete 導航成功的回調
// onAbort 導航終止的回調
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) })
      }
    }
    onAbort && onAbort(err)
  }
  
  // 判斷是否導航到相同的路由,如果是我們終止導航
  if (
    isSameRoute(route, current) &&
    route.matched.length === current.matched.length
  ) {
    this.ensureURL()
    return abort()
  }

  // 獲取所有需要激活,更新,銷燬的路由
  const {
    updated,
    deactivated,
    activated
  } = resolveQueue(this.current.matched, route.matched)

  // 獲取所有需要執行的路由守衛
  const queue = [].concat(
    extractLeaveGuards(deactivated),
    this.router.beforeHooks,
    extractUpdateHooks(updated), 
    activated.map(m => m.beforeEnter),
    resolveAsyncComponents(activated)
  )

  this.pending = route

  // 定義迭代器
  const iterator = (hook: NavigationGuard, next) => {
    if (this.pending !== route) {
      return abort()
    }
    try {
      hook(route, current, (to: any) => {
        if (to === false || isError(to)) {
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' ||
          (typeof to === 'object' && (
            typeof to.path === 'string' ||
            typeof to.name === 'string'
          ))
        ) {
          abort()
          if (typeof to === 'object' && to.replace) {
            this.replace(to)
          } else {
            this.push(to)
          }
        } else {
          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() })
          })
        }
      }
    )
  })
}

go, forward, back

在VueRouter上定義的go,forward,back方法都是調用history的屬性的go方法

// index.js

go (n) {
  this.history.go(n)
}

back () {
  this.go(-1)
}

forward () {
  this.go(1)
}

而hash上go方法調用的是history.go,它是如何更新RouteView的呢?答案是hash對象在setupListeners方法中添加了對popstate或者hashchange事件的監聽。在事件的回調中會觸發RoterView的更新

// go方法調用history.go
go (n) {
  window.history.go(n)
}

setupListeners

我們在通過點擊後退, 前進按鈕或者調用back, forward, go方法的時候。我們沒有主動更新_app.route和current。我們該如何觸發RouterView的更新呢?通過在window上監聽popstate,或者hashchange事件。在事件的回調中,調用transitionTo方法完成對_route和current的更新。

或者可以這樣說,在使用push,replace方法的時候,hash的更新在_route更新的後面。而使用go, back時,hash的更新在_route更新的前面。


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

HistoryRouter

HistoryRouter的實現基本於HashRouter一致。差異在於HistoryRouter不會做一些容錯處理,不會判斷當前環境是否支持historyAPI。默認監聽popstate事件,默認使用histroyAPI。感興趣的同學可以看/history/html5.js中關於HistoryRouter的定義。

組件

RouterView

RouterView是可以互相嵌套的,RouterView依賴了parent.$route屬性,parent.$route即this._routerRoot._route。我們使用Vue.util.defineReactive將_router設置爲響應式的。在transitionTo的回調中會更新_route, 這會觸發RouteView的渲染。(渲染機制目前不是很瞭解,目前還沒有看過Vue的源碼,猛男落淚)。

export default {
  name: 'RouterView',
  functional: true,
  // RouterView的name, 默認是default
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true

    // h爲渲染函數
    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循環找到Vue的根節點, _routerRoot是Vue的根實例
    // depth爲當前的RouteView的深度,因爲RouteView可以互相嵌套,depth可以幫組我們找到每一級RouteView需要渲染的組件
    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]

    // registerRouteInstance會在beforeCreated中調用,又全局的Vue.mixin實現
    // 在matched.instances上註冊組件的實例, 這會幫助我們修正confirmTransition中執行路由守衛中內部的this的指向
    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)
  }
}

結語

我們把VueRouter源碼看完了。總體來說不是很複雜。總的來說就是使用Vue.util.defineReactive將實例的_route屬性設置爲響應式。而push, replace方法會主動更新屬性_route。而go,back,或者點擊前進後退的按鈕則會在onhashchange或者onpopstate的回調中更新_route,而_route的更新會觸發RoterView的重新渲染

但是也略過了比如keep-live,滾動行爲的處理。我打算接下來,結合VueRouter核心原理實現了一個簡易版的VueRouter,當然現在還沒有開始。

其他

從3月中下旬左右一直在學一些庫的源碼,本身學習源碼對工作幫助並不是很大。因爲像VueRouter,Preact都有着完善的文檔。看源碼單純是個人的興趣,不過學習了這些庫的源碼,自己實現一個簡易版本,還是挺有成就感的一件事情。

Preact源碼分析

簡易的React的實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章