Vue 源碼深入解析之 matcher 和 路徑切換

一、 matcher

  1. matcher 相關的實現都在 src/create-matcher.js 中,我們先來看一下 matcher 的數據結構:
export type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
};
  1. Matcher 返回了兩個方法,matchaddRoutes,在之前我們接觸到了 match 方法,顧名思義它是做匹配,那麼匹配的是什麼,在介紹之前,我們先了解路由中重要的兩個概念,LoactionRoute,它們的數據結構定義在 flow/declarations.js 中,如下所示:
  • Location
declare type Location = {
  _normalized?: boolean;
  name?: string;
  path?: string;
  hash?: string;
  query?: Dictionary<string>;
  params?: Dictionary<string>;
  append?: boolean;
  replace?: boolean;
}

Vue-Router 中定義的 Location 數據結構和瀏覽器提供的 window.location 部分結構有點類似,它們都是對 url 的結構化描述。舉個例子:/abc?foo=bar&baz=qux#hello,它的 path/abcquery{foo:'bar',baz:'qux'}Location 的其他屬性我們之後會介紹。

  • Route
declare type Route = {
  path: string;
  name: ?string;
  hash: string;
  query: Dictionary<string>;
  params: Dictionary<string>;
  fullPath: string;
  matched: Array<RouteRecord>;
  redirectedFrom?: string;
  meta?: any;
}

Route 表示的是路由中的一條線路,它除了描述了類似 Loctaionpathqueryhash 這些概念,還有 matched 表示匹配到的所有的 RouteRecordRoute 的其他屬性我們之後會介紹。

  1. createMatcher,在瞭解了 LocationRoute 後,我們來看一下 matcher 的創建過程:
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

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

  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

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

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

      if (record) {
        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)
        }
      }
    }
    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, router)
  }

  return {
    match,
    addRoutes
  }
}
  1. createMatcher 接收兩個參數,一個是 router,它是我們 new VueRouter 返回的實例,一個是 routes,它是用戶定義的路由配置,來看一下我們之前舉的例子中的配置:
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]
  1. createMathcer 首先執行的邏輯是 const { pathList, pathMap, nameMap } = createRouteMap(routes) 創建一個路由映射表,createRouteMap 的定義在 src/create-route-map 中:
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 || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

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

  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }

  return {
    pathList,
    pathMap,
    nameMap
  }
}

  1. createRouteMap 函數的目標是把用戶的路由配置轉換成一張路由映射表,它包含三個部分,pathList 存儲所有的 pathpathMap 表示一個 pathRouteRecord 的映射關係,而 nameMap 表示 nameRouteRecord 的映射關係。那麼 RouteRecord 到底是什麼,先來看一下它的數據結構:
declare type RouteRecord = {
  path: string;
  regex: RouteRegExp;
  components: Dictionary<any>;
  instances: Dictionary<any>;
  name: ?string;
  parent: ?RouteRecord;
  redirect: ?RedirectOption;
  matchAs: ?string;
  beforeEnter: ?NavigationGuard;
  meta: any;
  props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}

它的創建是通過遍歷 routes 爲每一個 route 執行 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 || {}
  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) {
    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 || '/'
      )
    })
  }

  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}" }`
      )
    }
  }
}
  1. 我們只看幾個關鍵邏輯,首先創建 RouteRecord 的代碼如下:
 const record: RouteRecord = {
  path: normalizedPath,
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
  components: route.components || { default: route.component },
  instances: {},
  name,
  parent,
  matchAs,
  redirect: route.redirect,
  beforeEnter: route.beforeEnter,
  meta: route.meta || {},
  props: route.props == null
    ? {}
    : route.components
      ? route.props
      : { default: route.props }
}
  1. 這裏要注意幾個點,path 是規範化後的路徑,它會根據 parentpath 做計算;regex 是一個正則表達式的擴展,它利用了path-to-regexp 這個工具庫,把 path 解析成一個正則表達式的擴展,舉個例子:
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
  1. components 是一個對象,通常我們在配置中寫的 component 實際上這裏會被轉換成 {components: route.component}instances 表示組件的實例,也是一個對象類型;parent 表示父的 RouteRecord,因爲我們配置的時候有時候會配置子路由,所以整個 RouteRecord 也就是一個樹型結構,如下所示:
