图文解读Vue Router源码,保姆级解说

71 阅读7分钟

1 前言

用过Vue的各位小伙伴,想必或多或少都会接触过Vue Router

不知道你们是否也会跟我一样好奇Vue Router是如何实现的呢?

如果是的话,那么就跟随我来一起阅读Vue Router的源码,了解这款官方路由器是如何设计与实现的吧。

提示:本文讲解的是Vue Router3版本的源码。

2 Vue Router

想要读懂Vue Router,我们需要先思考Vue Router提供了哪些什么功能给我们?

接着才会考虑这些功能是如何实现的?然后从代码里找答案。

2.1 Vue Router功能

Vue Router功能.png

回想一下我们使用Vue Router,可以总结出来它其实就提供了两个功能:

  1. 路由组件渲染:通过router-view渲染我们配置的组件。
  2. 路由路径导航:通过router-link或者this.$router的方法进行导航

提前预告一下:

  1. router-view渲染的组件是从this.$router中找到匹配的路由的component里获取的。
  2. router-link的导航功能底层也是调用this.$router的方法,

所以只要搞懂this.$router从何而来,如何实现,也就基本上等于搞懂了Vue Router的工作原理了。

那么接下来就先看看VueRouter类提供了什么东西吧!

2.2 VueRouter类静态属性

VueRouter类静态属性.png

静态属性里最重要的就是install方法,该方法将$router$route注入到Vue的原型上。

inNavigationFailure可以用来判断路由导航过程中抛出的异常,进行个性化处理,但是较少用到。

2.3 VueRouter类的实例属性

Vue Router实例属性.png

VueRouter实例属性中最重要的就是history对象,后面会重点解读。

实例属性中提供了添加导航守卫路由导航路由添加这三类方法,我们主要用到的也就是这些方法。

route-view里会调用VueRoutermatch方法返回匹配到的路由进行渲染。

至此,基本理清楚了路由组件渲染路由路径导航两个功能是从何而来,接下来就是看代码咯~~

2.4 导航守卫

在路由路径导航的时候,会伴随调用很多的导航守卫函数,这也是Vue Router重点功能之一。

由于Vue Router的导航守卫较多,这里列出来导航流程给大家回忆一下,看代码会更有思路一点:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

3 源码文件结构

Vue Router的源码文件结构如下所示: 重点文件我已经用*号标注出来。

src
│  create-matcher.js       // *创建matcher对象,提供match,addRoute等方法
│  create-route-map.js     // *创建routeMap,便于根据名称或路径找到路由记录
│  index.js                // *入口文件
│  install.js              // *vue插件安装
│  router.js               // *VueRouter类定义
│
├─components   // 组件包
│      link.js             // *router-link
│      view.js             // *router-view
│
├─composables  // 函数方法
│      globals.js
│      guards.js
│      index.js
│      useLink.js
│      utils.js
│
├─entries    // 入口
│      cjs.js
│      esm.js
│
├─history      // history相关abstract.js       // node环境的history实现类base.js           // *history基类
│      hash.js           // *hash模式的history实现类
│      html5.js          // *history模式的history实现类
│
└─util         // 工具函数
        async.js
        dom.js
        errors.js
        location.js
        misc.js
        params.js
        path.js
        push-state.js
        query.js
        resolve-components.js
        route.js
        scroll.js
        state-key.js
        warn.js

打开入口文件可以看到其实就是引用router.js

所以我们直接从router.js开始阅读,但是在这之前先看看Vue.use(VueRouter),也就是install.js做了什么。

4 install.js

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

export let _Vue

export function install (Vue) {
  // 确保只注册一次
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  // 将vue实例注册到route实例上
  const registerInstance = (vm, callVal) => {
    // 获取当前组件实例的父虚拟节点(VNode)
    let i = vm.$options._parentVnode
    // 赋值表达式会返回等号右边的值
    // registerRouteInstance是route-view组件注入的方法
    // 作用是:将vue实例注册到route实例上
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    // 给vue组件混入beforeCreate方法
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        // 调用router的init方法,后面会讲到
        this._router.init(this)
        // 设置响应性
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 子组件从父组件中拿到$routers
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 将当前实例注册到路由实例中
      registerInstance(this, this)
    },
    // 取消注册
    destroyed () {
      registerInstance(this)
    }
  })

  // Vue原型增加$router
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  // Vue原型增加$route
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // Vue全局注册两个组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  // 获取Vue的合并策略
  const strats = Vue.config.optionMergeStrategies
  // beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate
  // 与created的合并策略保持一致
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

