vue-router源码解析

412 阅读9分钟

date: 2021-11-29 19:03:44
tags: vue-router 源码解析
author: coder@mc

定场诗

人人尽说清闲好,谁肯逢闲闲此身。

不是逢闲闲不得,清闲岂是等闲人。

版本说明

此博客主要是针对 vue-router@3.5.2做一次源码解析,主要研究以下几点:

  1. install函数;
  2. 初始化过程,即VueRouter构造函数的实现;
  3. 路由跳转流程;
  4. <router-view>router-link

思维导图

vue-router

Demo

// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
import VueRouter from 'vue-router';

Vue.use(VueRouter);

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
  routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
  router
}).$mount('#app')

上面的代码是vue-router官方例子,想要了解源码,得从两方面入手,一个是作为导入的插件被Vue.use注册时,instll函数具体实现过程;另一个是new VueRouter() 时具体做了哪些事,下面我们先看第一个:

一、install函数

我们都知道Vue.use(VueRouter)注册时必须要存在install函数,而import VueRouter from 'vue-router'导入时的入口文件是src/index.js,打开文件后会有一句:

VueRouter.install = install

可以看到install函数赋值给VueRouter,所以可以被vue.use()作为插件安装在Vue中,install函数源码位于src/install.js

import View from './components/view'
import Link from './components/link'

export let _Vue

/**
 * 1. 标记 vue-router 已经被注册过了
 * 2. 为了每个组件都能使用 router, 则通过 Vue.mixin 来创建公用数据
 * 3. Vue.prototype.defineProperty 劫持,使得能通过 this.$router 或者 this.$route 访问
 * 4. 定义 router-view 和 router-link 组件
 * 
 * @param {*} Vue 
 */
export function install (Vue) {
  // 表示 vue-router 已安装,防止重复安装
  if (install.installed && _Vue === Vue) return
  install.installed = true

  // 把 Vue 存起来并 export 供其它文件使用
  _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)
    }
  }

  // 注册一个全局的 mixin
  Vue.mixin({
    // 生命周期 beforeCerate 钩子函数
    beforeCreate () {
      // 初始化
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        // 调用 router.init()
        this._router.init(this)
        // 把 _router 变为响应式
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 如果已经初始化,继承父组件的 _routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 注册实例,实际上是挂载 <router-view></router-view>
      registerInstance(this, this)
    },
    destroyed () {
      // 离开时卸载
      registerInstance(this)
    }
  })

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

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

  // <router-view> 和 <router-link> 组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  // 利用 Vue 的合并策略新增几个相关的生命周期
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

该函数做了如下几件事:

  1. 通过给install函数上赋值静态属性installed,标记 vue-router 已经被注册过了,防止重复注册;
  2. 用了全局混入,使每个组件都能使用router,具体通过给每个组件添加_routerRoot属性,这个属性的_router其实就是new VueRouter()实例化后的对象,实例化过程看第二部分;
  3. 通过Object.defineProperty方法劫持组件实例上$router$route属性,使得每个组件可以通过this.$routerthis.$route访问到router实例和router.history.current
  4. 注册<router-view> <router-link> 组件;
  5. 利用 Vue 的合并策略新增几个相关的生命周期,包括beforeRouteEnterbeforeRouteLeavebeforeRouteUpdatecreated

二、初始化过程