if (route.children) {
  // ...
  route.children.forEach(child => {
  const childMatchAs = matchAs
    ? cleanPath(`${matchAs}/${child.path}`)
    : undefined
  addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
  1. 如果配置了 children,那麼遞歸執行 addRouteRecord 方法,並把當前的 record 作爲 parent 傳入,通過這樣的深度遍歷,我們就可以拿到一個 route 下的完整記錄,如下所示:
if (!pathMap[record.path]) {
  pathList.push(record.path)
  pathMap[record.path] = record
}

pathListpathMap 各添加一條記錄。

if (name) {
  if (!nameMap[name]) {
    nameMap[name] = record
  }
  // ...
}
  1. 如果我們在路由配置中配置了 name,則給 nameMap 添加一條記錄。由於 pathListpathMapnameMap 都是引用類型,所以在遍歷整個 routes 過程中去執行 addRouteRecord 方法,會不斷給他們添加數據。那麼經過整個 createRouteMap 方法的執行,我們得到的就是 pathListpathMapnameMap。其中 pathList 是爲了記錄路由配置中的所有 path,而 pathMapnameMap 都是爲了通過 pathname 能快速查到對應的 RouteRecord

  2. createMatcher 函數,接下來就定義了一系列方法,最後返回了一個對象,如下所示:

return {
  match,
  addRoutes
}

也就是說,matcher 是一個對象,它對外暴露了 matchaddRoutes 方法。

  1. addRoutesaddRoutes 方法的作用是動態添加路由配置,因爲在實際開發中有些場景是不能提前把路由寫死的,需要根據一些條件動態添加路由,所以 Vue-Router 也提供了這一接口:
function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}

addRoutes 的方法十分簡單,再次調用 createRouteMap 即可,傳入新的 routes 配置,由於 pathListpathMapnameMap 都是引用類型,執行 addRoutes 後會修改它們的值。

  1. match,如下所示:
function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location
): Route {
  const location = normalizeLocation(raw, currentRoute, false, router)
  const { name } = location

  if (name) {
    const record = nameMap[name]
    if (process.env.NODE_ENV !== 'production') {
      warn(record, `Route with name '${name}' does not exist`)
    }
    if (!record) return _createRoute(null, location)
    const paramNames = record.regex.keys
      .filter(key => !key.optional)
      .map(key => key.name)

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

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

    if (record) {
      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)
      }
    }
  }
  
  return _createRoute(null, location)
}
  1. match 方法接收三個參數,其中 rawRawLocation 類型,它可以是一個 url 字符串,也可以是一個 Location 對象;currentRouteRoute 類型,它表示當前的路徑;redirectedFrom 和重定向相關,這裏先忽略。match 方法返回的是一個路徑,它的作用是根據傳入的 raw 和當前的路徑 currentRoute 計算出一個新的路徑並返回。首先執行了 normalizeLocation,它的定義在 src/util/location.js 中:
export function normalizeLocation (
  raw: RawLocation,
  current: ?Route,
  append: ?boolean,
  router: ?VueRouter
): Location {
  let next: Location = typeof raw === 'string' ? { path: raw } : raw
  if (next.name || next._normalized) {
    return next
  }

  if (!next.path && next.params && current) {
    next = assign({}, next)
    next._normalized = true
    const params: any = assign(assign({}, current.params), next.params)
    if (current.name) {
      next.name = current.name
      next.params = params
    } else if (current.matched.length) {
      const rawPath = current.matched[current.matched.length - 1].path
      next.path = fillParams(rawPath, params, `path ${current.path}`)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(false, `relative params navigation requires a current route.`)
    }
    return next
  }

  const parsedPath = parsePath(next.path || '')
  const basePath = (current && current.path) || '/'
  const path = parsedPath.path
    ? resolvePath(parsedPath.path, basePath, append || next.append)
    : basePath

  const query = resolveQuery(
    parsedPath.query,
    next.query,
    router && router.options.parseQuery
  )

  let hash = next.hash || parsedPath.hash
  if (hash && hash.charAt(0) !== '#') {
    hash = `#${hash}`
  }

  return {
    _normalized: true,
    path,
    query,
    hash
  }
}
  1. normalizeLocation 方法的作用是根據 rawcurrent 計算出新的 location,它主要處理了 raw 的兩種情況,一種是有 params 且沒有 path,一種是有 path 的,對於第一種情況,如果 currentname,則計算出的 location 也有 name

  2. 計算出新的 location 後,對 locationnamepath 的兩種情況做了處理,如下所示:

  • name,有 name 的情況下就根據 nameMap 匹配到 record,它就是一個 RouterRecord 對象,如果 record 不存在,則匹配失敗,返回一個空路徑;然後拿到 record 對應的 paramNames,再對比 currentRoute 中的 params,把交集部分的 params 添加到 location 中,然後在通過 fillParams 方法根據 record.pathlocation.path 計算出 location.path,最後調用 _createRoute(record, location, redirectedFrom) 去生成一條新路徑。

  • path,通過 name 我們可以很快的找到 record,但是通過 path 並不能,因爲我們計算後的 location.path 是一個真實路徑,而 record 中的 path 可能會有 param,因此需要對所有的 pathList 做順序遍歷, 然後通過 matchRoute 方法根據 record.regexlocation.pathlocation.params 匹配,如果匹配到則也通過 _createRoute(record, location, redirectedFrom) 去生成一條新路徑。因爲是順序遍歷,所以我們書寫路由配置要注意路徑的順序,因爲寫在前面的會優先嚐試匹配。

  1. 最後我們來看一下 _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)
}