可以看到,插件安装主要做了四件事:

  1. Vue原型注入$router$route
  2. Vue全局注册组件route-viewroute-link
  3. Vue全组件混入beforeCreate生命周期钩子函数,用于将vue组件实例注册到route实例中。
  4. 设置了beforeRouteEnterbeforeRouteLeavebeforeRouteUpdate钩子函数的合并策略。

5 router.js(重点)

export default class VueRouter {
  // 静态属性
  // vue插件install函数
  static install: () => void
  // 版本
  static version: string
  // 是否跳转导航错误
  static isNavigationFailure: Function
  // 导航错误类型
  static NavigationFailureType: any
  // 起始路径,默认是"/"
  static START_LOCATION: Route

  // Vue根节点
  app: any
  apps: Array<any>
  // 保存new VueRouter是传入的配置
  options: RouterOptions
  // 模式
  mode: string
  // history对象
  history: HashHistory | HTML5History | AbstractHistory
  // 包含这四个函数:match,addRoutes,addRoute,getRoutes
  matcher: Matcher
  // 是否回退history
  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 = []
    // 返回这四个函数:match,addRoutes,addRoute,getRoutes
    // 入参分别是:我们传入的routes和router实例
    this.matcher = createMatcher(options.routes || [], this)

    // 默认hash模式
    let mode = options.mode || 'hash'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    // 不支持history,回退为hash模式
    if (this.fallback) {
      mode = 'hash'
    }
    // 不是浏览器环境,自动切换成abstract
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // 创建history实例
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

  // 实际是调用matcher.match
  // 作用:根据路径返回路由实例
  match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }

  // 获取当前路由,实际是从history中取的
  get currentRoute (): ?Route {
    return this.history && this.history.current
  }

  // 初始化方法,
  // 在组件beforeCreate的时候会调用
  // 入参为当前组件实例
  init (app: any) {
    // 保存到apps数组中
    this.apps.push(app)
    // 设置实例的摧毁钩子函数
    app.$once('hook:destroyed', () => {
      // 清除当前实例
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      if (this.app === app) this.app = this.apps[0] || null
      // 如果实例全部都被清除了,那么也清除history
      if (!this.app) this.history.teardown()
    })

    // 说明之前已经初始化过主应用,
    // 直接返回,不需要设置新的历史记录监听器。
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history
    // 设置history的监听器
    if (history instanceof HTML5History || history instanceof HashHistory) {
      // 如果有配置且支持滚动行为
      // 执行配置的滚动行为操作
      const handleInitialScroll = routeOrError => {
        const from = history.current
        const expectScroll = this.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll

        if (supportsScroll && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from, false)
        }
      }
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      // 第一次加载跳转到首页
      // 设置
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }
    // 给history设置监听事件
    history.listen(route => {
     // 更新组件实例中的route实例
     // 也就是$router
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }

  // 添加前置导航守卫
  beforeEach (fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
  }

  // 添加解析导航守卫
  beforeResolve (fn: Function): Function {
    return registerHook(this.resolveHooks, fn)
  }

  // 添加后置导航守卫
  afterEach (fn: Function): Function {
    return registerHook(this.afterHooks, fn)
  }

  // 给history添加ready回调
  onReady (cb: Function, errorCb?: Function) {
    this.history.onReady(cb, errorCb)
  }

  // 给history添加error回调
  onError (errorCb: Function) {
    this.history.onError(errorCb)
  }

  // 跳转页面
  // 调用的其实是history的push方法
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

  // replcae 页面
  // 调用的其实是history的replace方法
  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
  }

  // 跳转
  // 调用的也是history里的go
  go (n: number) {
    this.history.go(n)
  }

  // 返回上一个页面
  back () {
    this.go(-1)
  }

  // 前进
  forward () {
    this.go(1)
  }

  // 获取匹配路径到的组件
  getMatchedComponents (to?: RawLocation | Route): Array<any> {
    const route: any = 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]
        })
      })
    )
  }

  // 解析路径
  // 主要是返回三个东西
  // location,route,href
  resolve (
    to: RawLocation,
    current?: Route,
    append?: boolean
  ): {
    location: Location,
    route: Route,
    href: string,
    normalizedTo: Location,
    resolved: Route
  } {
    current = current || this.history.current
    const location = normalizeLocation(to, current, append, this)
    const route = this.match(location, current)
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)
    return {
      location,
      route,
      href,
      normalizedTo: location,
      resolved: route
    }
  }

  // 获取所有路由记录
  getRoutes () {
    return this.matcher.getRoutes()
  }

  // 添加路由,调用的是matcher.addRoute
  addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
    this.matcher.addRoute(parentOrRoute, route)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }

  // 批量添加路由,调用的是matcher.addRoutes
  // Vue Router4已废弃该方法
  addRoutes (routes: Array<RouteConfig>) {
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}