export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    // 根 Vue 实例
    this.app = null
    // 存在多实例的话,则保存
    this.apps = []
    // 传入的配置
    this.options = options
    // 存放已注册的导航守卫
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []

    // 创建 matcher, 返回 match, addRoute, getRoutes, addRoutes 四个函数
    this.matcher = createMatcher(options.routes || [], this)

    // 默认是 hash 模式
    let mode = options.mode || 'hash'

    // 如果使用了 history 模式,但不支持 pushState 也需回退到 hash
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }

    // 非浏览器环境(SSR),则使用 abstract
    if (!inBrowser) {
      mode = 'abstract'
    }

    this.mode = 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}`)
        }
    }
  }
  ...
}

可以看到在new VueRouter(options)时,大致可以分为三部分:

第一部分:初始化一些内置属性,比如options配置,导航守卫数组等;

第二部分:创建 matcher,返回一个对象,里面有4个方法match, addRoute, getRoutes, addRoutes

第三部分:根据mode构建不同的的history

其中第一部分初始化一些内置属性没什么可讲解的,主要看第二部分和第三部分,先看创建matcher

1. 创建macther

// 创建 matcher, 返回 match, addRoute, getRoutes, addRoutes 四个函数
this.matcher = createMatcher(options.routes || [], this)

从字面意思去解读:createMatcher是个函数,并传入两个参数routes ||[] thiscreateMatcher函数的源码位于src/create-matcher.js

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // pathList: path 的集合 [path1, path2, ...],
  // pathMap: path作为键的映射对象 {path1: record},
  // 当 route 里面有 name 则,将 name 作为键, record 作为值存储到 nameMap 中
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes (routes) { ... }

  function addRoute (parentOrRoute, route) { ... }

  function match (raw, currentRoute, redirectedFrom) {...}
  
  function getRoutes () {...}
  ...
  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

上面代码做了两件事,一是通过调用createRouteMap(routes)函数,返回了一个对象并解构里面的属性定义了三个变量pathListpathMapnameMap;二是返回了四个函数。第二步暂时还不看,先看createRouteMap函数,该函数源码位于src/create-route-map.js

/**
 * 1. 处理配置项 routes 里面的每一项,
 *      将 path 放到 pathList 数组中,将 path 作为 pathMap 的键,record({path: 'xx', name: '', ...}) 做值
 * 2. 确保 pathList 中通配符在末尾
 * 3. 非嵌套路由 path !== '' 的情况下首字符没有包含斜杠字符,会报警告
 * 4. 返回 pathList,pathMap,nameMap
 * @param {*} routes 路由配置
 * @param {*} oldPathList [path1. path2, ...]
 * @param {*} oldPathMap [{path1: {path: path1, ...}}]
 * @param {*} oldNameMap 
 * @param {*} parentRoute 
 */
export function createRouteMap(
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>,
  parentRoute?: RouteRecord
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  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)

  // 遍历每项 route, 生成三张表
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })

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

  // 警告:非嵌套路由首字符包含斜杠字符
  if (process.env.NODE_ENV === 'development') {
    // warn if routes do not include leading slashes
    const found = pathList
      // check for missing leading slash
      .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')

    if (found.length > 0) {
      const pathNames = found.map(path => `- ${path}`).join('\n')
      // 非嵌套路由必须包含斜杠字符
      warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
    }
  }

  return {
    pathList,
    pathMap,
    nameMap
  }
}

createRouteMap函数可以分为四部分去阅读:

  1. 定义了三个变量,通过遍历传入的routes,调用addRouteRecord函数处理配置项 routes 里面的每一项,将 path 放到 pathList 数组中,将 path 作为 pathMap 的键,record({path: 'xx', name: '', ...}) 做值;
  2. 确保通配符始终在末尾;
  3. 非嵌套路由 path !== '' 的情况下首字符没有包含斜杠字符,会报警告;
  4. 返回 pathListpathMapnameMap三张表。

需要关注的是addRouteRecord函数怎样处理routes中的每项,源码位于当前文件里面的addRouteRecord函数:

/**
 * 1. 检查是否有配置 pathToRegexpOptions 选项,这个属性值是路由高级匹配模式 (path-to-regex) 的参数
 * 2. 调用 normalizePath 将 path 标准化,这里会将子路由的 path 和 父路由的 path 拼接在一起
 * 3. 处理 caseSensitive 参数,这是 path-to-regexp 中的参数
 * 4. 声明一个 RouteRecord 
 * 5. 该路由存在子路由,递归调用 addRouteRecord 添加路由记录
 * 6. 将 record 存入 pathList,将这条记录以 path 作为 key 存入 pathMap 
 * 7. 如果存在 alias,则用 alias 作为 path 再添加一条路由记录
 * 8. 如果存在 name,则用 name 作为 key 存入到 nameMap
 * @param {*} pathList 
 * @param {*} pathMap 
 * @param {*} nameMap 
 * @param {*} route 
 * @param {*} parent 
 * @param {*} matchAs 
 */
function addRouteRecord(
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  const { path, name } = route
  
  // 校验 path,route 的 component 属性
  if (process.env.NODE_ENV !== 'production') {
    // path 是必须传入的
    assert(path != null, `"path" is required in a route configuration.`)
    // component 不能是字符串,必须是组件
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(
        path || name
      )} cannot be a ` + `string id. Use an actual component instead.`
    )

    // path 是 ASCLL 字符
    warn(
      // eslint-disable-next-line no-control-regex
      !/[^\u0000-\u007F]+/.test(path),
      `Route with path "${path}" contains unencoded characters, make sure ` +
      `your path is correctly encoded before passing it to the router. Use ` +
      `encodeURI to encode static segments of your path.`
    )
  }

  // 编译正则的选项
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}

  // 调用 normalizePath 将 path 标准化,
  // 如果存在父路由,这里会将子路由的 path 和 父路由的 path 拼接在一起
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  // 处理 caseSensitive 参数,这是 path-to-regexp 中的参数
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  // 声明一个 RouteRecord 
  const record: RouteRecord = {
    path: normalizedPath,
    // 用于匹配该路由的正则
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    // 该路由对应的组件,这里与 <router-view> 的 name 有关联
    components: route.components || { default: route.component },
    // 路由别名
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    instances: {},
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,   // 导航守卫
    meta: route.meta || {},   // 元信息
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }

  // 该路由存在子路由,递归调用 addRouteRecord 添加路由记录
  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,递归执行 addRouteRecord
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 将 record 存入 pathList,将这条记录以 path 作为 key 存入 pathMap 
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  // 如果存在 alias,则用 alias 作为 path 再添加一条路由记录
  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]
      // alias 不能和 path 重名
      if (process.env.NODE_ENV !== 'production' && alias === path) {
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }

      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    }
  }

  // 如果存在 name,则用 name 作为 key 存入到 nameMap
  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}" }`
      )
    }
  }
}