我們先不考慮 record.redirectrecord.matchAs 的情況,最終會調用 createRoute 方法,它的定義在 src/uitl/route.js 中:

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)
}
  1. createRoute 可以根據 recordlocation 創建出來,最終返回的是一條 Route 路徑,我們之前也介紹過它的數據結構。在 Vue-Router 中,所有的 Route 最終都會通過 createRoute 函數創建,並且它最後是不可以被外部修改的。Route 對象中有一個非常重要屬性是 matched,它通過 formatMatch(record) 計算而來:
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}

可以看它是通過 record 循環向上找 parent,直到找到最外層,並把所有的 record 都 push 到一個數組中,最終返回的就是 record 的數組,它記錄了一條線路上的所有 recordmatched 屬性非常有用,它爲之後渲染組件提供了依據。

  1. 總結:matcher 相關的主流程的分析就結束了,我們瞭解了 LocationRouteRouteRecord 等概念。並通過 matchermatch 方法,我們會找到匹配的路徑 Route,這個對 Route 的切換,組件的渲染都有非常重要的指導意義。

二、路徑切換

  1. history.transitionToVue-Router 中非常重要的方法,當我們切換路由線路的時候,就會執行到該方法,之前我們分析了 matcher 的相關實現,知道它是如何找到匹配的新線路,那麼匹配到新線路後又做了哪些事情,接下來我們來完整分析一下 transitionTo 的實現,它的定義在 src/history/base.js 中:
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()

    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) })
    }
  })
}
  1. transitionTo 首先根據目標 location 和當前路徑 this.current 執行 this.router.match 方法去匹配到目標的路徑。這裏 this.currenthistory 維護的當前路徑,它的初始值是在 history 的構造函數中初始化的:
this.current = START

START 的定義在 src/util/route.js 中:

export const START = createRoute(null, {
  path: '/'
})
  1. 這樣就創建了一個初始的 Route,而 transitionTo 實際上也就是在切換 this.current,稍後我們會看到。拿到新的路徑後,那麼接下來就會執行 confirmTransition 方法去做真正的切換,由於這個過程可能有一些異步的操作(如異步組件),所以整個 confirmTransition API 設計成帶有成功回調函數和失敗回調函數,先來看一下它的定義:
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) &&
    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),
    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() })
        })
      }
    })
  })
}
  1. 首先定義了 abort 函數,然後判斷如果滿足計算後的 routecurrent 是相同路徑的話,則直接調用 this.ensureUrlabortensureUrl 這個函數我們之後會介紹。接着又根據 current.matchedroute.matched 執行了 resolveQueue 方法解析出三個隊列:
function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}
  1. 因爲 route.matched 是一個 RouteRecord 的數組,由於路徑是由 current 變向 route,那麼就遍歷對比 2 邊的 RouteRecord,找到一個不一樣的位置 i,那麼 next 中從 0iRouteRecord 是兩邊都一樣,則爲 updated 的部分;從 i 到最後的 RouteRecordnext 獨有的,爲 activated 的部分;而 current 中從 i 到最後的 RouteRecord 則沒有了,爲 deactivated 的部分。拿到 updatedactivateddeactivated 三個 ReouteRecord 數組後,接下來就是路徑變換後的一個重要部分,執行一系列的鉤子函數。

  2. 導航守衛,官方的說法叫導航守衛,實際上就是發生在路由路徑切換的時候,執行的一系列鉤子函數。我們先從整體上看一下這些鉤子函數執行的邏輯,首先構造一個隊列 queue,它實際上是一個數組;然後再定義一個迭代器函數 iterator;最後再執行 runQueue 方法來執行這個隊列。我們先來看一下 runQueue 的定義,在 src/util/async.js 中:

export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => { 
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}
  1. 這是一個非常經典的異步函數隊列化執行的模式, queue 是一個 NavigationGuard 類型的數組,我們定義了 step 函數,每次根據 indexqueue 中取一個 guard,然後執行 fn 函數,並且把 guard 作爲參數傳入,第二個參數是一個函數,當這個函數執行的時候再遞歸執行 step 函數,前進到下一個,注意這裏的 fn 就是我們剛纔的 iterator 函數,那麼我們再回到 iterator 函數的定義:
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)
  }
}
  1. iterator 函數邏輯很簡單,它就是去執行每一個 導航守衛 hook,並傳入 routecurrent 和匿名函數,這些參數對應文檔中的 tofromnext,當執行了匿名函數,會根據一些條件執行 abortnext,只有執行 next 的時候,纔會前進到下一個導航守衛鉤子函數中,這也就是爲什麼官方文檔會說只有執行 next 方法來 resolve 這個鉤子函數。那麼最後我們來看 queue 是怎麼構造的:
const queue: Array<?NavigationGuard> = [].concat(
  extractLeaveGuards(deactivated),
  this.router.beforeHooks,
  extractUpdateHooks(updated),
  activated.map(m => m.beforeEnter),
  resolveAsyncComponents(activated)
)
  1. 按照順序如下:
  • 在失活的組件裏調用離開守衛。
  • 調用全局的 beforeEach 守衛。
  • 在重用的組件裏調用 beforeRouteUpdate 守衛
  • 在激活的路由配置裏調用 beforeEnter
  • 解析異步路由組件。
  1. 接下來我們來分別介紹這 5 步的實現。

  2. 第一步是通過執行 extractLeaveGuards(deactivated),先來看一下 extractLeaveGuards 的定義:

function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

它內部調用了 extractGuards 的通用方法,可以從 RouteRecord 數組中提取各個階段的守衛:

function extractGuards (
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  const guards = flatMapComponents(records, (def, instance, match, key) => {
    const guard = extractGuard(def, name)
    if (guard) {
      return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  return flatten(reverse ? guards.reverse() : guards)
}

這裏用到了 flatMapComponents 方法去從 records 中獲取所有的導航,它的定義在 src/util/resolve-components.js 中:

export function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Function> {
  return flatten(matched.map(m => {
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}

export function flatten (arr: Array<any>): Array<any> {
  return Array.prototype.concat.apply([], arr)
}

flatMapComponents 的作用就是返回一個數組,數組的元素是從 matched 裏獲取到所有組件的 key,然後返回 fn 函數執行的結果,flatten 作用是把二維數組拍平成一維數組。

那麼對於 extractGuardsflatMapComponents 的調用,執行每個 fn 的時候,通過 extractGuard(def, name) 獲取到組件中對應 name 的導航守衛:

function extractGuard (
  def: Object | Function,
  key: string
): NavigationGuard | Array<NavigationGuard> {
  if (typeof def !== 'function') {
    def = _Vue.extend(def)
  }
  return def.options[key]
}

獲取到 guard 後,還會調用 bind 方法把組件的實例 instance 作爲函數執行的上下文綁定到 guard 上,bind 方法的對應的是 bindGuard

function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
  if (instance) {
    return function boundRouteGuard () {
      return guard.apply(instance, arguments)
    }
  }
}

那麼對於 extractLeaveGuards(deactivated) 而言,獲取到的就是所有失活組件中定義的 beforeRouteLeave 鉤子函數。

  1. 第二步是 this.router.beforeHooks,在我們的 VueRouter 類中定義了 beforeEach 方法,在 src/index.js 中:
beforeEach (fn: Function): Function {
  return registerHook(this.beforeHooks, fn)
}

function registerHook (list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

當用戶使用 router.beforeEach 註冊了一個全局守衛,就會往 router.beforeHooks 添加一個鉤子函數,這樣 this.router.beforeHooks 獲取的就是用戶註冊的全局 beforeEach 守衛。

  1. 第三步執行了 extractUpdateHooks(updated),來看一下 extractUpdateHooks 的定義:
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

extractLeaveGuards(deactivated) 類似,extractUpdateHooks(updated) 獲取到的就是所有重用的組件中定義的 beforeRouteUpdate 鉤子函數。

  1. 第四步是執行 activated.map(m => m.beforeEnter),獲取的是在激活的路由配置中定義的 beforeEnter 函數。

  2. 第五步是執行 resolveAsyncComponents(activated) 解析異步組件,先來看一下 resolveAsyncComponents 的定義,在 src/util/resolve-components.js 中:

export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
  return (to, from, next) => {
    let hasAsync = false
    let pending = 0
    let error = null

    flatMapComponents(matched, (def, _, match, key) => {
      if (typeof def === 'function' && def.cid === undefined) {
        hasAsync = true
        pending++

        const resolve = once(resolvedDef => {
          if (isESModule(resolvedDef)) {
            resolvedDef = resolvedDef.default
          }
          def.resolved = typeof resolvedDef === 'function'
            ? resolvedDef
            : _Vue.extend(resolvedDef)
          match.components[key] = resolvedDef
          pending--
          if (pending <= 0) {
            next()
          }
        })

        const reject = once(reason => {
          const msg = `Failed to resolve async component ${key}: ${reason}`
          process.env.NODE_ENV !== 'production' && warn(false, msg)
          if (!error) {
            error = isError(reason)
              ? reason
              : new Error(msg)
            next(error)
          }
        })

        let res
        try {
          res = def(resolve, reject)
        } catch (e) {
          reject(e)
        }
        if (res) {
          if (typeof res.then === 'function') {
            res.then(resolve, reject)
          } else {
            const comp = res.component
            if (comp && typeof comp.then === 'function') {
              comp.then(resolve, reject)
            }
          }
        }
      }
    })

    if (!hasAsync) next()
  }
}
  1. resolveAsyncComponents 返回的是一個導航守衛函數,有標準的 tofromnext 參數。它的內部實現很簡單,利用了 flatMapComponents 方法從 matched 中獲取到每個組件的定義,判斷如果是異步組件,則執行異步組件加載邏輯,這塊和我們之前分析 Vue 加載異步組件很類似,加載成功後會執行 match.components[key] = resolvedDef 把解析好的異步組件放到對應的 components 上,並且執行 next 函數。

  2. 這樣在 resolveAsyncComponents(activated) 解析完所有激活的異步組件後,我們就可以拿到這一次所有激活的組件。這樣我們在做完這五步後又做了一些事情:

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() })
      })
    }
  })
})
  • 在被激活的組件裏調用 beforeRouteEnter

  • 調用全局的 beforeResolve 守衛。

  • 調用全局的 afterEach 鉤子。

  1. 對於在被激活的組件裏調用 beforeRouteEnter的這些相關的邏輯:
const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)

function extractEnterGuards (
  activated: Array<RouteRecord>,
  cbs: Array<Function>,
  isValid: () => boolean
): Array<?Function> {
  return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
    return bindEnterGuard(guard, match, key, cbs, isValid)
  })
}

function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string,
  cbs: Array<Function>,
  isValid: () => boolean
): NavigationGuard {
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      next(cb)
      if (typeof cb === 'function') {
        cbs.push(() => {
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}

function poll (
  cb: any,
  instances: Object,
  key: string,
  isValid: () => boolean
) {
  if (instances[key]) {
    cb(instances[key])
  } else if (isValid()) {
    setTimeout(() => {
      poll(cb, instances, key, isValid)
    }, 16)
  }
}
  1. extractEnterGuards 函數的實現也是利用了 extractGuards 方法提取組件中的 beforeRouteEnter 導航鉤子函數,和之前不同的是 bind 方法的不同。文檔中特意強調了 beforeRouteEnter 鉤子函數中是拿不到組件實例的,因爲當守衛執行前,組件實例還沒被創建,但是我們可以通過傳一個回調給 next 來訪問組件實例。在導航被確認的時候執行回調,並且把組件實例作爲回調方法的參數:
beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通過 `vm` 訪問組件實例
  })
}