// 返回一个函数,用来移除钩子
function registerHook (list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

// 创建href
function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}

// 设置静态属性
VueRouter.install = install
VueRouter.version = '__VERSION__'
VueRouter.isNavigationFailure = isNavigationFailure
VueRouter.NavigationFailureType = NavigationFailureType
VueRouter.START_LOCATION = START

// 浏览器环境下安装VueRouter插件
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

看完VueRouter类的代码,我们会发现其中有两个很重要的东西:

  1. matchermatchaddRoute都是调用matcher里的方法
  2. historypushreplacego等方法以及记录当前路由的current都是从history里获取的。

所以接下来我们就需要看看matcherhistory是如何实现的。

6 create-matcher.js

回顾上文的代码:

 // 入参分别是:我们传入的routes和router实例
 this.matcher = createMatcher(options.routes || [], this)

接下来看createMatcher的源码:

// 可以看出来Matcher返回了四个函数
export type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
  addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
  getRoutes: () => Array<RouteRecord>;
};

// 返回值是4个函数
// 入参是routes和router
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // createRouteMap的作用是根据传入的routes
  // 生成pathList, pathMap, nameMap三个对象
  // pathList:路径数组,按路径优先级排序
  // pathMap:path和RouteRecord的映射
  // nameMap:name和RouteRecord的映射
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  // 返回的第一个函数:addRoutes
  function addRoutes (routes) {
    // 在原有的基础上添加新传入的routes
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

  // 返回的第二个函数:addRoute
  // 如果传了两个参数,会把第二个参数,
  // 的路由添加到第一个参数的路由下
  // 如果只传一个参数,就直接添加单个路由
  function addRoute (parentOrRoute, route) {
    const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
    // 第五个参数代表添加到该路由下
    createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)

    // 如果父路由有别名
    // 则复制一份别名路由
    // 并添加
    if (parent && parent.alias.length) {
      createRouteMap(
        parent.alias.map(alias => ({ path: alias, children: [route] })),
        pathList,
        pathMap,
        nameMap,
        parent
      )
    }
  }

  // 返回的第三个函数:getRoutes
  // 比较简单,返回所有路由
  function getRoutes () {
    return pathList.map(path => pathMap[path])
  }

  // 返回的最后一个函数:match
  // 作用:根据传入的Location返回Route对象
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    // 返回标准化路径
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location
    
    // 有传路由name的情况
    // 从nameMap里找路由
    if (name) {
      // 根据name找到路由记录
      const record = nameMap[name]
      // 如果没找到,直接创建一个路由
      // 因为不然的话就没法跳转
      // 有了路由就可以跳转过去,但是内容是空的
      if (!record) return _createRoute(null, location)
      // 找出动态路由的必填参数
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)
      // 如果params不是对象,初始化为空对象
      if (typeof location.params !== 'object') {
        location.params = {}
      }
      // 如果有传currentRoute
      // 从currentRoute获取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]
          }
        }
      }
      // 填充动态参数
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      // 创建路由
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
      // 根据路径匹配
      location.params = {}
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        // 如果根据路径可以匹配的到就创建路由
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // 没匹配到,也要创建一个新的路由
    // 不然就跳转不过去了
    return _createRoute(null, location)
  }

  // 创建路由的方法
  function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    // 实际上调用的是该方法
    return createRoute(record, location, redirectedFrom, router)
  }

  // 最后返回这四个函数
  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