代码很长,前面经过一系列处理生成一个RouteRecord 变量,将 RouteRecord.path 存入 pathList,将这条记录以 path 作为 key存入 pathMap ;如果存在 name,则用 name 作为 key 存入到 nameMap;如果routes里存在children属性,则递归遍历该函数。

总结:createMatcher函数其实就是里面生成了三张表(注意:三张表作为全局变量,供里面方法调用,并未作为私有属性),并定义了一些方法作为返回值,放到router实例的matcher属性上。

2. 构建history

根据不同的mode构建不同的history属性,这里以modehash为例:

this.history = new HashHistory(this, options.base, this.fallback)

可以看到,通过new HashHistory() 实例化了一个hsitory对象,并传入三个参数thisoptions.basethis.fallback,前两个参数可以理解,this.fallback是什么呢?答案如下:

// 如果使用了 history 模式,但不支持 pushState 也需回退到 hash
this.fallback = 
    mode === 'history' && !supportsPushState && options.fallback !== false

this.fallback是一个布尔值,如果使用了 history 模式,但不支持 pushState,并且options.fallback也不为false时才为true。接下来具体看HashHistory构造函数初始化过程,该源码位于src/history/hash.js

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // 不支持 history 模式,支持 hash 模式,检查是否因为回退而使用 hash 模式,如果是的话则调用 checkFallback 检查它的返回值
    // 如果为 true,则不调用 ensureSlash()
    if (fallback && checkFallback(this.base)) {
      return
    }

    // 判断首字符是否存在 /,如果不是则要重定向以 / 开头的 URL
    ensureSlash()
  }
  ...
}

HashHistory构造函数做了三件事,一是通过super关键字调用父类构造函数History;二是,检查传入的fallback,如果为true则直接返回;三是判断首字符是否存在 /,如果不是则要重定向以 / 开头的 URL。父类构造函数History的源码位于src/history/base.js

export class History {
  constructor (router: Router, base: ?string) {
    // vueRouter 实例
    this.router = router
    // 序列化后的 base(首字母 "/" 最后一个字符不是 "/")
    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 = []
    this.listeners = []
  }
  ...
}

ensureSlash方法源码如下:

// 如果首字符是 / 返回 true,否则返回 false
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

三、路由跳转

路由跳转这里准备分为两部分了解:

第一部分:url 输入;

第二部分:TransitionTo方法;

第三部分:通过使用this.$router.push(xx)this.$router.replace(xx)跳转都干了什么。

1. url 输入

输入网址时,展示的是routes里对应的组件,跳转组件必然会经历生命周期函数。回顾一下,在安装插件时,会混入beforeCreate生命周期函数,每个组件都会走这一生命周期函数,里面有一句:

// 调用 router.init()
this._router.init(this)

可以看到会调用routerinit方法,并将当前组件作为参数传递过去,initVueRouter的原型方法,位于src/index.js

  /**
   * 1. 开发环境中确保使用 Vue.use 安装插件
   * 2. 将 vue 实例存入到 apps 中
   * 3. 当前实例销毁时需要在 apps 中移除
   * 4. 根据当前路由做路由跳转
   * 5. 监听路由变化给实例设置 _route 属性,以便通过 this.$route 获取
   * @param {*} app Vue 实例
   */
  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.`
      )
    
    // 保存当前 app 实例
    this.apps.push(app)

    // 当前 app 销毁时需要在 apps 中移除
    app.$once('hook:destroyed', () => {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      // ensure we still have a main app or null if no apps
      // we do not release the router so it can be reused
      if (this.app === app) this.app = this.apps[0] || null

      if (!this.app) this.history.teardown()
    })

    // 防止重复调用
    if (this.app) {
      return
    }

    // 根 Vue 实例
    this.app = app

    // 当前的 history,由之前的 new Router 时根据不同 mode 来创建
    const history = this.history

    // 根据当前路由位置做路由跳转
    if (history instanceof HTML5History || history instanceof HashHistory) {
      const handleInitialScroll = routeOrError => {
        const from = history.current
        const expectScroll = this.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll

        if (supportsScroll && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from, false)
        }
      }

      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      
      // 初始化切换路由的方法
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

    // 监听路由变化,在所有 app 实例中设置当前路由
    // 所以可以通过 this.$route 拿到当前路由
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }

着重需要关注的是路由跳转方法history.transitionTo()方法,这是输入网址时路由跳转的方法。可以看下传递的第一个参数history.getCurrentLocation()

getCurrentLocation () {
  return getHash()
}
/**
 * 截取 hash 后面的字符,例如:basePath/#/path => /path
 */
export function getHash (): string {
  let href = window.location.href
  const index = href.indexOf('#')
  
  if (index < 0) return ''

  href = href.slice(index + 1)

  return href
}

其实就是当前路由。

2. transitionTo方法

由于init方法和push/replace方法里都调用了该方法,所以放在同层讲解,该方法位于src/history/base.js

  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    
    let route
    // 这里要 try 一下是因为 match 方法内部会在有 redirect 属性时调用它
    // 但用户提供的 redirect 方法可能会报错,所以这里需要捕获到错误的回调方法
    try {
      route = this.router.match(location, this.current)
    } catch (e) {
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      // 依然要抛出异常,让用户得知
      throw e
    }
    
    // 记录之前的路由,后面会用到
    const prev = this.current
    // 切换路由的真正方法
    this.confirmTransition(
      // 传入准备切换的路由
      route,
      // 切换之后的回调
      () => {
      	...
      },
      // 发生错误的回调
      err => {
 		...
      }
    )
  }

这一函数其实就是做了两件事,一是通过调用this.router.match(location, this.current)方法来生成,route变量;二是调用真正的切换路由方法confirmTransition

2.1 match方法

// 实际上是调用 matcher 的 match 方法
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
  return this.matcher.match(raw, current, redirectedFrom)
}

可以看到实际上是调用 matchermatch 方法,还记得matcher.mactch方法在哪吗?上面提到过match方法,该方法位于src/create-matcher.js

  /**
   * 1. 将待切换的路由转换成一个标准的 Location 对象
   * 2. 判断是否存在 name 属性,若存在,则继承父路由的 params 属性,并将 path 和 params 合并为 URL 创建路由对象
   * 3. name 不存在,判断是否存在 path 属性,遍历 pathList 路由表,找到对应的记录,并创建路由对象,否则,创建一个空路由对象
   * 4. 上面两种情况都不存在,则创建一个空路由对象
   * @param {*} raw 待切换的路由
   * @param {*} currentRoute 当前路由
   * @param {*} redirectedFrom 使用重定向方式切换时才会传入
   */
  function match (
    // 待切换路由,取值为 字符串 或 Location对象
    raw: RawLocation,    
    // 当前的路由
    currentRoute?: Route,
    // 使用重定向方式切换时才会传入
    redirectedFrom?: Location
  ): Route {
    // 将待切换的路由转换成一个标准的 Location 对象
    // 比如:path 补全,合并 params
    const location = normalizeLocation(raw, currentRoute, false, router)
    // 待切换路由的 name
    const { name } = location

    if (name) {
      // 有 name 的属性,直接通过 nameMap 获取,根本无需遍历,非常高效
      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)

      // 获取可以从父路由中继承的 param 参数
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

      // params 需要为对象
      if (typeof location.params !== 'object') {
        location.params = {}
      }

      // 继承父路由的 param 参数
      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]
          }
        }
      }

      // 将 path 和 params 合并为 URL
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      // 创建路由记录
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
      // 如果是通过 path 跳转,则需要通过遍历 pathList 匹配对应的路由
      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)
  }

该方法最终都会执行_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)
  }

该方法会判断是否存在重定向属性以及别名属性,都没存在则走createRoute方法,该方法位于src/util/route.js

/**
 * 创建 route 对象,不可修改的对象
 * @param {*} record 
 * @param {*} location 
 * @param {*} redirectedFrom 
 * @param {*} router 
 */
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)
}

最终返回一个route不可修改对象,包含namepathmetahash等属性。

2.2 confirmTransition方法

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
   ...
}
  1. 获取到 跳转前的路由(from) 与 待跳转的路由(to)

    // 跳转前的路由 (from)
    const current = this.current
    // 待跳转的路由 (to)
    this.pending = route
    
  2. 错误的回调

     // 错误的回调
     const abort = err => {
       if (!isNavigationFailure(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)
    }  
    
  3. 判断是否是相同的路由

    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1 
    
    // 判断是否是相同路径,例如:同一路由点击多次
    if (
      isSameRoute(route, current) &&
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
       // 依旧切换
       this.ensureURL()
       // 报一个重复导航的错误
       return abort(createNavigationDuplicatedError(current, route))
    }
    
  4. 导航流程

    导航流程

    // 通过 from 和 to 的 matched 数组拿到新增,更新,销毁的部分,以便执行组件的生命周期
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
    
    // 一个队列,里面存放着各种组件的生命周期和导航守卫
    // 注意:这里只是完整的解析流程 2~6 步
    const queue: Array<?NavigationGuard> = [].concat(
      // 调用此次失活的部分组件的 beforeRouteLeave
      extractLeaveGuards(deactivated),
      // 调用全局的 before 钩子
      this.router.beforeHooks,
      // 调用此次更新的部分组件的 beforeRouteUpdate
      extractUpdateHooks(updated),
      // 调用此次激活的路由配置 beforeEnter 钩子函数
      activated.map(m => m.beforeEnter),
      // 解析异步路由组件
      resolveAsyncComponents(activated)
    )
    
    // 迭代器,每次执行一个钩子,调用 next 才会执行下一个钩子函数
    const iterator = (hook: NavigationGuard, next) => {
     // 在当前导航还没有完成之前又有了一个新的导航
     // 比如,在等待导航守卫的过程中又调用了 router.push
     // 这时候需要报一个 cancel 的错误
     if (this.pending !== route) {
       return abort(createNavigationCancelledError(current, route))
      }
    
      // 执行当前的钩子,但用户传入的导航守卫有可能会出错,需要 try 一下
      try {
        // 这就是路由钩子函数的参数:to, from, next
        hook(route, current, (to: any) => {
          // 可以通过 next('/login') 这样的方式来重定向
          // 如果传入 false 则中断当前的导航,并将 URL 重置到from 路由对应的地址
          if (to === false) {
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
           } else if (isError(to)) {
             // 如果传入 next 的参数是一个 Error 实例,
             // 则导航会被终止且该错误会被传递给 router.onError() 注册过的回调
             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(createNavigationRedirectedError(current, route))
                // 判断切换类型
                if (typeof to === 'object' && to.replace) {
                  this.replace(to)
                } else {
                  this.push(to)
                }
              } else {
                // 不符合则跳转至 to
                next(to)
              }
            })
          } catch (e) {
            // 走错误的回调
            abort(e)
          }
        }
    
    // 执行队列
    // queue 就是上面 钩子函数 的队列
    // iterator 传入 to, from, next,只有执行 next 才进入下一项
    // cb 回调函数,当执行完整个队列后调用
    // 注意:这里嵌套执行了两次 runQueue, 这是因为前面构造的 queue 只是 vue-router 完整的导航解析流程的 2-6 步,接下来要执行的是 7-9 步
    runQueue(queue, iterator, () => {
      // 这时候异步组件已经解析完成
      // 下面是构造 beforeRouteEnter 和 beforeRouteResolve 守卫的队列
      const enterGuards = extractEnterGuards(activated)
      const queue = enterGuards.concat(this.router.resolveHooks)
    
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
         }
         this.pending = null
    
         // 这里是调用 transitionTo 传入的 onComplete 回调
         // 在这里会做一些更新路由、URL、调用 afterHooks、onReady 的回调
         onComplete(route)
         if (this.router.app) {
           // 下次更新 DOM 时触发 handleRouteEntered
           this.router.app.$nextTick(() => {
             handleRouteEntered(route)
            })
          }
       })
     })
    

    上述代码阐述了vue-router导航流程2-9步,首先将2-6步的得导航守卫存到queue中,通过runQueue函数,依次执行里面的回调函数。看看runQueue函数具体做了什么:

    /**
     * 步骤:
     *   从 0 开始顺序遍历 queue 中的每一项,在调用 fn 时作为第一个参数传入
     *   当使用者调用了第二个参数的回调时, 才进入下一项
     *   最后遍历完 queue 中的所有项后,调用 cb 回到参数
     * @param {*} queue 钩子函数队列
     * @param {*} fn 迭代器函数
     * @param {*} cb 迭代器函数执行完后的回调函数
     */
    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. 先将2-6步的导航守卫函数做包装存入到queue队列中,包装成(from, to, next) => {...}
    2. 定义迭代器方法;
    3. 通过runQueue函数,依次执行迭代器函数,实际上就是遍历queue队列中的方法,依次执行;
    4. 将7-9步的导航存放到queue队列中,然后同上,执行完导航守卫方法后,执行onComplete方法,即confirmTransition方法传入的第二个参数;
    5. 执行init中的history.setupListeners函数。

3. pushreplace方法

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

其实pushreplace方法实际上也是调用的transitionTo方法,该方法上述讲过,流程与上述一致。

四、<router-view><router-link>

1. <router-view>组件

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
    // 缓存路由视图,keepAlive 时会用到
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    
    ...

    // 这里渲染已经缓存的视图
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        // #2301
        // pass props
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        return h()
      }
    }

    // 拿到对应的视图组件
    const matched = route.matched[depth]
    const component = matched && matched.components[name]
    if (!matched || !component) {
      cache[name] = null
      return h()
    }

    // cache component
    cache[name] = { component }

	...
    // 渲染组件
    return h(component, data, children)
  }
}

可以看到router-view组件时一个函数组件,传入一个属性是name属性,通过$route.components拿到渲染的组件数据,并最后做缓存并渲染。

2. <router-link>组件

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    custom: Boolean,
    exact: Boolean,
    exactPath: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    ariaCurrentValue: {
      type: String,
      default: 'page'
    },
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    const router = this.$router
    const current = this.$route

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

    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    // Support global empty active class
    const activeClassFallback =
      globalActiveClass == null ? 'router-link-active' : globalActiveClass
    const exactActiveClassFallback =
      globalExactActiveClass == null
        ? 'router-link-exact-active'
        : globalExactActiveClass
    const activeClass =
      this.activeClass == null ? activeClassFallback : this.activeClass
    const exactActiveClass =
      this.exactActiveClass == null
        ? exactActiveClassFallback
        : this.exactActiveClass

    const compareTarget = route.redirectedFrom
      ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
      : route

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

    const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
    
    // 事件处理,不一定是 click,取决于用户传入的 event
    const handler = e => {
      if (guardEvent(e)) {
        // 使用不同的方式来切换路由
        if (this.replace) {
          router.replace(location, noop)
        } else {
          router.push(location, noop)
        }
      }
    }

    // 注册事件
    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 (this.tag === 'a') {
      data.on = on
      data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
     ...
    }
    
    return h(this.tag, data, this.$slots.default)
  }
}

该组件也是个函数组件,传递的参数在props里,将数据放在data对象上,包含attrson属性,然后通过h函数渲染,即Vue源码里的_createElement方法。

上述阐述只是大概流程,具体细节还需自己debug去看,欢迎指错,共同学习~

参考链接