來看一下這是怎麼實現的。

  1. bindEnterGuard 函數中,返回的是 routeEnterGuard 函數,所以在執行 iterator 中的 hook 函數的時候,就相當於執行 routeEnterGuard 函數,那麼就會執行我們定義的導航守衛 guard 函數,並且當這個回調函數執行的時候,首先執行 next 函數 rersolve 當前導航鉤子,然後把回調函數的參數,它也是一個回調函數用 cbs 收集起來,其實就是收集到外面定義的 postEnterCbs 中,然後在最後會執行:
if (this.router.app) {
  this.router.app.$nextTick(() => {
    postEnterCbs.forEach(cb => { cb() })
  })
}
  1. 在根路由組件重新渲染後,遍歷 postEnterCbs 執行回調,每一個回調執行的時候,其實是執行 poll(cb, match.instances, key, isValid) 方法,因爲考慮到一些了路由組件被套 transition 組件在一些緩動模式下不一定能拿到實例,所以用一個輪詢方法不斷去判斷,直到能獲取到組件實例,再去調用 cb,並把組件實例作爲參數傳入,這就是我們在回調函數中能拿到組件實例的原因。

  2. 第七步是獲取 this.router.resolveHooks,這個和
    this.router.beforeHooks 的獲取類似,在我們的 VueRouter 類中定義了 beforeResolve 方法:

beforeResolve (fn: Function): Function {
  return registerHook(this.resolveHooks, fn)
}

當用戶使用 router.beforeResolve 註冊了一個全局守衛,就會往 router.resolveHooks 添加一個鉤子函數,這樣 this.router.resolveHooks 獲取的就是用戶註冊的全局 beforeResolve 守衛。

  1. 第八步是在最後執行了 onComplete(route) 後,會執行 this.updateRoute(route) 方法:
updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}

同樣在我們的 VueRouter 類中定義了 afterEach 方法:

afterEach (fn: Function): Function {
  return registerHook(this.afterHooks, fn)
}
  1. 當用戶使用 router.afterEach 註冊了一個全局守衛,就會往 router.afterHooks 添加一個鉤子函數,這樣 this.router.afterHooks 獲取的就是用戶註冊的全局 afterHooks 守衛。所有導航守衛的執行分析完畢了,我們知道路由切換除了執行這些鉤子函數,從表象上有兩個地方會發生變化,一個是 url 發生變化,一個是組件發生變化。接下來我們分別介紹這兩塊的實現原理。

  2. url,當我們點擊 router-link 的時候,實際上最終會執行 router.push,如下:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)
}
  1. this.history.push 函數,這個函數是子類實現的,不同模式下該函數的實現略有不同,我們來看一下平時使用比較多的 hash 模式該函數的實現,在 src/history/hash.js 中:
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)
}

push 函數會先執行 this.transitionTo 做路徑切換,在切換完成的回調函數中,執行 pushHash 函數:

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

supportsPushState 的定義在 src/util/push-state.js 中:

export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})()

如果支持的話,則獲取當前完整的 url,執行 pushState 方法:

export function pushState (url?: string, replace?: boolean) {
  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)
  }
}
  1. pushState 會調用瀏覽器原生的 historypushState 接口或者 replaceState 接口,更新瀏覽器的 url 地址,並把當前 url 壓入歷史棧中。然後在 history 的初始化中,會設置一個監聽器,監聽歷史棧的變化:
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)
      }
    })
  })
}
  1. 當點擊瀏覽器返回按鈕的時候,如果已經有 url 被壓入歷史棧,則會觸發 popstate 事件,然後拿到當前要跳轉的 hash,執行 transtionTo 方法做一次路徑轉換。在使用 Vue-Router 開發項目的時候,打開調試頁面 http://localhost:8080 後會自動把 url 修改爲 http://localhost:8080/#/,這是怎麼做到呢?原來在實例化 HashHistory 的時候,構造函數會執行 ensureSlash() 方法:
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : href.slice(index + 1)
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

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