再补充一下_createRoute里调用的createRoute方法:

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}
  // 可以看到每次match到的路由是在这里创建的
  // 有些信息是来自location,
  // 有些信息是来自record
  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),
    // 我们打印this.$route可以看到
    // 里面有matched数组,
    // 就是匹配到的一个个嵌套路由
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}

总结一下,create-matcher其实内容并不复杂,match函数的作用是根据传入的location返回一个路由记录。

另外三个方法都是来自于createRouteMap这个方法,所以接下来我们再来看一下它做了什么吧~

7 create-route-map.js

// 该方法支持五个参数
export function createRouteMap (
  routes: Array<RouteConfig>,      //要添加的路由
  oldPathList?: Array<string>,     //路径数组
  oldPathMap?: Dictionary<RouteRecord>, //路径与路由记录的映射
  oldNameMap?: Dictionary<RouteRecord>, //名称与路由记录的映射
  parentRoute?: RouteRecord       //父路由
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // 用来控制路径匹配优先级
  // 类似这样的数组['/dashboard','/about']
  const pathList: Array<string> = oldPathList || []
  // 路径与路由记录的映射
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // 名称与路由记录的映射
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
    // 遍历,添加route,会发现
    // 所有的逻辑都在这个方法里
    // 作用就是获取route数据,
    // 保存到pathList, pathMap, nameMap这三个对象里
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })

  // 如果存在通配符路由*,将它移动到最后面
  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
  }
}

function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  const { path, name } = route
  
  const pathToRegexpOptions = route.pathToRegexpOptions || {}
  // 标准化路径
  // 比如
  // 去除路径末尾的斜杆
  // 连续的斜杆替换为1个
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  // 是否大小写敏感
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  // 要保存在nameMap和pathMap里的路由记录
  const record: RouteRecord = {
    path: normalizedPath,
    // 解析路由正则表达,
    // 底层使用的是'path-to-regexp'插件
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    // component都会被放到components里,默认是default
    components: route.components || { default: route.component },
    // 说明别名可以设置为数组,也就是可以设置多个
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    // 保存路由对应组件实例
    // 在history里会用到
    instances: {},
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    // 如果设置了components,
    // 那么props也要用对象的形式设置
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }

  if (route.children) {
    // 递归添加子路由
    // 父路由有别名情况添加子路由,
    // 实际匹配的时候还是用原有名称
    // matchAs含义就是实际匹配用的路径
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 如果路径不存在,添加
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  // 如果配置了路由别名
  if (route.alias !== undefined) {
    // 转为数组
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      // 添加别名路由
      // 但是实际匹配(matchAs)还是用原路由
      // 最后一个参数就是matchAs
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' 
      )
    }
  }

  // 如果路由配置了名称
  // 并不会再添加,而是用原来的
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } 
  }
}

// 路径转为正则表达是对象
// 使用'path-to-regexp'插件转换
function compileRouteRegex (
  path: string,
  pathToRegexpOptions: PathToRegexpOptions
): RouteRegExp {
  const regex = Regexp(path, [], pathToRegexpOptions)
  // regex.keys里保存了动态参数的信息
  if (process.env.NODE_ENV !== 'production') {
    const keys: any = Object.create(null)
    regex.keys.forEach(key => {
      warn(
        !keys[key.name],
        `Duplicate param keys in route with path: "${path}"`
      )
      keys[key.name] = true
    })
  }
  return regex
}

// 标准化path
function normalizePath (
  path: string,
  parent?: RouteRecord,
  strict?: boolean
): string {
  // 非严格模式下,去除path末尾的/返回
  if (!strict) path = path.replace(/\/$/, '')
  if (path[0] === '/') return path
  if (parent == null) return path
  // 严格模式下,且路径不是/开头,会拼接父路由的path
  // cleanPath是用来清除连续多个斜杆为一个斜杆
  return cleanPath(`${parent.path}/${path}`)
}

至此,我们理清楚了matcher里的addRoute是如何实现的了!!

各位看官,如果能看到这里,可以给自己鼓个掌,真的很不容易!

8 history

