vue-router3 源码简略分析

442 阅读5分钟

从官网文档开始

vue-router 官方文档中,基本用法如下

html:

// html
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>

javascript:

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

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

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

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

从以上代码我们大致可以猜到两点:

  1. VueRouter的初始化。首先把文档规定的路由配置集合(routes)传入构造类,创建出一个router对象。然后把router对象挂载到全局的Vue中。
  2. RouterLink组件和RouterView组件,两者应该配合完成了一些事情。目前还不知道,猜测可能是RouterLink组件传入要跳转的路由信息,然后通过一个类似全局对象和RouterView通信?

这里就引出了三个个问题:

  1. VueRouter对象在构造过程中,有那些处理?
  2. RouterLink组件和RouterView组件是如何通信的?
  3. 点击RouterLink组件过程中发生了什么?

那么我们就从这两个问题开始我们源码探索之旅。我选取的版本是目前最新的3.4.8

VueRouter构造过程

首先我们也按照官方文档步骤,先从VueRouter构造函数入手,代码如下:

// VueRouter的constructor
constructor (options: RouterOptions = {}) {
    // ...省略,一些属性设置
    // 创建Matcher
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'
    // ...一系列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:
        // ...
    }
  }

眯起眼睛看的话,以上过程主要是创建了一个Matcher对象和History对象。

创建Matcher对象

createMatcher,看名字就知道是用来创建的Matcher对象的,代码如下

function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

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

  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    //...暂时省略
  }

  function redirect (
    record: RouteRecord,
    location: Location
  ): Route {
    //...暂时省略
  }

  function alias (
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {
    //...暂时省略
  }

  function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    //...暂时省略
  }

  return {
    match,
    addRoutes
  }
}

其中的addRoutes函数可能就是文档上提到过的动态添加路由的api,它也是在createMatcher函数中创建的。先来看下createMatcher执行过程中的调用了createRouteMap函数

function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

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

  //... 省略
  return {
    pathList,
    pathMap,
    nameMap
  }
}

createRouteMap对传入的routes参数进行了遍历地调用addRouteRecord。再进入addRouteRecord函数中看一下吧


function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  const { path, name } = route
   
  const record: RouteRecord = {
  	//  ...省略其他属性
    components: route.components || { default: route.component }
  }
  // 遍历孩子节点
  if (route.children) {
    route.children.forEach(child => {
      const childMatchAs = 'XXXX'
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  // 原地在pathList和pathMap添加数据
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
   // 处理alias
  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
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    }
  }
  // 处理有name属性的情况
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    }
  }
}

这里递归调用addRouteRecord。在遍历了当前躺节点的孩子后,又对当前躺传入的参数(与RouteRecord关联的数据集合)进行了原地修改。到此发现没有可以往下进行的地方了,创建Matcher的过程结束。

小结

创建Matcher对象的过程如下图所示

其中涉及到数据类型有Matcher、RouteConfig、RouteRecord。

Matcher类型有两个方法,match和addRoutes

type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
}

RouteConfig就是官方文档上规定的,要传入的routes对象

const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

RouteRecord是从RouteConfig转换过来的,是在组件内部使用的路由数据类型。

总结Matcher的创建过程,就是根据传入的路由配置表信息,生成了内部使用内部使用的路由表。得到了一个matcher对象,它有两个方法,从数据类型上推测这两个方法的作用:

  1. addRoutes方法:动态对路由配置表进行添加,
  2. match方法:根据传入参数,获取Route对象

创建History对象

创建History对象过程还是比较简单的,VueRouter根据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:
    // ...
}

以HashHistory为例,它继承了History类

class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    //...
  }
  // ...
}

在看History类,注意这个START变量,来看看它具体是什么。

export class History {
  // ...
  constructor (router: Router, base: ?string) {
    this.router = router 
    this.base = normalizeBase(base) // 确定basePath
    // start with a route object that stands for "nowhere"
    this.current = START
    // this.xxx = []等等
  }
  //..
}

START变量是在const START = createRoute(null, { path: '/'})这里创建的,createRoute函数看名字,应该是创建供内部使用的Route路由信息的对象。

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

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}

最后返回一个Route对象,到这里History的构造过程就分析完毕了。总结一下,目前这个过程主要是创建了Route对象,保存传入的事件到队列中。

路由对象注入Vue中

上面的过程完毕后,生成了router对象后,接下来就是把路由注入到Vue实例中

const app = new Vue({
  router
}).$mount('#app')

通过学习Vue插件,我们知道上面的代码是通过全局混入来添加组件选项的。代码在install.js中

  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    // vm的父节点的选项中有registerRouteInstance的话,就执行registerRouteInstance(vm, callVal)
  }

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // 访问_route,就是访问router.history.current
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        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 }
  })
  // 注册RouterView和RouterLink组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

beforeCreate回调中,做了两件事情,一个是设置了对Vue根对象设置了router和route属性,并且分别通过Object.defineProperty在Vue的原型中挂载成为routerrouter和route。this._router.init(this)里面是history对象中,注册了一些公共事件。

