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后会修改它们的值。

持续更新中...

相关推荐

  1. VueRouter

    2024-06-09 04:14:04       64 阅读
  2. <span style='color:red;'>VueRouter</span>

    VueRouter

    2024-06-09 04:14:04      35 阅读
  3. SDWebImage分析

    2024-06-09 04:14:04       27 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-09 04:14:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-09 04:14:04       101 阅读
  3. 在Django里面运行非项目文件

    2024-06-09 04:14:04       82 阅读
  4. Python语言-面向对象

    2024-06-09 04:14:04       91 阅读

热门阅读

  1. 7.0 android中Service的基础知识

    2024-06-09 04:14:04       28 阅读
  2. python的sql解析库-sqlparse

    2024-06-09 04:14:04       23 阅读
  3. vite+vue+ts项目中报错解决方案

    2024-06-09 04:14:04       26 阅读
  4. 前端学习笔记

    2024-06-09 04:14:04       31 阅读