這個時候 path 爲空,所以執行 replaceHash('/' + path),然後內部會執行一次 getUrl,計算出來的新的 urlhttp://localhost:8080/#/,最終會執行 pushState(url, true),這就是 url 會改變的原因。

  1. 組件,路由最終的渲染離不開組件,Vue-Router 內置了 <router-view> 組件,它的定義在 src/components/view.js 中,如下所示:
export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
   
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    if (inactive) {
      return h(cache[name], data, children)
    }

    const matched = route.matched[depth]
    if (!matched) {
      cache[name] = null
      return h()
    }

    const component = cache[name] = matched.components[name]
   
    data.registerRouteInstance = (vm, val) => {     
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
    
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }

    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      propsToPass = data.props = extend({}, propsToPass)
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }

    return h(component, data, children)
  }
}
  1. <router-view> 是一個 functional 組件,它的渲染也是依賴 render 函數,那麼 <router-view> 具體應該渲染什麼組件呢,首先獲取當前的路徑:
const route = parent.$route

我們之前分析過,在 src/install.js 中,我們給 Vue 的原型上定義了 $route

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

然後在 VueRouter 的實例執行 router.init 方法的時候,會執行如下邏輯,定義在 src/index.js 中:

history.listen(route => {
  this.apps.forEach((app) => {
    app._route = route
  })
})

history.listen 方法定義在 src/history/base.js 中:

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

然後在 updateRoute 的時候執行 this.cb

updateRoute (route: Route) {
  //. ..
  this.current = route
  this.cb && this.cb(route)
  // ...
}
  1. 我們執行 transitionTo 方法最後執行 updateRoute 的時候會執行回調,然後會更新 this.apps 保存的組件實例的 _route 值,this.apps 數組保存的實例的特點都是在初始化的時候傳入了 router 配置項,一般的場景數組只會保存根 Vue 實例,因爲我們是在 new Vue 傳入了 router 實例。$route 是定義在 Vue.prototype 上。每個組件實例訪問 $route 屬性,就是訪問根實例的 _route,也就是當前的路由線路。<router-view> 是支持嵌套的,回到 render 函數,其中定義了 depth 的概念,它表示 <router-view> 嵌套的深度。每個 <router-view> 在渲染的時候,執行如下邏輯:
data.routerView = true
// ...
while (parent && parent._routerRoot !== parent) {
  if (parent.$vnode && parent.$vnode.data.routerView) {
    depth++
  }
  if (parent._inactive) {
    inactive = true
  }
  parent = parent.$parent
}

const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]
  1. parent._routerRoot 表示的是根 Vue 實例,那麼這個循環就是從當前的 <router-view> 的父節點向上找,一直找到根 Vue 實例,在這個過程,如果碰到了父節點也是 <router-view> 的時候,說明 <router-view> 有嵌套的情況,depth++。遍歷完成後,根據當前線路匹配的路徑和 depth 找到對應的 RouteRecord,進而找到該渲染的組件。除了找到了應該渲染的組件,還定義了一個註冊路由實例的方法:
data.registerRouteInstance = (vm, val) => {     
  const current = matched.instances[name]
  if (
    (val && current !== vm) ||
    (!val && current === vm)
  ) {
    matched.instances[name] = val
  }
}

  1. vnodedata 定義了 registerRouteInstance 方法,在 src/install.js 中,我們會調用該方法去註冊路由的實例:
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 () {
    // ...
    registerInstance(this, this)
  },
  destroyed () {
    registerInstance(this)
  }
})
  1. 在混入的 beforeCreate 鉤子函數中,會執行 registerInstance 方法,進而執行 render 函數中定義的 registerRouteInstance 方法,從而給 matched.instances[name] 賦值當前組件的 vm 實例。render 函數的最後根據 component 渲染出對應的組件 vonde
return h(component, data, children)
  1. 那麼當我們執行 transitionTo 來更改路由線路後,組件是如何重新渲染的呢?在我們混入的 beforeCreate 鉤子函數中有這麼一段邏輯:
Vue.mixin({
  beforeCreate () {
    if (isDef(this.$options.router)) {
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    }
    // ...
  }
})
  1. 由於我們把根 Vue 實例的 _route 屬性定義成響應式的,我們在每個 <router-view> 執行 render 函數的時候,都會訪問 parent.$route,如我們之前分析會訪問 this._routerRoot._route,觸發了它的 getter,相當於 <router-view> 對它有依賴,然後再執行完 transitionTo 後,修改 app._route 的時候,又觸發了setter,因此會通知 <router-view> 的渲染 watcher 更新,重新渲染組件。

  2. Vue-Router 還內置了另一個組件 <router-link>
    它支持用戶在具有路由功能的應用中(點擊)導航。 通過 to 屬性指定目標地址,默認渲染成帶有正確鏈接的 <a> 標籤,可以通過配置 tag 屬性生成別的標籤。另外,當目標路由成功激活時,鏈接元素自動設置一個表示激活的 CSS 類名。<router-link> 比起寫死的 <a href="..."> 會好一些,理由如下:

  • 無論是 HTML5 history 模式還是 hash 模式,它的表現行爲一致,所以,當你要切換路由模式,或者在 IE9 降級使用 hash 模式,無須作任何變動。

  • HTML5 history 模式下,router-link 會守衛點擊事件,讓瀏覽器不再重新加載頁面。

  • 當你在 HTML5 history 模式下使用 base 選項之後,所有的 to屬性都不需要寫(基路徑)了。

  1. 那麼接下來我們就來分析它的實現,它的定義在 src/components/link.js 中:
export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(this.to, current, this.append)

    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    const activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass
    const activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }

    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }

    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      const a = findAnchor(this.$slots.default)
      if (a) {
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        data.on = on
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
}

<router-link> 標籤的渲染也是基於 render 函數,它首先做了路由解析:

const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)

router.resolveVueRouter 的實例方法,它的定義在 src/index.js 中:

resolve (
  to: RawLocation,
  current?: Route,
  append?: boolean
): {
  location: Location,
  route: Route,
  href: string,
  normalizedTo: Location,
  resolved: Route
} {
  const location = normalizeLocation(
    to,
    current || this.history.current,
    append,
    this
  )
  const route = this.match(location, current)
  const fullPath = route.redirectedFrom || route.fullPath
  const base = this.history.base
  const href = createHref(base, fullPath, this.mode)
  return {
    location,
    route,
    href,
    normalizedTo: location,
    resolved: route
  }
}

function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}
  1. 它先規範生成目標 location,再根據 locationmatch 通過 this.match 方法計算生成目標路徑 route,然後再根據 basefullPaththis.mode 通過 createHref 方法計算出最終跳轉的 href。解析完 router 獲得目標 locationroutehref 後,接下來對 exactActiveClassactiveClass 做處理,當配置 exacttrue 的時候,只有當目標路徑和當前路徑完全匹配的時候,會添加 exactActiveClass;而當目標路徑包含當前路徑的時候,會添加 activeClass。接着創建了一個守衛函數 :
const handler = e => {
  if (guardEvent(e)) {
    if (this.replace) {
      router.replace(location)
    } else {
      router.push(location)
    }
  }
}

function guardEvent (e) {
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  if (e.defaultPrevented) return
  if (e.button !== undefined && e.button !== 0) return 
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}

const on = { click: guardEvent }
  if (Array.isArray(this.event)) {
    this.event.forEach(e => { on[e] = handler })
  } else {
    on[this.event] = handler
  }
  1. 最終會監聽點擊事件或者其它可以通過 prop 傳入的事件類型,執行 hanlder 函數,最終執行 router.push 或者 router.replace 函數,它們的定義在 src/index.js 中:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
 this.history.replace(location, onComplete, onAbort)
}

實際上就是執行了 historypushreplace 方法做路由跳轉。

最後判斷當前 tag 是否是 <a> 標籤,<router-link> 默認會渲染成 <a> 標籤,當然我們也可以修改 tagprop 渲染成其他節點,這種情況下會嘗試找它子元素的 <a> 標籤,如果有則把事件綁定到 <a> 標籤上並添加 href 屬性,否則綁定到外層元素本身。

  1. 總結:路由的 transitionTo 的主體過程分析完畢了,其他一些分支比如重定向、別名、滾動行爲等可以自行再去分析。路徑變化是路由中最重要的功能,我們要記住以下內容:路由始終會維護當前的線路,路由切換的時候會把當前線路切換到目標線路,切換過程中會執行一系列的導航守衛鉤子函數,會更改 url,同樣也會渲染對應的組件,切換完畢後會把目標線路更新替換當前線路,這樣就會作爲下一次的路徑切換的依據。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章