Vue Router源码分析

摘要:最近项目中遇到了点Vue Router的问题,发现有些只是没理清楚,再次复习了下Vue Router的源码,记录下...

        Vue-Router的能力十分强大,它支持hash、history、abstract 3种路由方式,提供了<router-link>和<router-view>2种组件,还提供了简单的路由配置和一系列好用的 API。

        先来看一个最基本使用例子,学习源码可结合这个例子逐步调试,理解整个路由工作过程:

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 组件来导航. -->
    <!-- 通过传入 `to` 属性指定链接. -->
    <!--** <router-link> 默认会被渲染成一个 `<a>` 标签** -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'

**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({
  el: '#app',
  render(h) {
    return h(App)
  },
  router
})

        关于VueRouter,先从 Vue.use(VueRouter) 说起。

1. 路由注册

        Vue 从设计上就是一个渐进式JavaScript框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router就是官方维护的路由插件,在介绍它的注册实现之前,我们先来分析一下 Vue 通用的插件注册原理

1.1 Vue.use

        Vue提供了Vue.use的全局API来注册这些插件,Vue.js插件初始化函数的实现定义在 vue/src/core/global-api/use.js 中:

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {  // **Vue对象的use方法,用于注册插件**
    // 存储所有注册过的plugin,未定义过则初始化为空数组**
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {  // **保证插件只注册一次**
      return this
    }

    const args = toArray(arguments, 1)  // toArray函数将arguments对象转换为真正的数组,索引从1开始,跳过第一个参数(即plugin)
    args.unshift(this)  // install方法的第一个参数,存储Vue
    if (typeof plugin.install === 'function') {  // 判断plugin有没有定义install方法
      plugin.install.apply(plugin, args)  // 调用插件的install方法,将plugin对象作为上下文,并将args数组作为参数传递给install方法
    } else if (typeof plugin === 'function') {  // 插件本身就是一个函数,则直接调用
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)   // 已注册的插件添加到installedPlugins数组中,以便跟踪已安装的插件
    return this
  }
}

        上述方法中,Vue.use 接受一个plugin参数,并且维护了一个_installedPlugins数组,它存储所有注册过的plugin;接着又会判断 plugin 有没有定义install方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue;最后把plugin存储到 installedPlugins 中。 可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个install方法的第一个参数我们可以拿到Vue对象,这样的好处就是作为插件的编写方不需要再额外去import Vue(Vue的插件对Vue对象是有依赖的,但又不能去单独去import Vue,因为那样会增加包体积,所以就通过这种方式拿到Vue对象)。

1.2 路由安装

        Vue-Router的入口文件是src/index.js,其中定义了VueRouter类,也实现了install的静态方法:VueRouter.install = install,它的定义在 src/install.js 中。

export let _Vue   // _Vue变量,用于存储传入的Vue构造函数;export后可在源码的任何地方访问Vue
export function install (Vue) { // VueRouter的install的静态方法,用于安装Vue Router**
  if (install.installed && _Vue === Vue) return  // 避免重复安装。如果已安装且传入的Vue构造函数与之前保存的相同,则直接返回
  install.installed = true  // 已安装的标志位

  _Vue = Vue  // 保留传入的Vue**

  const isDef = v => v !== undefined  // 检查变量是否已定义

  const registerInstance = (vm, callVal) => {  // registerInstance,用于在组件中注册路由实例
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 最重要的一步:利用Vue.mixin去把beforeCreate和destroyed钩子函数注入到每一个组件中**
  Vue.mixin({
    beforeCreate () {  // beforeCreate生命周期钩子中执行一些逻辑,包括**初始化路由**、定义响应式对象等
      if (isDef(this.$options.router)) { // 判断当前组件是否存在$options.router,存在则是根组件
        this._routerRoot = this  // 将当前组件设置为根组件(根Vue实例)
        this._router = this.$options.router  //  将当前组件的$options.router赋值给 _router,即保存了路由实例
        this._router.init(this)
        // 将_route变量变成响应式对象,实现当路由发生变化时自动更新视图
        Vue.util.defineReactive(this, '_route', this._router.history.current)  // 把this._route变成响应式对象
      } else {  // 非根组件, 将其与根组件关联起来
        // 当前组件有父组件,并且父组件存在_routerRoot,则将其设置为当前组件的_routerRoot,否则将当前组件设置为自身的_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () { // 注销路由实例
      registerInstance(this)
    }
  })
  // 原型上定义$router属性,使得在组件中可以通过**this.$router访问路由实例**
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  // 原型上定义 $route 属性,使得在组件中可以通过**this.$route访问当前路由信息**
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  Vue.component('RouterView', View) **// 全局注册名为RouterView的组件,使用的组件是View**
  Vue.component('RouterLink', Link) **// 全局注册名为RouterLink的组件,使用的组件是Link**

  const strats = Vue.config.optionMergeStrategies  // 获取Vue的配置选项合并策略
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created  // 将路由相关的生命周期钩子函数合并策略设置为created
}

        当用户执行Vue.use(VueRouter)的时候,实际上就是在执行install函数(完成将 Vue Router 注入到 Vue 实例中),为了确保 install 逻辑只执行一次,用了install.installed变量做已安装的标志位。

        另外用一个全局的_Vue来接收参数Vue,因为作为Vue的插件对Vue对象是有依赖的,但又不能去单独去import Vue,因为那样会增加包体积,所以就通过这种方式拿到Vue对象。 Vue-Router安装最重要的一步就是利用Vue.mixin去把beforeCreate和destroyed钩子函数注入到每一个组件中。Vue.mixin的定义,在vue/src/core/global-api/mixin.js 中:

export function initMixin (Vue: GlobalAPI) { // 接受参数Vue,用于初始化Vue实例
  Vue.mixin = function (mixin: Object) {  // 将mixin函数添加到Vue上。参数mixin,表示要混入的选项
    this.options =** mergeOptions(this.options, mixin)  // 将当前Vue实例的选项与传入的mixin对象进行合并
    return this
  }
}

        它的实现实际上非常简单,就是把要混入的对象通过mergeOptions合并到Vue的options 中,由于每个组件的构造函数都会在extend阶段合并Vue.options到自身的options中,所以也就相当于每个组件都定义了mixin定义的选项。 回到 Vue-Router 的install方法,先看混入的beforeCreate钩子函数,对于根Vue实例而言,执行该钩子函数时定义了this._routerRoot表示它自身; this._router表示VueRouter的实例router,它是在new Vue的时候传入的;另外执行了this._router.init()方法初始化 router,这个逻辑之后介绍,然后用 defineReactive方法把this._route变成响应式对象,这个作用我们之后会介绍。而对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候this._routerRoot始终指向的离它最近的传入了router对象作为配置而实例化的父实例。 对于beforeCreate和destroyed钩子函数,它们都会执行registerInstance方法,这个方法的作用之后会介绍。 接着给Vue原型上定义了router和route 2个属性的get方法,这就是为什么我们可以在组件实例上可以访问this.router以及 this.route,它们的作用之后介绍。 接着又通过Vue.component 方法定义了全局的<router-link>和<router-view> 2个组件,这也是为什么我们在写模板的时候可以使用这两个标签,它们的作用也是之后介绍。 最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。

总结:

  • Vue编写插件的时候,通常要提供静态的install方法;
  • Vue-Router的install方法会给每一组件注入beforeCreated和destoryed钩子函数。在beforeCreated做一些私有属性定义和路由初始化工作;

2. VueRouter对象

        VueRouter 的实现是一个类,定义在 src/index.js 中,先对它做一个简单地分析:

export default class VueRouter {
  static install: () => void;
  static version: string;

  app: any;
  apps: Array<any>;
  ready: boolean;
  readyCbs: Array<Function>;
  options: RouterOptions;
  mode: string;
  history: HashHistory | HTML5History | AbstractHistory;
  matcher: Matcher;
  fallback: boolean;
  beforeHooks: Array<?NavigationGuard>;  
  resolveHooks: Array<?NavigationGuard>;
  afterHooks: Array<?AfterNavigationHook>;
  // 构造函数**
  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'  // **根据传入的options.mode来确定路由模式**
    **// history模式支持判断,**supportsPushState会对浏览器UA进行检测
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {  // abstract模式不是浏览器环境下使用
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) {  // 根据路由模式创建对应的路由历史对象this.history(继承于history Class)
      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}`)
        }
    }
  }
  // 路由匹配方法: 传入原始位置raw、当前路由current和重定向来源redirectedFrom,返回匹配的路由对象
  match (
    raw: RawLocation,
    current?: Route,
    redirectedFrom?: Location
  ): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }

  get currentRoute (): ?Route {  // 当前的路由对象,通过访问路由历史对象的current属性获取
    return this.history && this.history.current
  }
  // 初始化路由和应用程序实例,并监听路由变化,更新应用程序实例的_route属性
  init (app: any) {
    // 非生产环境,并且 install.installed 不为真,则抛出错误提示,提醒在创建根实例之前调用Vue.use(VueRouter)
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    this.apps.push(app)  // 应用程序实例app添加到apps数组中,用于跟踪多个应用程序实例

    if (this.app) { // 如果当前应用程序实例this.app已存在,则直接返回,否则将传入的应用程序实例app 设置为当前应用程序实例
      return
    }

    this.app = app

    const history = this.history
    // 根据路由模式初始化路由历史对象,并监听路由变化
    if (history instanceof HTML5History) {  //  History模式,则调用transitionTo方法进行路由过渡到当前位置
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) { //  Hash模式则设置监听器并调用transitionTo 方法进行路由过渡到当前位置
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {  // 每当路由变化时,更新每个应用程序实例 _route属性为新的路由信息
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }
  // 导航触发之前调用的钩子函数,可以用来进行导航守卫
  beforeEach (fn: Function): Function {  // 函数作为参数,并将其注册到beforeHooks钩子数组中
    return registerHook(this.beforeHooks, fn)
  }
  // 导航确认之前调用的钩子函数,和 beforeEach 类似,但是在**所有异步路由组件解析之后调用**
  beforeResolve (fn: Function): Function {  // 将函数注册到resolveHooks钩子数组中
    return registerHook(this.resolveHooks, fn)
  }
  // 导航成功完成之后调用
  afterEach (fn: Function): Function {  // 将函数注册到afterHooks钩子数组中
    return registerHook(this.afterHooks, fn)
  }
  // 路由初始化完成时调用的回调函数
  onReady (cb: Function, errorCb?: Function) {  // 将onReady方法传递给路由历史管理对象(如this.history) 的onReady方法
    this.history.onReady(cb, errorCb)
  }
  // 当路由初始化失败时调用的回调函数
  onError (errorCb: Function) {  // 将onError方法传递给路由历史管理对象的onError方法
    this.history.onError(errorCb)
  }
  // 路由历史堆栈中添加一个新的路由记录
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)  // 调用路由历史管理对象的push方法,用于向历史堆栈中添加新的路由记录。
  }
  // 替换当前的路由记录,导航到指定的位置
  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.replace(location, onComplete, onAbort)  // 类似于push,但是用于替换当前路由记录而不是添加新的记录
  }

  go (n: number) {
    this.history.go(n)  // 整数参数n,表示前进或后退的步数。调用路由历史管理对象的go方法,以在浏览器历史记录中导航
  }

  back () {
    this.go(-1)  // back方法调用go(-1),表示后退一页
  }

  forward () {
    this.go(1)  // forward方法调用go(1),表示前进一页
  }
  // 获取与目标位置匹配的组件数组,用于动态加载路由组件
  getMatchedComponents (to?: RawLocation | Route): Array<any> {  
    const route: any = to  // 可选的参数to,表示要匹配的目标路由。返回目标路由的匹配组件数组
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if (!route) {
      return []
    }
    return [].concat.apply([], route.matched.map(m => {
      return Object.keys(m.components).map(key => {
        return m.components[key]
      })
    }))
  }

  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)  // 使用路由匹配器(matcher)对目标位置进行匹配,得到路由信息
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)  // 根据路由信息生成href,并返回解析后的路由信息对象
    return {
      location,
      route,
      href,
      normalizedTo: location,
      resolved: route
    }
  }
  // 将新的路由配置添加到路由匹配器中,并触发对应的路由更新**
  addRoutes (routes: Array<RouteConfig>) {  
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}

        VueRouter 定义了一些属性和方法,下面按照书序逐个分析其作用:

        首先,从它的构造函数看,当我们执行 new 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'
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = 'hash'
  }
  if (!inBrowser) {
    mode = 'abstract'
  }
  this.mode = 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}`)
      }
  }
}

        构造函数定义了一些属性,其中this.app表示根Vue实例,this.apps保存持有$options.router属性的 Vue实例,this.options 保存传入的路由配置

        this.beforeHooks、 this.resolveHooks、this.afterHooks 表示一些钩子函数,我们之后会介绍;this.matcher表示路由匹配器(重点),我们之后会介绍;

        this.fallback表示在浏览器不支持history.pushState的情况下,根据传入的fallback配置参数,决定是否回退到hash模式;

        this.mode表示路由创建的模式;

        this.history表示路由历史的具体的实现实例,它是根据this.mode的不同实现不同,它有History基类,然后不同的history实现都是继承History。 实例化VueRouter后会返回它的实例router,我们在new Vue的时候会把router作为配置的属性传入,回顾一下上一节我们讲beforeCreate混入的时候有这么一段代码:

beforeCreate() {
  if (isDef(this.$options.router)) {  // 检查是否定义了路由器实例
    // ...
    this._router = this.$options.router  // 将路由器实例赋值给组件实例的 _router 属性
    this._router.init(this)  // 传入了router实例,都会执行router.init方法
    // ...
  }
}  

        所以组件在执行 beforeCreate 钩子函数的时候,如果传入了router实例,都会执行router.init方法:

init (app: any) {
  // 非生产环境下,使用assert函数检查是否已经安装了Vue Router插件
  process.env.NODE_ENV !== 'production' && assert(
    install.installed,
    `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
    `before creating root instance.`
  )

  this.apps.push(app)  // Vue实例,然后存储到this.apps中

  if (this.app) {  // 已经初始化过应用程序,则直接返回,避免重复初始化
    return
  }

  this.app = app  // 将传入的应用程序实例赋值给路由器实例的app属性

  const history = this.history  // 获取this.history,后面判断使用

  if (history instanceof HTML5History) {  // HTML5History
    history.transitionTo(history.getCurrentLocation())  // 将当前路由状态切换到当前地址对应的路由
  } else if (history instanceof HashHistory) {  // HashHistory执行不同的逻辑
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(  // 将当前路由状态切换到当前地址对应的路由,并设置哈希变化时的监听器。
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }

  history.listen(route => {  // 监听路由变化,并将路由信息_route更新到所有应用程序实例中
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

        init()方法的逻辑:

        传入的参数是Vue实例,然后存储到this.apps中;只有根Vue实例会保存到this.app中,并且会拿到当前的this.history,根据它的不同类型来执行不同逻辑。

        由于我们平时使用 hash路由多一些,所以我们先看这部分逻辑,先定义了 setupHashListener 函数,接着执行了 history.transitionTo 方法,它是定义在History基类中,代码在 src/history/base.js:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const route = this.router.match(location, this.current) // 做匹配
  // ...
}

        先不着急去看 transitionTo 的具体实现,先看第一行代码,它调用了this.router.match 函数:

// match方法,用于根据给定的原始位置(raw)、当前路由对象(current)和重定向来源(redirectedFrom)
// 来匹配路由并返回匹配的路由对象
match (
  raw: RawLocation,
  current?: Route,
  redirectedFrom?: Location
): Route {
 ** return this.matcher.match(raw, current, redirectedFrom)**
}

        实际上是调用了this.matcher.match方法去做匹配,所以接下来我们先来了解一下 matcher的相关实现。

总结:

        路由初始化的时机是在组件的初始化阶段执行到beforeCreated钩子函数的时候会执行router.init方法。然后会执行history.transitionTo方法做路由过度。

3. matcher

        上一小节实例化Vue Router对象时,会执行器构造函数,然后会执行createMatcher方法,传入参数是用户配置数组options.routes和实例对象VueRouter实例,返回Matcher对象。

        matcher相关的实现都在src/create-matcher.js中,我们先来看一下matcher的数据结构:

export type Matcher = {  // Matcher对象的属性值为函数
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
};

        Matcher返回了2个方法,match和addRoutes,在上一节我们接触到了 match 方法,顾名思义它是做匹配,那么匹配的是什么

        在介绍之前,我们先了解路由中重要的2个概念,Loaction 和 Route,它们的数据结构定义在 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是 /abc,query是 {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表示的是路由中的一条线路,它除了描述了类似Loctaion的path、query、hash这些概念,还有matched表示匹配到的所有的 RouteRecord。Route的其他属性我们之后会介绍。

3.1 createMatcher

        在了解了Location和Route后,我们来看一下matcher的创建过程:

export function createMatcher (
  routes: Array<RouteConfig>,  **// 用户定义的路由配置数组**
  router: VueRouter  // **new VueRouter返回的实例,用于路由导航等操作**
): Matcher {  // Matcher对象,每个属性都是一个函数
  const { pathList, pathMap, nameMap } = createRouteMap(routes)  // 初始化,创建路由映射表
  // pathList存储了所有路由的路径,pathMap将路径映射到路由记录,nameMap将命名路由映射到路由记录

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

  // 根据路由记录创建路由对象。如果路由记录包含重定向信息,则调用redirect函数进行重定向;如果包含别名
  // 信息,则调用 alias 函数进行别名处理;否则,调用 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)
  }

  return {
    match,
    addRoutes
  }
}

        createMatcher接收2个参数,一个是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 }
]

        首先, 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 => {  // 遍历用户定义的路由配置数组routes
    **addRouteRecord(pathList, pathMap, nameMap, route)**
  })

  for (let i = 0, l = pathList.length; i < l; i++) { **// 通配符优先级逻辑**
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }

  return {  // 返回
    pathList,
    pathMap,
    nameMap
  }
}

        createRouteMap函数的目标是把用户的路由配置转换成一张路由映射表,该路由映射表包含3个部分pathList 存储所有的path,pathMap表示一个path到RouteRecord的映射关系,而nameMap表示name到RouteRecord的映射关系。

        那么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>;  // 路由组件是否需要注入路由参数,可以是布尔值、对象、函数或者字典
}

        RouteRecord的创建是通过遍历routes为每一个route执行addRouteRecord 方法生成一条记录,来看一下addRouteRecord的定义:

function addRouteRecord (
  pathList: Array<string>,  // 路由路径列表,存储所有路由记录的路径
  pathMap: Dictionary<RouteRecord>,  // 路由路径映射表,根据路径快速查找对应的路由记录
  nameMap: Dictionary<RouteRecord>,  // 路由名称映射表,根据路由名称快速查找对应的路由记录
  route: RouteConfig,  // 路由配置对象,包含了路由的各种信息,如路径、组件等
  parent?: RouteRecord,  // 父路由记录对象,用于构建路由的嵌套结构
  matchAs?: string  // 匹配路径的别名,用于处理路由的别名情况
) {
  const { path, name } = route  // 解构路由配置对象,获取路径和名称

  // 在开发环境下进行一些验证,确保路由配置的正确性
  if (process.env.NODE_ENV !== 'production') {
    assert(path != null, `"path" is required in a route configuration.`)  // 确保提供了路径属性
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(path || name)} cannot be a ` +
      `string id. Use an actual component instead.`  // 确保组件不是一个字符串 ID,而是一个有效的组件
    )
  }

  // 根据路由配置的path和pathToRegexpOptions生成正则表达式,并将其保存在regex属性中
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  const normalizedPath = normalizePath(
    path,
    parent,
    **pathToRegexpOptions.strict**
  )

  // 如果路由配置中指定了是否大小写敏感,则设置正则表达式的 sensitive 属性
  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 }  // 路由组件的属性
  }

  **// 如果路由配置中存在子路由,则递归调用 addRouteRecord 处理子路由**
  if (route.children) {  **// 路由有Children时(嵌套路由)**
    if (process.env.NODE_ENV !== 'production') {
      if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
          `When navigating to this named route (:to="{name: '${route.name}'"), ` +
          `the default child route will not be rendered. Remove the name from ` +
          `this route and use the name of the default child route for named ` +
          `links instead.`
        )
      }
    }
    route.children.forEach(child => {  **// 遍历Children,递归调用addRouteRecord方法**
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, **child**, record, childMatchAs)
    })
  }

  // 处理路由的别名,并将别名对应的路由也加入到pathMap和nameMap中(非主线逻辑)
  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 || '/'
      )
    })
  }

  // 将当前路由记录添加到pathMap中
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  // 如果存在路由名称,则将当前路由记录添加到 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 的代码如下:

const record: RouteRecord = {  // 参数
  path: normalizedPath,  // 规范化后的路径,通过 normalizePath 函数生成
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),  // 路径对应的正则表达式,通过 compileRouteRegex 函数生成
  components: route.components || { default: route.component },  //  路由组件,可以是一个对象,也可以是一个组件名称 
  instances: {},  // 路由实例
  name,  //  路由名称
  parent,  // 父级路由记录
  matchAs,  // 匹配别名
  redirect: route.redirect,  // 重定向路径
  beforeEnter: route.beforeEnter,  // 进入路由前的钩子函数
  meta: route.meta || {},  // 路由元信息,例如标题、描述等
  props: route.props == null  // 路由组件的 props
    ? {}
    : route.components
      ? route.props
      : { default: route.props }
}

        这里要注意几个点:

        path 是规范化后的路径,它会根据parent的path做计算;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: '[^\\/]+?' }]

        components是一个对象,通常我们在配置中写的component实际上这里会被转换成 {components: route.component};

        instances表示组件的实例,也是一个对象类型;

        parent表示父的RouteRecord,因为我们配置的时候可能会配置子路由,所以整个RouteRecord也就是一个树型结构

if (route.children) {  // 当前路由配置中存在子路由,就会遍历每一个子路由,并对其调用addRouteRecord 函数,以便添加到路由记录中
  // ...
  route.children.forEach(child => {
    const childMatchAs = matchAs
      ? cleanPath(`${matchAs}/${child.path}`)
      : undefined
    addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)  // 递归**
  })
}

        如果配置了children,那么递归执行addRouteRecord方法,并把当前的record作为parent传入,通过这样的深度遍历,我们就可以拿到一个route下的完整记录。

if (!pathMap[record.path]) {  // 检查pathMap中是否已经存在了相同路径的路由记录
  pathList.push(record.path)  // 当前路由记录的路径record.path添加到pathList中。pathList 是一个数组,用于存储所有路由记录的路径
  pathMap[record.path] = record // 当前路由记录record添加到pathMap中,以路径record.path为键。这样就可以通过路径快速查找对应的路由记录
}

        为 pathList 和 pathMap 各添加一条记录。

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

        如果我们在路由配置中配置了 name,则给nameMap添加一条记录。 由于pathList、pathMap、nameMap都是引用类型,所以在遍历整个routes过程中去执行 addRouteRecord方法,会不断给他们添加数据。

        那么经过整个createRouteMap方法的执行,我们得到的就是 pathList、pathMap和nameMap。其中 pathList 是为了记录路由配置中的所有path,而pathMap和nameMap都是为了通过path和name能快速查到对应的RouteRecord。 再回到 createMatcher 函数,接下来就定义了一系列方法,最后返回了一个对象。

return {
  match,
  addRoutes
}

        也就是说,matcher是一个对象,它对外暴露了match和addRoutes方法

3.2 addRoutes

        addRoutes方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由(例如服务端下发或者动态添加),所以Vue-Router也提供了这一接口:

function addRoutes (routes) {
  // 接受一个路由配置数组作为参数,并调用了之前提到的createRouteMap函数来更新路由映射表
  createRouteMap(routes, pathList, pathMap, nameMap)
}

        addRoutes的方法十分简单,再次调用createRouteMap即可,传入新的routes配置,由于pathList、pathMap、nameMap 都是引用类型,执行addRoutes后会修改它们的值。

3.3 match

        路由匹配的函数match的实现如下:

function match (
  raw: RawLocation,    // 原始的位置信息,可以是字符串路径或者一个Location对象
  currentRoute?: Route,  // 当前路由对象,可选参数,用于处理嵌套路由情况
  redirectedFrom?: Location  // 重定向来源的位置信息,可选参数,用于记录重定向信息
): Route {
  // normalizeLocation函数对原始位置信息进行处理,得到标准化的位置信息对象location,其中包括路径信息和路由名称
  const location = normalizeLocation(raw, currentRoute, false, router)
  const { name } = location

  if (name) {  // 位置信息中包含**路由名称**
    const record = nameMap[name]  // 在nameMap中查找对应的路由记录record
    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)  // 以上条件都不符合,返回一个空的路由对象
}

        match方法接收3个参数:

        raw是RawLocation类型,它可以是一个url字符串,也可以是一个Location对象;

        currentRoute是Route类型,它表示当前的路径;

        redirectedFrom和重定向相关,这里先忽略。

        match方法返回的是一个路径,它的作用是根据传入的raw和当前的路径currentRoute计算出一个新的路径并返回

        首先,执行normalizeLocation对原始位置信息进行处理,以得到标准化的位置信息对象Location,它的定义在src/util/location.js中:

export function normalizeLocation (
  raw: RawLocation,  // raw是原始位置信息
  current: ?Route,  // current是当前路由对象(可选)
  append: ?boolean, // append表示是否附加路径(可选)
  router: ?VueRouter  // router是VueRouter的实例(可选)
): Location {  // 返回一个标准化的位置信息对象Location
  let next: Location = typeof raw === 'string' ? { path: raw } : raw // 判断raw是不是字符串,是则将其转换为包含path属性的对象
  if (next.name || next._normalized) {  // 检查该对象是否已经包含了name属性或_normalized属性,如果已经包含则直接返回该对象
    return next
  }
  // 参数处理:next对象中不包含path属性但**包含params属性**,并且传入了current参数
  if (!next.path && next.params && current) { 
    next = assign({}, next)  // 克隆next对象
    next._normalized = true  // 标记为已标准化
    const params: any = assign(assign({}, current.params), next.params)  // 当前路由对象的参数与 next 对象的参数合并
    **if (current.name) {  // 当前路由对象有name属性**
      next.name = current.name  // 将next对象的name属性设置为当前路由对象的name
      next.params = params  // 并更新params属性
    } else if (current.matched.length) {  //如果当前路由对象的matched数组不为空
      const rawPath = current.matched[current.matched.length - 1].path  //使用最后一个匹配项的路径来填充参数
      next.path = fillParams(rawPath, params, `path ${current.path}`)  // 更新path属性
    } else if (process.env.NODE_ENV !== 'production') {  // 以上条件都不满足,并且不是生产环境,则发出警告
      warn(false, `relative params navigation requires a current route.`)
    }
    return next
  }

  const parsedPath = parsePath(next.path || '')   // 解析next.path
  const basePath = (current && current.path) || '/'    // 确定基础路径basePath
  const path = parsedPath.path   // next.path解析后存在路径
    ? resolvePath(parsedPath.path, basePath, append || next.append)  // 使用resolvePath函数来解析路径
    : basePath  // 使用basePath

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

  let hash = next.hash || parsedPath.hash   // 处理哈希值,如果next.hash存在则使用它,否则使用解析后的哈希值
  if (hash && hash.charAt(0) !== '#') {
    hash = `#${hash}`
  }
  // 返回标准化的位置信息对象Location,其中包括_normalized属性、路径path、查询参数query和哈希值 hash
  return {
    _normalized: true,
    path,
    query,
    hash
  }
}

        normalizeLocation方法的作用是根据raw,current计算出新的location,它主要处理了raw的两种情况:

        一种是有params且没有path;

        一种是有path的。

        对于第一种情况,如果current有name,则计算出的location也有name。计算出新的location后,对 location的name和path的两种情况做了处理.

  • name

        有name的情况下就根据nameMap匹配到record,它就是一个RouterRecord对象,如果record不存在,则匹配失败,返回一个空路径;然后拿到record对应的paramNames,再对比currentRoute中的 params,把交集部分的params添加到location中,然后在通过fillParams方法根据record.path和 location.path计算出location.path,最后调用_createRoute(record, location, redirectedFrom)去生成一条新路径,该方法我们之后会介绍。

  • path

        通过name 我们可以很快的找到record,但是通过path并不能,因为我们计算后的location.path是一个真实路径,而record中的path可能会有param,因此需要对所有的pathList 做顺序遍历, 然后通过 matchRoute方法根据record.regex、location.path、location.params匹配,如果匹配到则也通过 _createRoute(record, location, redirectedFrom) 去生成一条新路径。因为是顺序遍历,所以我们书写路由配置要注意路径的顺序,因为写在前面的会优先尝试匹配。

        技巧:上述方法的辅助函数非常多,需要结合具体场景做出多种判断,结合源码中test测试的方法查看,可更有助于理解输入输出

        最后,我们来看一下_createRoute的实现:

function _createRoute (  // 创建路由对象Route
  record: ?RouteRecord,   //record 表示路由记录对象
  location: Location,  // location 表示位置信息对象
  redirectedFrom?: Location  // redirectedFrom 表示重定向来源的位置信息对象(可选)
): Route {
  if (record && record.redirect) {  // 检查路由记录对象record是否存在且是否有重定向属性redirect
    // 存在重定向,则调用redirect 函数来创建重定向的路由对象,并传入record和重定向来源或位置信息对象
    return redirect(record, redirectedFrom || location)
  }
  if (record && record.matchAs) {  // 检查路由记录对象record是否存在且是否有matchAs属性
    // 存在matchAs属性,则调用alias函数来创建别名路由对象,并传入record、位置信息对象和matchAs属性
    return alias(record, location, record.matchAs)
  }
  // 以上条件都不满足,则调用 createRoute 函数来创建普通路由对象,并传入 record、位置信息对象、重定向来源位置信息对象和路由器实例(如果有)
  return createRoute(record, location, redirectedFrom, router)
}

        先不考虑record.redirect和record.matchAs的情况,最终会调用createRoute方法,它的定义在 src/uitl/route.js 中:

export function createRoute (
  record: ?RouteRecord,  // record表示路由记录对象
  location: Location,  // location表示位置信息对象
  redirectedFrom?: ?Location,  // redirectedFrom表示重定向来源的位置信息对象(可选)
  router?: VueRouter  // router表示VueRouter实例(可选)
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}  // 初始值为 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) : []
  }
  // 若传入了重定向来源的位置信息对象redirectedFrom,则将重定向来源的完整路径赋值给route对象的
  // redirectedFrom 属性。
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)  // 避免对象从外部进行修改
}

        createRoute可以根据record和location创建出来,最终返回的是一条Route路径,我们之前也介绍过它的数据结构。在Vue-Router中,所有的Route最终都会通过createRoute函数创建,并且它最后是不可以被外部修改的。Route 对象中有一个非常重要属性是matched,它通过formatMatch(record)计算而来:

// formatMatch 函数接受一个可选的 RouteRecord 对象,并返回一个 RouteRecord 数组。
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {  // 接收一个可选的路由记录对象record
  const res = []
  while (record) {
    res.unshift(record)  // 将当前记录插入结果数组的头部
    record = record.parent   // 继续向上查找父记录
  }
  return res   // 返回包含从根到当前记录的数组
}

        可以看它是通过 record 循环向上找parent,直到找到最外层,并把所有的record都push到一个数组中,最终返回的就是record的数组,它记录了一条线路上的所有record。matched属性非常有用,它为之后渲染组件提供了依据。

        总结:

        createMatcher的初始化就是根据路由的配置描述创建映射表,包括路径、名称到路由record的映射关系;

        match会根据传入的位置和路径计算出新的位置,并匹配到对应的路由record,然后根据新的位置和record创建新的路径并返回。

4. 路径切换

        history.transitionTo是Vue-Router中切换路由线路的时候执行的重要方法方法。前一节分析了matcher的相关实现,知道matcher是如何找到匹配的新线路,那么匹配到新线路后的逻辑呢?

        接下来我们来完整分析一下transitionTo的实现,它的定义在src/history/base.js中:

**// 路由切换**
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {  // 参数: 目标位置location(类型为RawLocation)、完成时的回调函数onComplete(可选参数)、中止时的回调函数onAbort(可选参数)
  const route = this.router.match(location, this.current)  // 匹配目标路由地址location,并**将匹配到的路由对象保存在route变量中**
  **this.confirmTransition(route, () => {  // 传入目标路由对象route,以及两个回调函数**
    this.updateRoute(route)    // 更新当前的路由状态为目标路由状态
    onComplete && onComplete(route)  // 存在onComplete回调函数,则调用该函数并传入目标路由对象route作为参数
    this.ensureURL()  // 更新URL地址,确保URL与当前路由状态一致

    if (!this.ready) {  // 路由尚未准备好
      this.ready = true
      this.readyCbs.forEach(cb => { cb(route) })  //  遍历readyCbs数组中的每个回调函数,并将目标路由对象route作为参数依次调用
    }
  }, err => {  // 匹配失败时的回调函数
    if (onAbort) {  // 存在onAbort回调函数,则调用该函数并传入错误对象err作为参数
      onAbort(err)
    }
    if (err && !this.ready) {  // 存在错误对象err且路由尚未准备好
      this.ready = true
      this.readyErrorCbs.forEach(cb => { cb(err) })  //  遍历readyErrorCbs数组中的每个回调函数,并将错误对象err作为参数依次调用
    }
  })
}

        transitionTo首先根据目标location和当前路径this.current执行this.router.match方法去匹配到目标的路径。这里this.current是history维护的当前路径,它的初始值是在history的构造函数中初始化的:

this.current = START   // this.current初始值

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

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

        这样就创建了一个初始的Route,而transitionTo实际上也就是在切换this.current,稍后我们会看到。 拿到新的路径后,那么接下来就会执行confirmTransition方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个confirmTransition API设计成带有成功回调函数和失败回调函数,先来看一下它的定义:

// 路由过渡确认函数,主要用于处理路由切换时的**过渡效果和导航守卫**
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {  // 参数:目标路由对象route(类型为Route)、完成时的回调函数onComplete(类型为Function)、中止时的回调函数onAbort(可选参数,类型为Function)
 ** const current = this.current  // 当前路由对象保存在变量current中**
  const abort = err => {  // 定义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()  // 调用this.ensureURL()来确保当前路由地址和浏览器URL一致,并执行abort()函数来终止路由过渡
    return abort()
  }
  **// 导航守卫相关逻辑,数组updated、deactivated、activated,分别表示需要更新的路由、需要离开的路由、需要进入的路由**
  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)
  )  // 队列queue,其中包括离开路由的导航守卫、全局前置守卫、更新路由的导航守卫、进入路由的beforeEnter 钩子、异步组件的解析

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

        首先,定义了abort函数,然后判断如果满足计算后的route和current是相同路径的话,则直接调用 this.ensureUrl和abort,ensureUrl这个函数我们之后会介绍。 接着又根据current.matched和route.matched执行了resolveQueue方法解析出3个队列:

**// 比较当前路由和目标路由之间的差异,返回一个包含需要更新、激活和停用的路由记录数组的对象**
function resolveQueue (
  current: Array<RouteRecord>,  // 参数:当前路由记录数组current和目标路由记录数组next
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,  // 返回对象,包含三个属性:updated、activated和deactivated,它们都是路由记录数组
  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),   // 从目标路由记录数组中截取出来的前i个元素
    activated: next.slice(i),  // 从目标路由记录数组中截取剩余的元素
    deactivated: current.slice(i)  // 从当前路由记录数组中截取剩余的元素
  }
}

        因为route.matched是一个RouteRecord的数组,由于路径是由current变向route,那么就遍历对比2 边的RouteRecord,找到一个不一样的位置i,那么next中从0到i的RouteRecord 是两边都一样,则为updated的部分;从i到最后的RouteRecord是next独有的,为activated的部分;而current中从i到最后的RouteRecord则没有了,为deactivated的部分。 拿到updated、activated、deactivated 3 个ReouteRecord数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数。

4.1 导航守卫(重要)

        所谓导航守卫,实际上就是发生在路由路径切换的时候,执行的一系列钩子函数

        先从整体上看一下这些钩子函数执行的逻辑,首先构造一个队列queue,它实际上是一个数组;然后再定义一个迭代器函数iterator;最后再执行runQueue方法来执行这个队列。我们先来看一下runQueue的定义,在 src/util/async.js 中:

// 按顺序执行一个包含导航守卫回调函数的队列
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => { // step的函数的参数index作为当前队列中要执行的导航守卫回调函数的索引
    if (index >= queue.length) {  // 索引超过队列的长度。表示已经执行完所有导航守卫回调函数,调用完成时的回调函数cb()
      cb()
    } else {
      if (queue[index]) {  // 检查队列中当前索引位置的导航守卫回调函数是否存在
        fn(queue[index], () => {  //调用传入的回调函数fn,并将当前导航守卫回调函数作为第一个参数,以及一个完成时的回调函数作为第二个参数。在完成时的回调函数中,调用step(index + 1)来继续执行下一个导航守卫回调函数
          step(index + 1)
        })
      } else {  // 当前索引位置的导航守卫回调函数不存在,直接调用step(index + 1)来继续执行下一个导航守卫回调函数
        step(index + 1)
      }
    }
  }
  step(0)
}

        这是一个非常经典的异步函数队列化执行的模式, queue是一个NavigationGuard类型的数组,我们定义了step函数,每次根据index从queue中取一个guard,然后执行fn函数,并且把guard作为参数传入,第二个参数是一个函数,当这个函数执行的时候再递归执行step函数,前进到下一个,注意这里的fn就是我们刚才的iterator函数,那么我们再回到iterator函数的定义:

const iterator = (hook:** NavigationGuard**, next) => {  // 参数:导航守卫回调函数hook和next函数
  if (this.pending !== route) {  // 检查当前正在处理的路由是否与传入的路由route相同
    return abort()   // 路由已经发生变化,立即调用abort()函数终止当前导航
  }
  try {
    hook(route, current, (to: any) => {  //调用传入的导航守卫回调函数hook,参数:当前路由route、当前路由状态current以及一个回调函数
      if (to === false || isError(to)) {  // 回调函数返回的结果to是否为false或者是一个错误对象
        this.ensureURL(true) // 调用ensureURL(true)确保URL与当前路由匹配,并调用abort(to)函数终止当前导航
        abort(to)
      } else if (   // 检查回调函数返回的结果to是否为字符串或者是一个包含path或name属性的对象
        typeof to === 'string' ||
        (typeof to === 'object' && (
          typeof to.path === 'string' ||
          typeof to.name === 'string'
        ))
      ) {
        abort()  // 终止当前导航
        **// 检查回调函数返回的结果to是否为一个包含replace属性的对象。如果是,则调用this.replace(to)
        // 进行路由替换操作,否则调用this.push(to)进行路由跳转操作**
        if (typeof to === 'object' && to.replace) {
          this.replace(to)
        } else {
          this.push(to)
        }
      } else {  // 回调函数返回的结果既不是false,也不是一个字符串或者包含path或name属性的对象,则调用next(to)函数继续导航
        next(to)
      }
    })
  } catch (e) {
    abort(e)  // 捕获异常,终止导航
  }
}

        iterator函数逻辑很简单,它就是去执行每一个导航守卫hook,并传入route、current 和匿名函数,这些参数对应文档中的to、from、next,当执行了匿名函数,会根据一些条件执行abort或next,只有执行next的时候,才会前进到下一个导航守卫钩子函数中,这也就是为什么官方文档会说只有执行next方法来resolve这个钩子函数。 那么最后我们来看 queue 是怎么构建一个导航守卫队列的:

const queue: Array<?NavigationGuard> = [].concat(  // 声明了queue的数组变量,用于存储导航守卫
  extractLeaveGuards(deactivated),
  this.router.beforeHooks,
  extractUpdateHooks(updated),
  activated.map(m => m.beforeEnter),
  resolveAsyncComponents(activated)
)

        按照顺序如下:

  1. 在失活的组件里调用离开守卫;
  2. 调用全局的beforeEach守卫;
  3. 在重用的组件里调用beforeRouteUpdate守卫;
  4. 在激活的路由配置里调用beforeEnter;
  5. 解析异步路由组件;

        接下来我们来分别介绍这 5 步的实现。 第一步 通过执行extractLeaveGuards(deactivated),先来看一下extractLeaveGuards 的定义:

function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  //参数:
  // deactivated: 即将被停用的路由记录数组。
  // beforeRouteLeave: 一个字符串,指定要提取的守卫类型是beforeRouteLeave。这是VueRouter中定义的守卫之一,它在路由离开当前组件时触发。
  // bindGuard: 一个函数,用于绑定守卫函数到相应的上下文中。这个参数在调用时可能需要提供具体的实现逻辑。
  // true: 一个布尔值,根据上下文,这可能表示某种特定的行为或标志,例如是否提取嵌套路由的守卫
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

        它内部调用了extractGuards的通用方法,可以从RouteRecord数组中提取各个阶段的守卫

function extractGuards (
  records: Array<RouteRecord>,  // 路由记录的数组,每个记录代表一个路由
  name: string,  // 指定要提取的守卫类型,例如 'beforeRouteLeave
  bind: Function,  // 一个函数,用于绑定守卫函数到相应的上下文中
  reverse?: boolean  // 可选的布尔值参数,用于控制守卫数组的顺序
): Array<?Function> {
  // def: 组件的定义。
  // instance: **组件的实例**。
  // match: 与路由匹配的信息。
  // key: 一个键值,可能用于唯一标识组件
  const guards = flatMapComponents(records, (def, instance, match, key) => {  // **guard为真实的守卫函数**
    const guard = **extractGuard**(def, name)  // 获取到组件中对应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)  // 调用flatten函数处理得到一个扁平化的数组并返回
}

        这里用到了flatMapComponents方法去从records中获取所有的导航,它的定义在src/util/resolve-components.js中

export function flatMapComponents (
  matched: Array<RouteRecord>,  // 参数:一个由RouteRecord对象组成的数组matched
  fn: Function   // 参数:一个回调函数fn
): Array<?Function> {    // 返回一个由函数组成的数组
  return flatten(matched.map(m => {     // 对matched数组进行扁平化处理
    // 遍历当前RouteRecord对象m中包含的所有组件,对每个组件应用一个回调函数fn,并将结果作为数组返回
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],   // 对应的是组件的实例
      m, key
    ))
  }))
}

export function flatten (arr: Array<any>): Array<any> {  // 参数:任意数组arr,返回一个扁平化后的数组
  return Array.prototype.concat.apply([], arr)
}

        flatMapComponents的作用就是返回一个数组,数组的元素是从matched里获取到所有组件的key,然后返回fn函数执行的结果,flatten作用是把二维数组拍平成一维数组。 那么对于extractGuards中flatMapComponents的调用,执行每个fn的时候,通过extractGuard(def, name) 获取到组件中对应name的导航守卫:

function extractGuard (
  def: Object | Function,
  key: string
): NavigationGuard | Array<NavigationGuard> {  // 返回一个导航守卫或导航守卫数组
  if (typeof def !== 'function') { // def不是一个函数类型
    def = _Vue.extend(def)  // **使用_Vue.extend方法将def对象转换为一个Vue组件构造函数**
  }
  return def.options[key]  //  返回转换后的组件构造函数的options属性中与传入的key相对应的值
}

        获取到guard(组件中对应name的导航守卫)后,还会调用bind方法把组件的实例instance作为函数执行的上下文绑定到guard上,bind方法的对应的是bindGuard:

function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard { // 参数:导航守卫guard和Vue实例instance
  if (instance) {  // Vue实例instance
    return function boundRouteGuard () {  // 返回一个新的函数boundRouteGuard
      return guard.apply(instance, arguments)  // 在新函数中调用原始导航守卫guard,使用apply方法将其绑定到Vue实例instance上,并传入调用boundRouteGuard时的所有参数
    }
  }
}

        那么对于 extractLeaveGuards(deactivated) 而言,最终获取到的就是所有失活组件中定义的 beforeRouteLeave 钩子函数。 第二步 this.router.beforeHooks,在我们的VueRouter类中定义了beforeEach方法,在src/index.js 中:

beforeEach (fn: Function): Function {
  return registerHook(this.beforeHooks, fn)  // 注册全局守卫;this.beforeHooks是一个数组,存储了所有的全局前置守卫
}

function registerHook (list: Array<any>, fn: Function): Function {  // 参数:数组list和函数fn
  list.push(fn)  // 将fn函数添加到list数组中,将其注册为一个守卫
  return () => {
    const i = list.indexOf(fn)  // 获取fn函数在list数组中的索引
    if (i > -1) list.splice(i, 1)   // 如果索引大于-1,则从list数组中移除fn函数
  }
}

        当用户使用router.beforeEach注册了一个全局守卫,就会往router.beforeHooks添加一个钩子函数,这样this.router.beforeHooks获取的就是用户注册的全局beforeEach守卫。 第三步 执行extractUpdateHooks(updated),来看一下extractUpdateHooks的定义:

function **extractUpdateHooks** (updated: Array<RouteRecord>): Array<?Function> { //参数:路由记录数组updated
  // 调用extractGuards函数,提取所有路由记录中的beforeRouteUpdate钩子函数,并使用bindGuard函数将这些钩子函数绑定到Vue实例上
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

        类似于extractLeaveGuards(deactivated) 函数,extractUpdateHooks(updated)获取到的就是所有重用的组件中定义的beforeRouteUpdate钩子函数。 第四步 执行activated.map(m => m.beforeEnter),获取的是在激活的路由配置中定义的beforeEnter函数。 第五步 执行resolveAsyncComponents(activated)解析异步组件,先来看一下 resolveAsyncComponents的定义,在src/util/resolve-components.js中:

export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {  // 参数:路由记录数组matched
  return (to, from, next) => {  // 参数:目标路由、当前路由和一个回调函数
    let hasAsync = false  // 用于判断是否存在需要异步加载的组件
    let pending = 0  // 计数未完成的异步加载组件数量
    let error = null  // 存储组件加载过程中发生的错误
    **// 调用flatMapComponents函数,遍历所有路由记录,并执行回调函数**
    **flatMapComponents**(matched, (def, _, match, key) => {
      //  判断当前路由记录是否需要异步加载
      if (typeof def === 'function' && def.cid === undefined) {  // 组件ID(cid),为undefined则为异步组件
        hasAsync = true  // 表示存在需要异步加载的组件
        pending++   // 异步加载组件数量+1

        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函数,继续路由跳转流程
            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变量中,并调用next函数,中断路由跳转流程
            error = isError(reason)
              ? reason
              : new Error(msg)
            next(error)
          }
        })

        let res
        try {
          res = def(resolve, reject)  // 尝试加载组件
        } catch (e) {
          reject(e)   // 出现错误则调用reject函数
        }
        // 处理加载成功后返回的结果,如果返回的结果是一个Promise,则通过其then方法注册组件加载成功和
        // 失败的回调函数。否则,如果返回结果包含了component属性,则尝试通过component.then方法注册
        // 组件加载成功和失败的回调函数      
        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()  // 不存在需要异步加载的组件,则直接调用next函数,继续路由跳转流程
  }
}

        resolveAsyncComponents返回的是一个导航守卫函数,有标准的to、from、next参数。它的内部实现很简单,利用了flatMapComponents方法从matched中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和我们之前分析 Vue 加载异步组件很类似,加载成功后会执行 match.components[key] = resolvedDef 把解析好的异步组件放到对应的components上,并且执行next函数。 这样在resolveAsyncComponents(activated)解析完所有激活的异步组件后,我们就可以拿到这一次所有激活的组件。这样我们在做完这 5 步后又做了一些事情:

runQueue(queue, iterator, () => {  // 参数:队列queue、迭代器iterator和一个回调函数
  const postEnterCbs = []   // 空数组postEnterCbs,用于保存进入导航守卫后的回调函数
  const isValid = () => this.current === route  // isValid的箭头函数,用于判断当前路由是否和目标路由相同
  const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)  // 据激活的组件和isValid函数提取出进入导航守卫
  const queue = enterGuards.concat(this.router.resolveHooks)  //  将进入导航守卫和路由解析钩子合并成一个新的队列
  **// 递归调用runQueue函数,使用递归的方式来依次执行队列中的任务**
  runQueue(queue, iterator, () => {  
    if (this.pending !== route) {  // 当前的待处理路由不等于目标路由,则调用abort()函数中断路由跳转流程
      return abort()  
    }
    this.pending = null  // 将待处理路由设为null,表示路由跳转已完成
    onComplete(route)  // 执行一些完成后的操作
    if (this.router.app) {  // 如果存在this.router.app(Vue应用实例)
      this.router.app.$nextTick(() => {  // 下一个tick中执行postEnterCbs数组中的每个回调函数
        postEnterCbs.forEach(cb => { cb() })
      })
    }
  })
})
  1. 在被激活的组件里调用beforeRouteEnter。
  2. 调用全局的beforeResolve守卫;
  3. 调用全局的afterEach钩子;

        第六步 有这些相关的逻辑:

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)
        })
      }
    })
  }
}
// 用于轮询检查路由实例是否存在,如果存在则执行回调函数,否则延迟16毫秒后再次轮询
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)
  }
}

        extractEnterGuards函数的实现也是利用了extractGuards方法提取组件中的beforeRouteEnter导航钩子函数,和之前不同的是bind方法的不同。文档中特意强调了beforeRouteEnter钩子函数中是拿不到组件实例的,因为当守卫执行前,组件实例还没被创建,但是我们可以通过传一个回调给next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:

beforeRouteEnter (to, from, next) {  // 守卫函数,用于在路由进入前执行一些逻辑
  next(vm => {   // to是即将进入的路由对象。from是即将离开的路由对象。next是一个回调函数,用于控制路由的进入行为
    // 通过 `vm` 访问组件实例
  })
}

        来看一下这是具体实现的。在bindEnterGuard函数中,返回的是routeEnterGuard函数,所以在执行iterator中的hook函数的时候,就相当于执行routeEnterGuard函数,那么就会执行我们定义的导航守卫guard函数,并且当这个回调函数执行的时候,首先执行next函数rersolve当前导航钩子,然后把回调函数的参数,它也是一个回调函数用cbs收集起来,其实就是收集到外面定义的postEnterCbs中,然后在最后会执行:

if (this.router.app) {
  this.router.app.$nextTick(() => {  // 判断this.router.app 是否存在,this.router是路由对象,app是Vue实例(存在则表示路由实例已被创建)
    postEnterCbs.forEach(cb => { cb() })  // 在根路由组件重新渲染后,遍历postEnterCbs执行回调
  })
}

        在根路由组件重新渲染后,遍历postEnterCbs执行回调,每一个回调执行的时候,其实是执行poll(cb, match.instances, key, isValid)方法,因为考虑到一些了路由组件被套transition组件在一些缓动模式(?)下不一定能拿到实例,所以用一个轮询方法不断去判断,直到能获取到组件实例,再去调用 cb,并把组件实例作为参数传入,这就是我们在回调函数中能拿到组件实例的原因。 第七步 获取this.router.resolveHooks,这个和this.router.beforeHooks的获取类似,在我们的VueRouter类中定义了beforeResolve方法:

/**
* 方法接受一个函数 fn 作为参数,并将其通过 registerHook 函数注册到 resolveHooks 数组中。
* registerHook 函数的作用是将传入的函数 fn 注册到指定的数组中,然后返回这个函数。在 Vue Router 中,resolveHooks 是一个数组,用于存储注册的 beforeResolve 守卫函数。
* 通过调用 beforeResolve 方法,可以在路由解析之前执行一些逻辑。这个守卫函数会在路由解析过程中被调用,可以用来进行一些异步操作或者在路由解析之前进行一些检查。
* 注意,beforeResolve 方法返回的是注册的守卫函数本身,这样可以方便链式调用其他的守卫方法。
*/
beforeResolve (fn: Function): Function {   // 注册 beforeResolve 守卫
  return registerHook(this.resolveHooks, fn)
}

        当用户使用router.beforeResolve注册了一个全局守卫,就会往router.resolveHooks添加一个钩子函数,这样this.router.resolveHooks获取的就是用户注册的全局beforeResolve守卫。 第八步 在最后执行了onComplete(route)后,会执行this.updateRoute(route)方法:

updateRoute (route: Route) {  // 更新当前路由,参数表示要更新的新路由对象
  const prev = this.current   // 将当前路由对象保存到prev变量中
  this.current = route    // 新的路由对象赋值给 this.current
  this.cb && this.cb(route)  // this.cb存在,则调用回调函数this.cb(route)将新路由对象作为参数传递给回调函数。
  this.router.afterHooks.forEach(hook => {  // 遍历this.router.afterHooks数组,执行每个注册的后置守卫函数
    hook && hook(route, prev)
  })
}

        同样在我们的 VueRouter 类中定义了 afterEach 方法:

afterEach (fn: Function): Function {
  return registerHook(**this.afterHooks**, fn)
}

        当用户使用router.afterEach注册了一个全局守卫,就会往router.afterHooks添加一个钩子函数,这样 this.router.afterHooks获取的就是用户注册的全局afterHooks守卫。 那么至此我们把所有导航守卫的执行分析完毕了,我们知道路由切换除了执行这些钩子函数,从表象上有 2 个地方会发生变化,一个是url发生变化,一个是组件发生变化。接下来我们分别介绍这两块的实现原理

4.2 url

        当我们点击router-link的时候,实际上最终会执行router.push,如下

/**
* **push 方法进行路由跳转**
* location:表示要跳转的目标位置。它可以是一个字符串路径,也可以是一个描述路由的对象。
* onComplete(可选):是一个回调函数,在路由跳转完成后被调用。
* onAbort(可选):是一个回调函数,在路由跳转被中断或取消时被调用
*/
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)   // this.history.push方法,将目标位置、完成回调和中断回调作为参数传递给路由的 push 方法
}

        this.history.push这个函数是子类实现的,不同模式下该函数的实现略有不同,我们来看一下平时使用比较多的hash模式该函数的实现,在 src/history/hash.js 中:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this  // 解构赋值将当前路由对象的current属性保存到变量fromRoute中。这个属性表示当前路由状态
  this.transitionTo(location, route => {  // 传入目标位置location和回调函数(在路由切换完成后被调用)
    pushHash(route.fullPath)  // 将新路由的完整路径推入浏览器的历史记录栈中,以便支持浏览器的前进和后退操作
    handleScroll(this.router, route, fromRoute, false) // 控制页面滚动行为
    onComplete && onComplete(route)  // 新的路由对象作为参数传入
  }, onAbort)
}

        push函数会先执行this.transitionTo做路径切换,在切换完成的回调函数中,执行pushHash 函数:

// 用于在支持HTML5 History API的浏览器中,通过history.pushState方法来推入新的路由路径;在不支持的情况下,通过设置window.location.hash 来实现路由路径的变更
function pushHash (path) {
  if (supportsPushState) {  // 浏览器支持HTML5 History API(即 supportsPushState 为真)
    pushState(getUrl(path))  // 获取当前完整的url,可理解为base(window.location.href去除'#'及之后部分)+patch
  } else {
    window.location.hash = path   // 直接将路径写入window.location.hash中
  }
}

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

//检测当前浏览器是否支持HTML5 History API 中的 pushState 方法 
export const supportsPushState = inBrowser && (function () {  
  const ua = window.navigator.userAgent  // 获取当前浏览器的用户代理字符串
  // 判断排除一些不支持pushState方法的特定情况,比如在某些旧版本的Android和iOS上,或者是某些不支持pushState的特定浏览器(如旧版本的Mobile Safari和Windows Phone浏览器)
  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
  }
  // 通过检查window.history和pushState方法是否存在来确定当前浏览器是否支持HTML5 History API中的 pushState
  return window.history && 'pushState' in window.history
})()

        如果支持的话,则获取当前完整的url,执行pushState方法:

// 将新的状态推入浏览器的历史记录栈
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()  // 存当前页面的滚动位置
  const history = window.history  // 获取浏览器的 history 对象
  try {  // 尝试使用history.pushState或history.replaceState方法来推入新的状态
    if (replace) {
      history.replaceState({ key: _key }, '', url)  // 参数:状态对象、标题(在这里为空字符串)、新的 URL
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {   // 通过window.location.replace或window.location.assign方法来进行URL的重定向,实现页面的跳转
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

        pushState会调用浏览器原生的history的pushState接口或者replaceState接口,更新浏览器的url地址,并把当前url压入历史栈中。 然后在history的初始化中,会设置一个监听器,监听历史栈的变化:

// 设置路由变化时的监听器
setupListeners () {
  const router = this.router  // 从当前对象中获取 router 和 router.options.scrollBehavio
  const expectScroll = router.options.scrollBehavior
  const supportsScroll = supportsPushState && expectScroll  // 判断是否支持使用HTML5 History API 中的 pushState 方法以及是否需要处理滚动行为

  if (supportsScroll) {  // 是否调用setupScroll()方法来设置滚动行为的监听器
    setupScroll()
  }
  // 事件监听器,根据浏览器是否支持**pushState**决定监听popstate事件或hashchange事件
  window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
    // 当路由发生变化时,执行回调函数
    const current = this.current
    if (!**ensureSlash()**) {  // 检查当前路径是否确保以斜杠结尾,如果不是则返回
      return
    }
    this.transitionTo(getHash(), route => {  // 执行路由的过渡动作
      if (supportsScroll) {  // 断是否支持滚动行为 (supportsScroll)
        handleScroll(this.router, route, current, true)
      }
      if (!supportsPushState) {  // 不支持pushState,则调用 replaceHash(route.fullPath) 方法来更新 hash 值,实现路由的变更
        replaceHash(route.fullPath)
      }
    })
  })
}

        当点击浏览器返回按钮的时候,如果已经有url被压入历史栈,则会触发popstate事件,然后拿到当前要跳转的hash,执行transtionTo方法做一次路径转换。 在使用Vue-Router开发项目的时候,打开调试页面http://localhost:8080后会自动把url修改为 http://localhost:8080/#/,这是怎么做到呢?原来在实例化HashHistory的时候,构造函数会执行 ensureSlash() 方法:

// 用于处理URL中的哈希部分,并确保哈希值始终以斜杠开头
function ensureSlash (): boolean {  // 用于确保哈希值以斜杠开头
  const path = getHash()  // 获取当前的哈希值
  if (path.charAt(0) === '/') {  // 第一个字符是斜杠,则返回 true
    return true
  }
  replaceHash('/' + path)  //在当前路径前添加斜杠,并将页面的哈希值更新为带有斜杠的新路径
  return false
}
// 获取当前页面的哈希值。它通过获取window.location.href,找到哈希符号 # 的索引,并返回哈希符号后的字符串
export function getHash (): string { // 避免了直接使用 window.location.hash,因为在某些浏览器中,这个属性的行为可能不一致
  // 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)
}
// 用于根据给定的路径生成完整的 URL
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,计算出来的新的 url为 http://localhost:8080/#/, 最终会执行pushState(url, true),这就是url会改变的原因。

4.3 组件

        路由最终的渲染离不开组件,Vue-Router内置了<router-view>组件,它的定义在 src/components/view.js 中。

// 函数式组件(functional component)RouterView,用于渲染路由视图
export default {
  name: 'RouterView',  // 组件的名称为 'RouterView'
  functional: true,  // 声明一个函数式组件,函数式组件没有响应式数据,也没有实例,它们只接受props并返回一个 VNode
  props: {  // 组件接受一个名为 name 的 prop,类型为字符串,默认值为 'default'
    name: {
      type: String,
      default: 'default'
    }
  },
  // render函数内部
  render (_, { props, children, parent, data }) {
    data.routerView = true  // 将routerView设置为true,以标识这是一个路由视图
    const h = parent.$createElement  // 获取父组件的$createElement方法
    const name = props.name   // 获取props中的name属性
    const route = parent.$route  // 获取父组件的路由信息$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})  // 创建一个缓存对象cache,用于缓存组件实例

    let depth = 0    
    let inactive = false
    // 通过遍历父组件链,计算当前组件在路由中的深度depth,并检查父组件是否处于非活动状态inactive
    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]  // 否则,从当前路由的matched中获取当前深度的路由记录,并获取其中对应的组件
    if (!matched) { // 没有匹配的路由记录,则将缓存中对应的组件实例置为null,并返回空的VNode
      cache[name] = null
      return h()
    }
    // 否则,将当前匹配的组件缓存起来,并设置一个函数registerRouteInstance,用于注册路由实例
    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.prepatch 钩子,在组件更新前更新路由实例
    (data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    // 解析并传递props,并处理额外的属性
    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]
        }
      }
    }
    // 最后,使用h方法创建并返回组件的VNode
    return h(component, data, children)
  }
}

        <router-view>是一个functional组件,它的渲染也是依赖render函数,那么<router-view>具体应该渲染什么组件呢?

        首先, 获取当前的路径:

const route = parent.$route

        在src/install.js 中,我们给Vue的原型上定义了$route

Object.defineProperty(Vue.prototype, '$route', {  // 定义Vue原型(Vue.prototype)上的$route属性
  /**
  * this._routerRoot._route是Vue Router内部的路由对象,包含了当前路由的信息。通过将$route定义为
  * 原型属性,并且使用getter来访问内部的_route对象,我们可以在Vue应用的任何组件中通过this.$route 
  * 来访问当前路由的信息。
  */
  get () { return this._routerRoot._route }
})

        然后,在VueRouter的实例执行router.init方法的时候,会执行如下逻辑监听路由变化,其定义在src/index.js 中:

history.listen(route => {  // 当路由发生变化时,会执行回调函数并将新的路由信息作为参数传递给回调函数
  this.apps.forEach((app) => {  // 回调函数中,通过遍历this.apps数组(假设this指向Vue实例)
    app._route = route  // 将每个应用的 _route 属性更新为新的路由信息
  })
})

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

listen (cb: Function) {  // listen 的方法,它接受一个cb参数,类型为函数。
  this.cb = cb  // 在方法内部,它将传入的函数赋值给对象的cb属性
}

        然后,在updateRoute的时候执行this.cb:

updateRoute (route: Route) {
  //. ..
  this.current = route  //传入的route赋值给对象的current属性
  this.cb && this.cb(route)  // 检查cb是否存在,如果存在则调用cb回调函数,并将route作为参数传递进去
  // ...
}

        也就是我们执行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
}
// 代码根据计算出的深度(即depth变量)在当前路由的 matched 数组中找到相应深度的路由记录,并从中取出对应的组件信息
const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]

        parent._routerRoot表示的是根Vue实例,那么这个循环就是从当前的<router-view>的父节点向上找,一直找到根Vue实例,在这个过程,如果碰到了父节点也是<router-view>的时候,说明<router-view>有嵌套的情况,depth++。遍历完成后,根据当前线路匹配的路径和depth找到对应的RouteRecord,进而找到该渲染的组件。 除了找到了应该渲染的组件,还定义了一个注册路由实例的方法:

data.registerRouteInstance = (vm, val) => {     
  const current = matched.instances[name]  // 从matched.instances中获取当前的实例current
  if (
    (val && current !== vm) ||
    (!val && current === vm)
  ) {
  //  val存在且与当前实例vm不相等,或者val不存在且当前实例current等于vm,则将matched.instances[name] 设置为 val
    matched.instances[name] = val
  }
}

        给vnode的data定义了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)  // 在组件实例销毁时将其从路由实例中注销
  }
})

        在混入的beforeCreate钩子函数中,会执行registerInstance方法,进而执行render函数中定义的registerRouteInstance方法,从而给matched.instances[name]赋值当前组件的vm实例。 render函数的最后根据component渲染出对应的组件vonde:

return h(component, data, children)

        那么当我们执行transitionTo来更改路由线路后,组件是如何重新渲染的呢?在我们混入的beforeCreate钩子函数中有这么一段逻辑:

Vue.mixin({
  beforeCreate () {
    // 通过检查this.$options.route 是否被定义来确定当前组件是否是一个Vue Router的根实例
    if (isDef(this.$options.router)) {
      // 使用Vue.util.defineReactive方法来定义一个响应式属性_route,并将其初始化为当前路由的状态,即this._router.history.current
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    }
    // ...
  }
})

        由于我们把根Vue实例的_route属性定义成响应式的,我们在每个<router-view>执行render函数的时候,都会访问parent.$route,如我们之前分析会访问this._routerRoot._route,触发了它的getter,相当于<router-view>对它有依赖,然后再执行完transitionTo后,修改app._route的时候,又触发了setter,因此会通知<router-view>的渲染watcher更新,重新渲染组件。

        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属性都不需要写(基路径)了。

        那么接下来我们就来分析它的实现,它的定义在src/components/link.js 中:

export default {
  name: 'RouterLink',  // 定义了一个名为 RouterLink 的 Vue 组件,用于生成路由链接
  props: {  // 配置项,例如目标路由路径to、标签类型tag、是否精确匹配exact 等
    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 // 首先通过this.$router和this.$route获取当前的路由和路由器实例
    const current = this.$route**
    // 解析目标路由路径to,获取对应的目标位置、路由信息以及完整的 URL
    const { location, route, href } = router.resolve(this.to, current, this.append)
    // 根据当前路由和目标路由的比较结果,动态生成 classes 对象,包括活动状态和精确活动状态的类名.
    // 根据配置项设置相应的活动类名,如果未配置,则使用默认值。
    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)
    // 定义了事件处理函数handler,用于处理点击事件。根据配置项决定是使用router.replace还是router.push 来更新路由
    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }

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

    const data: any = {
      class: classes
    }
    // 根据标签类型不同分别处理。如果是<a>标签,则设置对应的事件监听和href属性。如果是其他标签,
    // 则通过查找默认插槽中的锚点元素,并动态设置其属性和事件监听
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      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
      }
    }
    // h函数创建虚拟节点,并根据配置渲染对应的标签及属性,包括类名、事件监听和子节点
    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.resolve是VueRouter的实例方法,它的定义在src/index.js 中:

// 解析目标位置(to)到实际的路由和URL
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)  // match() 函数匹配路由,获取目标位置对应的路由信息
  const fullPath = route.redirectedFrom || route.fullPath
  const base = this.history.base
  **const href = createHref(base, fullPath, this.mode)**   // 生成完整的 URL(href)
  return {
    location,
    route,
    href,
    normalizedTo: location,
    resolved: route
  }
}
**// 用于创建 URL
function createHref (base: string, fullPath: string, mode)** {  // 接受基本路径(base)、完整路径(fullPath)、路由模式(mode,比如 hash 模式)等参数
  var path = mode === 'hash' ? '#' + fullPath : fullPath  // 在路径前面添加 # 符号(如果是 hash 模式)
  return base ? cleanPath(base + '/' + path) : path  // 根据基本路径和完整路径生成完整的 URL
}

        它先规范生成目标location,再根据location和match通过this.match方法计算生成目标路径route,然后再根据base、fullPath和this.mode通过createHref方法计算出最终跳转的href。 解析完router获得目标location、route、href后,接下来对exactActiveClass和activeClass做处理,当配置exact为true的时候,只有当目标路径和当前路径完全匹配的时候,会添加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  // 检查是否按下了 Meta、Alt、Ctrl 或 Shift 键
  if (e.defaultPrevented) return  // 检查事件是否已经被阻止了默认行为,如果是则返回 false
  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  // 事件目标有设置 target="_blank" 属性
  }
  if (e.preventDefault) {
    e.preventDefault()  // 如果事件对象有 preventDefault 方法,则调用它来阻止事件的默认行为
  }
  return true
}
// 用于存储事件类型和对应的处理函数
const on = { click: guardEvent }
  if (Array.isArray(this.event)) {  // this.event是一个数组,表示需要绑定多个事件,那么就遍历数组,将每个事件和处理函数关联起来
    this.event.forEach(e => { on[e] = handler })
  } else {
    on[this.event] = handler  // 将单个事件和处理函数关联起来
  }

        最终,会监听点击事件或者其它可以通过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)
}

        实际上就是执行了history的push和replace方法做路由跳转。 最后,判断当前tag是否是<a>标签,<router-link>默认会渲染成<a>标签,当然我们也可以修改tag的 prop 渲染成其他节点,这种情况下会尝试找它子元素的<a>标签,如果有则把事件绑定到<a>标签上并添加href 属性,否则绑定到外层元素本身。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值