理清楚了matcher中的函数是怎么来的,接下来就剩下history是如何实现的了。

由于Vue Router提供了三种history,分别是hash,history,abstract,那么肯定是有三种具体的实现方法,就像axios底层发送请求提供了xhr,http两种模式。

但是代码肯定不可能写三套,分析history的作用,可以发现无论是哪一种history,我们都是使用它进行页面跳转,调用push,replace,go这样的方法。

另外由于Vue Router提供了多种导航守卫,导航守卫肯定是和页面跳转紧密关联。

push函数为例,我们在调用push之前要调用前置导航守卫,push之后要调用后置导航守卫,当然还有其他的导航守卫,这些导航守卫的调用无论是哪一类history,逻辑都是一样的。

所以只要定义一个公共的基类,此处叫做base类,实现可以共用调用导航守卫的逻辑,再让hash,history,abstract各自实现push,replace,go这些方法就可以了。

所以先让我们看一下base.js的实现吧~

8.1 base.js

export class History {
  // 路由器
  router: Router
  // 基础路径
  base: string
  // 当前路由对象
  current: Route
  // 跳转中的路由
  // 如果有多个跳转中的路由
  // 会把上一个跳转中的取消掉
  // 这就是cancelled导航故障
  pending: ?Route
  cb: (r: Route) => void
  // 是否准备完毕
  ready: boolean
  // 准备完毕回调
  readyCbs: Array<Function>
  // 准备异常回调
  readyErrorCbs: Array<Function>
  // history异常回调
  errorCbs: Array<Function>
  // 监听器
  listeners: Array<Function>
  // 清空监听器
  cleanupListeners: Function

  // 实现类需要实现以下方法
  // 因为hash,history的跳转原理不一样
  // 这些方法交给具体的history来实现
  +go: (n: number) => void
  +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
  +replace: (
    loc: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) => void
  +ensureURL: (push?: boolean) => void
  +getCurrentLocation: () => string
  +setupListeners: Function

  constructor (router: Router, base: ?string) {
    // 路由器
    this.router = router
    // 基础路径
    this.base = normalizeBase(base)
    // 当前路径,初始为"/"
    this.current = START
    // 初始化基础的信息
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
    this.listeners = []
  }

  // 设置回调函数
  listen (cb: Function) {
    this.cb = cb
  }

  // 添加ready回调函数
  onReady (cb: Function, errorCb: ?Function) {
    // 如果history状态已经ready
    if (this.ready) {
      // 执行传入的回调函数
      cb()
    } else {
      // 否则将回调函数保存到数组中
      this.readyCbs.push(cb)
      if (errorCb) {
        this.readyErrorCbs.push(errorCb)
      }
    }
  }

  // 添加错误回调函数
  onError (errorCb: Function) {
    this.errorCbs.push(errorCb)
  }