最后还对RouterView和RouterLink组件进行了注册。下面就这两个组件进行分析

RouterLink组件

RouterLink组件代码如下:

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    // ...
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    …
  }
}

只有一个render函数,处理逻辑应该就放在了这里。

// RouterLink的render函数
render (h: Function) {
    const router = this.$router
    const current = this.$route
    // 这里调用的router的resovle方法有什么作用还不得而知,不过看返回值有location、route,href的话,也能猜到大概了。
    const { location, route, href } = router.resolve(
      this.to,
      current,
      this.append
    )

    /*
       省略部分是对class进行处理
    */
    
    // 处理事件选项
    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location, noop)
        } else {
          router.push(location, noop)
        }
      }
    }
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => {
        on[e] = handler
      })
    } else {
      on[this.event] = handler
    }
    // data是h函数要传入的选项
    const data: any = { class: classes } // 之前省略的class处理结果
    /* 
       处理插槽
    */
    const scopedSlot =
      !this.$scopedSlots.$hasNormal &&
      this.$scopedSlots.default &&
      this.$scopedSlots.default({
        href,
        route,
        navigate: handler,
        isActive: classes[activeClass],
        isExactActive: classes[exactActiveClass]
      })

    if (scopedSlot) {
      if (scopedSlot.length === 1) {
        return scopedSlot[0]
      } else if (scopedSlot.length > 1 || !scopedSlot.length) {
        return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
      }
    }
    /*
       以下代码是合并data选项并传入h函数中,
       其中会有a标签处理:如果当前标签是a标签或者插槽里面有a标签,就以找出他来,并把a标签的选项混入到选项中
      
    */
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
      // find the first <a> child and apply listener and href
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const aData = (a.data = extend({}, a.data))
        aData.on = aData.on || {}
        // transform existing events in both objects into arrays so we can push later
        for (const event in aData.on) {
          const handler = aData.on[event]
          if (event in on) {
            aData.on[event] = Array.isArray(handler) ? handler : [handler]
          }
        }
        // append new listeners for router-link
        for (const event in on) {
          if (event in aData.on) {
            // on[event] is always a function
            aData.on[event].push(on[event])
          } else {
            aData.on[event] = handler
          }
        }

        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        aAttrs.href = href
        aAttrs['aria-current'] = ariaCurrentValue
      } else {
        // 没有a标签的情况,比如自定义的组件
        data.on = on
      }
    }

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

整理上面的代码,主要做的工作是组织h函数用到的data选项参数和插槽。在data选项添加了公共的事件处理,插槽部分混入了前面得到的route等对象。注意在这里调用了router.resovle方法,包括添加的事件函数里面也调用了router.push/replace方法。RouterLink组件可能就要通过这里与Router对象产生联系了。我们先看router.resovle方法吧。

resolve (
  to: RawLocation,
  current?: Route,
  append?: boolean
): {
  location: Location,
  route: Route,
  href: string,
  // for backwards compat
  normalizedTo: Location,
  resolved: Route
} {
  current = current || this.history.current
  const location = normalizeLocation(to, current, append, this)  // Location类型
  const route = this.match(location, current) //  这里的this是Router对象,返回Route类型
  // ...省略
  return {
    location,
    route
    // ...省略
  }
}

回想创建Matcher对象这个部分,这里的router.resovle方法就是调用了我们在VueRouter构造过程中创建了matcher对象中的match方法。

function match (
    raw: RawLocation, // raw就是api文档中要传入的to参数,<router-link to="/foo">Go to Foo</router-link>
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    
    const location = normalizeLocation(raw, currentRoute, false, router) // 得到将来要跳转的相关信息
    const { name } = location
    /*
        下面的nameMap,pathList和pathMap都是之前createMatcher已经创建好的
    */
    // 有name的情况
    if (name) {
      const record = nameMap[name]
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys.filter(key => !key.optional).map(key => key.name) // regex解析参考path-to-regexp库

      if (typeof location.params !== 'object') {
        location.params = {}
      }
      // 把location.params没有但currentRoute.params有的属性,赋值给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]
          }
        }
      }
      location.path = fillParams(record.path, location.params, `named route "${name}"`) 
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
      location.params = {}
      // 遍历保存的路由配置表数据,如果record的regex正则如果能与location.path匹配
      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方法传入了将来跳转的to对象,返回一个Route类型的对象。可以看代码中总会调用_createRoute方法。

  function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

_createRoute会根据情况选择执行redirect或者alias,这两个函数最后都会调用createRoute方法。这个方法前面的创建History对象章节已经提到过,是用来创建Route对象的。

至此RouterLink组件大体上分析完毕,做的工作有:

  1. 添加公共的事件绑定(主要是click事件,绑定的函数会调用router的replace/push方法)
  2. 生成将来要跳转的路由route
  3. 处理插槽slot

RouterView组件

下面我们分析,RouterView组件,它是一个函数式组件,接收的props只有一个name

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
   ...
  }
}

接下来看看render中做了什么