  // 跳转方法,重点
  // 传参为:
  // 1.要跳转的路径
  // 2.跳转成功回调函数
  // 3.跳转失败回调函数
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    try {
      // 获取匹配到的路由,
      // 前面讲到的match方法
      route = this.router.match(location, this.current)
    } catch (e) {
      // 抛出错误前执行异常回调
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      // 继续抛出
      throw e
    }
    // 跳转后current就是prev了
    // 所以命名prev记录
    const prev = this.current
    // 调用确认跳转方法,
    // 此处又嵌套了一层
    // 该方法也是三个参数:
    // 1.跳转到的路由
    // 2.跳转成功的回调
    // 3.跳转失败的回调
    // 先看看这两个回调函数做了什么
    this.confirmTransition(
      // 跳转到的路由
      route,
      // 跳转成功的回调
      () => {
        // 更新当前路由
        this.updateRoute(route)
        // 执行transitionTo传入的回调函数
        onComplete && onComplete(route)
        // 确保路由正确
        this.ensureURL()
        // 执行全局后置导航守卫
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })
        // 只会执行一次
        if (!this.ready) {
          // 修改ready状态
          this.ready = true
          // 执行ready回调函数
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        // 调用transitionTo传入的
        // 异常回调函数
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          // 切换状态并且执行ready异常回调函数
          if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
            this.ready = true
            this.readyErrorCbs.forEach(cb => {
              cb(err)
            })
          }
        }
      }
    )
  }

  // 真正跳转的方法实现
  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    // 记录当前路由
    const current = this.current
    // 记录当前正在跳转的路由
    this.pending = route
    // 执行错误回调
    const abort = err => {
      // 非导航故障,执行
      // errorCbs回调函数
      if (!isNavigationFailure(err) && isError(err)) {
        if (this.errorCbs.length) {
          this.errorCbs.forEach(cb => {
            cb(err)
          })
        }
      }
      // 执行传入的异常回调函数
      onAbort && onAbort(err)
    }
    // 获取路由深度
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    // 判断是否同一个路由
    if (
      isSameRoute(route, current) &&
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      if (route.hash) {
        // 如果有hash,执行滚动行为
        handleScroll(this.router, current, route, false)
      }
      // 返回duplicated导航故障
      return abort(createNavigationDuplicatedError(current, route))
    }

    // 获取更新、激活、失活的路由
    // 假设有路由嵌套两层,
    // 从/app/route1跳转到/app/route2
    // updated     就是/app对应的路由
    // deactivated 就是/app/route1对应的路由
    // activated   就是/app/route2对应的路由
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )

    // 拼接回调执行队列
    const queue: Array<?NavigationGuard> = [].concat(
      // 组件内leave守卫
      extractLeaveGuards(deactivated),
      // 全局前置守卫
      this.router.beforeHooks,
      // 组件内update守卫
      extractUpdateHooks(updated),
      // 路由独享守卫
      activated.map(m => m.beforeEnter),
      // 解析异步组件
      // 这样在路由的components就可以取到组件
      resolveAsyncComponents(activated)
    )

    // 第一个参数是路由守卫函数
    // 第二个参数就是next函数
    const iterator = (hook: NavigationGuard, next) => {
      // 一个路由还没导航完,又有新的路由导航,
      // 执行上面定义的错误回调,会抛出导航故障:cancelled
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      try {
        // 执行守卫函数
        // to,from,next方法
        // 注意看next的实现
        hook(route, current, (to: any) => {
          // next(false)
          // 执行上面定义的错误回调,会抛出abort导航故障
          if (to === false) {
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) {
            // next(Error)
            // 执行上面定义的错误回调
            this.ensureURL(true)
            abort(to)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            // next('/') or next({ path: '/' }) -> redirect
            // 执行上面定义的错误回调,会抛出重定向导航故障
            abort(createNavigationRedirectedError(current, route))
            // 抛出异常后会继续跳转到新的路径
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // 调用iterator中传入的next方法
            // 作用其实是索引加1,
            // 接着执行下一个守卫函数
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }

    // 顺序执行队列(queue)里的守卫函数
    // 全部执行完后执行第三个参数传入的回调函数
    runQueue(queue, iterator, () => {
      // 守卫函数全部执行完
      // 这个时候异步组件都已经加载好了
      // 提取activated组件内的beforeRouteEnter守卫函数
      const enterGuards = extractEnterGuards(activated)
      // 拼接上全局解析守卫
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        // 调用完组件内的beforeRouteEnter之后
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null
        // confirmTransition完成的回调函数
        onComplete(route)
      })
    })
  }
  // 如果看到这里还没迷糊的同学
  // 可能会发现我看了半天,执行了很多守卫,
  // 但是没看到页面跳转(push)
  // 这是因为页面跳转是在onComplete回调函数里传进来的

  // 更新当前路由
  // 并执行监听函数
  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }

  // 默认是空的
  setupListeners () {
    // Default implementation is empty
  }

  // 清除函数
  teardown () {
    // 监听函数全部执行一遍
    this.listeners.forEach(cleanupListener => {
      cleanupListener()
    })
    // 清空监听函数
    this.listeners = []
    // 还原初始设置
    this.current = START
    this.pending = null
  }
}

function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  // updated的路由
  updated: Array<RouteRecord>,
  // activated的路由
  activated: Array<RouteRecord>,
  // deactivated的路由
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}

// 提取组件内部的守卫
// beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate
// 在这些守卫函数里this指向当前组件
// 是因为第三个参数bind进行了绑定
function extractGuards (
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  // 扁平化多层组件,返回执行回调函数后的结果
  // def:组件,instance:vue实例,match:路由记录,key:路由组件名称
  const guards = flatMapComponents(records, (def, instance, match, key) => {
    // 获取组件里的守卫函数,
    // def是组件,name是守卫函数名称
    const guard = extractGuard(def, name)
    if (guard) {
      // 给守卫函数绑定this
      return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  // 多层数组扁平化
  return flatten(reverse ? guards.reverse() : guards)
}

function extractGuard (
  def: Object | Function,
  key: string
): NavigationGuard | Array<NavigationGuard> {
  if (typeof def !== 'function') {
    def = _Vue.extend(def)
  }
  // 返回对应的守卫函数
  return def.options[key]
}

// 给守卫绑定this为vue组件实例
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
  if (instance) {
    return function boundRouteGuard () {
      return guard.apply(instance, arguments)
    }
  }
}

// 提取beforeRouteLeave函数
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  // 最后一个参数为false,
  // 将所有组件反序排列
  // 获取deactivated路由里的所有beforeRouteLeave守卫函数
  // 并且给函数绑定this指向组件实例
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

// 提取beforeRouteUpdate函数
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

// 提取beforeRouteEnter函数
function extractEnterGuards (
  activated: Array<RouteRecord>
): Array<?Function> {
  return extractGuards(
    activated,
    'beforeRouteEnter',
    (guard, _, match, key) => {
      return bindEnterGuard(guard, match, key)
    }
  )
}

// beforeRouteEnter守卫里,
// 组件实例还没加载,所以this取不到
// 用回调的方式
function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string
): NavigationGuard {
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      if (typeof cb === 'function') {
        if (!match.enteredCbs[key]) {
          match.enteredCbs[key] = []
        }
        match.enteredCbs[key].push(cb)
      }
      next(cb)
    })
  }
}

8.2 hash.js

最后以hash模式的history为例子,看它的具体实现:

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    // 调用base的构造函数
    super(router, base)
    // 补充hash模式中间的#号
    if (fallback && checkFallback(this.base)) {
      return
    }
    // 调整路径的斜杆
    ensureSlash()
  }

  // 构建初始的listeners
  setupListeners () {
    // 已经有了就不再创建
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    // 是否支持滚动条
    if (supportsScroll) {
      // 添加设置滚动行为的回调
      this.listeners.push(setupScroll())
    }

    // 处理路由导航
    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    // 判断是监听history变化还是hash变化
    // 添加window的原生监听事件
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    // 移除window监听事件
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      // 成功回调
      route => {
        // 处理完各种导航守卫后,
        // 页面调整
        pushHash(route.fullPath)
        // 处理滚动行为
        handleScroll(this.router, route, fromRoute, false)
        // 执行用户传入的成功回调
        onComplete && onComplete(route)
      },
      // 执行用户传入的失败回调
      onAbort
    )
  }

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

  go (n: number) {
    window.history.go(n)
  }

  // 确保url正确
  // hash和fullPath要一致
  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }

  // 获取当前Location
  getCurrentLocation () {
    return getHash()
  }
}

// 添加hash模式路径的#
function checkFallback (base) {
  const location = getLocation(base)
  if (!/^\/#/.test(location)) {
    window.location.replace(cleanPath(base + '/#' + location))
    return true
  }
}

// 调整路径的斜杆
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

// 获取url#之后的内容
export function getHash (): string {
  let href = window.location.href
  const index = href.indexOf('#')
  if (index < 0) return ''
  href = href.slice(index + 1)
  return href
}

// 根据path返回完整的url
// /dashboard => https://test.xx.com/xxx/#/dashboard
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

// 优先使用history.pushState
// 这个是浏览器的原生方法
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

// 优先使用history.replaceState
// 这个是浏览器的原生方法
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

至此,history的实现也讲解结束!!!!

9 结束

本文篇幅很长,本来想要拆成多篇来写,但是感觉逻辑关系十分紧密,不好拆分,就干脆一篇讲完。

还剩下router-viewrouter-link的实现还没讲到,会在下一篇文章中讲解。

如果您觉得本文对您有所帮助,记得点赞、收藏、关注支持一下,我会分享更多的源码解读文章。

您也可以在本专栏查看其他源码阅读的文章,比如vuexaxiosdayjs