// RouterView render方法
render (_, { props, children, parent, data }) {
    // 使用父类的$createElement方法作为渲染函数,这样组件就可以渲染传入的具名插槽
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route // 这里的$route触发了install.js中的 this._routerRoot._route =》 this._router.history.current
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    /*
        
        检查当前嵌套路由组件相对根组件嵌套的深度,同时检测dom树中是否存在inactive同时已经keep-live状态的组件
        如果存在就渲染它
    */
    // ...省略代码中获取了嵌套路径片段的路由记录的depth
    const matched = route.matched[depth]
    const component = matched && matched.components[name]
    // 没有路由记录,或者没有传组件,直接渲染,并且清空缓存
    if (!matched || !component) {
      cache[name] = null
      return h()
    }
    // 如果有嵌套路由:缓存组件
    cache[name] = { component }
    /*
        省略代码是添加了一些hook函数
    */

    const configProps = matched.props && matched.props[name]
    // 把路由记录matched中的props属性合并选项对象data中
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }

    return h(component, data, children)
  }

总结下RouterView在render过程中的工作:

  1. 检查是否有嵌套路由的情况,或者没有component的情况
    1. 不存在,直接渲染
    2. 存在,把组件缓存在当前的父级组件对象
  2. 把路由记录matched中的props属性合并选项对象data中
  3. 组件来自全局注册的route.matched数组中,根据install.js部分的分析,我们知道route.matched数组中,根据install.js部分的分析,我们知道route也就router.history.current。也就是说之所以RouterView是根据router.history.current。也就是说之所以RouterView是根据router.history.current来动态的渲染组件。

触发$router的api

根据上面的分析,我们猜测,通过点击RouterLink组件,触发$router的方法,从而改变绑定history上的数据,这就整体的流程就完成了。下面就分析一下,事件绑定在RouterLink组件中,回顾对应小结并进行代码追踪发现,点击事件总会触发router.push/replace这个两个方法,这两个方法又都调用了transitionTo方法。

// transitionTo
transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
    ) {
    let route = this.router.match(location, this.current)
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
         this.updateRoute(route)
        // ...onComplete
      },
      err => {
        //...onAbort
      }
    )
  }

transitionTo里,调用了router.match方法,得到要跳转的route对象。传给confirmTransition方法里的回调函数中执行了this.updateRoute(route)。这里执行了$route的更新。

  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }

我们终于找到了$route更新的地方!对于vue-router的整体分析可以暂时告一段落了。

后面还调用了confirmTransition方法

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current // Route对象,也是$route最后要访问的对象
    this.pending = route
    const abort = err => {
       // ...触发的话,会把错误队列errorCbs里的保存的函数,或者console.error(err),有onAbort回调就执行回调
    }
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    // 判断要改变的route和当前的route是否『相等』
    if (
      isSameRoute(route, current) &&
      // 处理路由动态添加的情况?
      lastRouteIndex === lastCurrentIndex && 
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      // createNavigationDuplicatedError ,看名字应该是报一个导航重复的错误
      return abort(createNavigationDuplicatedError(current, route))
    }

    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
    // 定义导航守卫
    const queue: Array<?NavigationGuard> = [].concat(
        // ......
    )
    // 定义了一个迭代器
    const iterator = (hook: NavigationGuard, next) => {
        
    }
    // 执行一系列生命周期的hook
    runQueue(queue, iterator, () => {
     
    })
  }

到这里发现,点击事件的执行完成了。

总结下,尝试解答一下开篇提到的问题:

1. VueRouter对象在构造过程中,有那些处理?

  1. 创建matcher对象。这个对象作用是匹配传入的路径参数(RawLocation),生成对应的路由信息对象(route),我们在vue组件中访问的$route对象,实际上都是通过这个方法获得的。经过处理的路由配置信息也是保存在了matcher关联的闭包中。
  2. 创建history对象,这个对象负责了事件队列处理,为router提供api支持。很多底层方法都是放在了这里。如果想看导航守卫、文档上的api源码的话,可以看下这里。

2. RouterLink组件和RouterView组件是如何通信的?

VueRouter在Vue中注册的时候,会在Vue原型中添加routerrouter和route的get访问方法。route又是route又是router.history.current。RouterLink通过传入的参数(例如,to="/foo"),调用router.match后获取下一跳的路由对象route。在VueRouter注册过程中,对route添加了响应监听,所以route添加了响应监听,所以route变化对触发VueRouterView组件的render方法,从而进行后续的操作。

3. 点击RouterLink组件过程中发生了什么?

点击RouterLink组件的处理过程中,会触发router.push/replace方法,更新$route值。之后会把注册的函数队列按照顺序执行。

总结

整个VueRouter的源码分析就结束了,其中有很多值得学习的地方。比如在VueRouter的constructor方法中,对于History的创建采用了工厂模式,用来应对不同的路由模式。创建matcher的地方对应闭包的使用同样很好。还有对于router和route的使用了Object.defineProperty挂载属性,这样访问代码能少写很多。在history里对于事件的处理中,使用到了事件队列和迭代器,优雅的实现了导航守卫以及相关的事件处理代码。

参考

  1. path的路径解析用到了path-to-regexp
  2. Vue源码-Vue-Router