Vue-router 源码解析

188 阅读10分钟

引言

参考版本: vue-router@v3.0.1

github.com/vuejs/vue-r…

路由介绍

什么是路由

路由是指确定如何响应不同URL的过程。它是将URL与特定的操作、资源或内容相关联的方法。在Web开发中,路由通常用于确定当用户访问特定URL时应该展示哪个页面或执行哪些操作。

后端路由

路由这个概念,最开始应该是出现在后端的,在前后端没有分离的时代,通过解析不同的 URL 去拼接需要的 Html 或模板。每次路由的改变,都需要向服务器发送请求,得到一个新的HTML页面。

前端路由

在单页面应用中,浏览器中处理URL变化并相应地加载相应视图内容的机制,就是路由。前端路由库会捕捉这些变化,并根据定义的路由规则,动态更新页面的某部分内容,而不是向服务器请求新页面。

用法回顾

基本使用

<div id="app">
    <h1>Hello App!</h1>
    <p>
        <!-- 使用 router-link 组件来导航. -->
        <!-- 通过传入 `to` 属性指定链接. -->
        <!-- <router-ink> 默认会被渲染成一个 `<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>

// 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. 创建和挂载根实例。
const app = new Vue({
    router
}).$mount('#app')

路由模式

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

hash模式:

www.baidu.com/#login

history模式:

www.baidu.com/login

const router = new VueRouter({mode: 'history',routes: [...]})

router

访问路由器对象,VueRouter实例对象

 // 导航守卫
router.beforeEach((to, from, next) => {
    next()
})
router.beforeResolve((to, from, next) => {
    next()
})
router.afterEach((to, from) => {})

router.push
router.replace
router.go
router.back
router.forward
router.addRoutes
router.addRoute
router.getRoutes

route

访问当前路由对象

{
    fullPath: "/application-form/151730645?AUTH_TICKET=2SadvWLh53JlBMT-XiY3ZMjenSCLoDSM-Kx1bz61eFvXNXWwujtEPBPTr-xgKHWdpTelEldR07pq4bT4BnwkCWsRsSDQYLt_I1xnm_IO0eI%3D&LANG=ZH-CN", // 完整路径
    hash: "", //hash值
    matched: (2) [{…}, {…}], // 当前路由下的嵌套路由路径
    meta: {}, // 路由文件中的meta信息
    name: "ApplicationForm", // 路由名称
    params: {uid: '151730645'}, // 路径参数
    path: "/application-form/151730645", // 路由路径
    query: {AUTH_TICKET: '2SadvWLh53JlBMT-XiY3ZMjenSCLoDSM-Kx1bz61eFvXNXWwuj…gKHWdpTelEldR07pq4bT4BnwkCWsRsSDQYLt_I1xnm_IO0eI=', LANG: 'ZH-CN'} // URL查询参数,跟在?后
}

路由守卫(导航守卫)

提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

全局路由守卫

在路由实例上的钩子函数,所有的路由配置都会触发,通常可以用来控制路由的权限

  • beforeEach(to,form,next) - 在路由跳转前触发。
  • beforeResolve(to,form,next) - 触发时机在全局beforeEach、组件内beforeRouteEnter 之后,afterEach之前调用。
  • afterEach(to,form) - 在路由跳转完成后触发
router.beforeEach((to, from, next) => {
  if (to.path === '/login') return next();
  //获取token
  const tokenStr = window.sessionStorage.getItem('token')
  if (!tokenStr) return next('/login')
  next()
})

守卫参数:

  • to: 要进入的目标路由

  • form: 要离开的路由

  • next: 放行函数

    • next(): 直接进入下一个路由守卫钩子

    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。

    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。参数可以是一个 Location 对象

    • next(error): 如果传入的是一个error,路由跳转终止,并且该错误会传递给router.onError

路由独享的守卫

是指在单个路由配置的时候也可以设置的钩子函数,这些守卫与全局守卫beforeEach的方法参数是一样的。

  • beforeEnter: (to, from, next): 只会在路由进入时触发,不会在hash改变时触发
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件级路由守卫

  • beforeRouteEnter(to, from, next) - 守卫执行前,组件实例还没被创建。因此不能使用this。
  • beforeRouteUpdate(to, from, next) - 路由改变时,组件被复用,这个钩子就会被调用,可以访问this。
  • beforeRouteLeave(to, from, next) - 导航离开该组件的对应路由时调用。
// beforeRouteEnter 可以通过给next传递一个回调来获取实例
beforeRouteEnter (to, from, next) {
    next(vm => {
        // 通过 `vm` 访问组件实例
    })
}

beforeRouteUpdate (to, from, next) {
    // just use `this`
    this.name = to.params.name
    next()
}

// 这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
    const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
    if (answer) {
        next()
    } else {
        next(false)
    }
}

完整的导航解析流程

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

源码解析

whiteboard_exported_image_副本.png

目录介绍

// vue-router 项目 src下
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
    - abstract.js // abstract模式
    - base.js // History 父类
    - hash.js // hash 模式
    - html5.js // h5模式(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
- create-matcher.js // Route 匹配
- create-route-map.js // Route 映射
- index.js // Router 入口
- install.js // Router 插件安装
- router.js // Router 类
  • componets是RouterLink和RouterView这两个组件;
  • create-matcher.js就是我们创建match的入口文件;
  • create-route-map.js用于创建映射表,pathMap,nameMap等;
  • history是创建hitory类的逻辑;
  • index.js就是我们的入口文件,其中创建了VueRouter这个类;
  • install.js是我们挂载vue-router插件的逻辑;
  • util定义了很多工具函数;

需要知道的内容

Location

用来描述路由位置的对象,this.route.push(location),当这里是个object,就是Location

declare type Location = {
  name?: string; // 路由名称
  path?: string; // 路由的路径,可能包含params和query
  hash?: string; // Url的hash部分
  query?: Dictionary<string>; // url的查询参数,跟在?后面
  params?: Dictionary<string>; // 路由参数
  append?: boolean;// 当设置为true时,如果path存在,会将路径添加到当前路径之后。例如,如果当前路径是/a,path是/b,那么最终路径将是/a/b。
  replace?: boolean;// 设置为true时,在导航时会调用router.replace()而不是router.push(),这意味着导航不会留下历史记录。
}

Route

路由对象,this.route 就是一个路由对象

declare type Route = {
  path: string; // 表示路由的路径(不包括查询参数和哈希)
  name: ?string; //  路由的名称。
  hash: string; // URL中的哈希值,以#开始。
  query: Dictionary<string>; // URL中的查询参数
  params: Dictionary<string>; //  路由的参数对象。
  fullPath: string; //  完整的解析后的URL,包括查询参数和哈希值。
  matched: Array<RouteRecord>; // 一个数组,包含了当前路由匹配到的所有路由记录(RouteRecord)。
  redirectedFrom?: string; // 如果当前路由是重定向的结果,这个字段会包含重定向前的路径。
  meta?: any; // 路由元信息。可以在路由守卫中访问
}

RouteRecord

路由记录(我们写的那个路由配置)

declare type RouteRecord = {
  path: string; // 路由的路径,根据 parent 的 path 做计算
  alias: Array<string>; // 路由的别名,一个或多个
  regex: RouteRegExp; // path解析的正则表达式扩展
  components: Dictionary<any>; // 用来存储路由视图对应的组件
  instances: Dictionary<any>; // 用来存储路由组件的实例
  enteredCbs: Dictionary<Array<Function>>; // 函数数组,这些函数是在路由被确认进入之前需要调用的回调。
  name: ?string; // 路由的名称
  parent: ?RouteRecord; // 父路由记录
  redirect: ?RedirectOption; // 定义路由的重定向规则
  matchAs: ?string; // 当路由有别名时,matchAs属性会被设置为别名的路径。
  beforeEnter: ?NavigationGuard; // 路由独享守卫
  meta: any; // 路由元信息
  props: boolean | Object | Function | Dictionary<boolean | Object | Function>; // 用于传递props给组件
}

RouteRegExp 是正则表达式的扩展,它利用了path-to-regexp 这个工具库,把 path 解析成一个正则表达式的扩展

const keys = [];
const regexp = pathToRegexp("/user/:id", keys);

console.log(regexp) // /^/user(?:/([^/#?]+?))[/#?]?$/i
console.log(keys) // [{name: "id", prefix: "/", suffix: "", pattern: "[^/#?]+?", modifier: ""}]

插件安装 —— install.js

vue插件

Vue-router 本质上是官方提供的一个插件,在vue 中用 Vue.use 来注册插件

,使用方式如下:

import Vue from 'vue';
import Router from 'vue-router';

// 在vue中注册组件
Vue.use(Router);

vue.use的源码

export function initUse (Vue: GlobalAPI) {
    // plugin 可以使一个函数也可以是一个数组
  Vue.use = function (plugin: Function | Object) {
    
    // 组件是否被注册过
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    const args = toArray(arguments, 1)
    // this指向的是vue,后面把vue传给了组件,后面在看vue-rputer的install也可以看到
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    
    // 最后,将插件添加到installedPlugins中,标注已被注册过
    installedPlugins.push(plugin)
    return this
  }
}

Vue.use接收一个plugin(可以是一个函数也可以是一个Object)的参数,并且维护了一个_installedPlugins,用于缓存注册过的plugin。然后会判断当前的plugin是否被注册过,如果被注册过提前结束并返回vue;如果没有,注册组件,然后把vue传给plugin的注册函数并调用了,这样插件就不需要额外的引入vue了。

install.js

//  /src/intall.js

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

export let _Vue

// 这里就是就上面传过来的vue实例
export function install (Vue) {

  // 防止重复注册
  if (install.installed && _Vue === Vue) return
  
  install.installed = true
  
  // 这里把vue给保存起来了
  _Vue = Vue
  
  // 是不是根组件
  const isDef = v => v !== undefined
  
  
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  
  // 这里通过全局mixin,执行一些操作
  Vue.mixin({
    beforeCreate () {
    
    // 如果是根组件
      if (isDef(this.$options.router)) {
      
        this._routerRoot = this
        // VueRouter 的实例
        this._router = this.$options.router
        // vuerouter初始化
        this._router.init(this)
        
        // 让_route属性变成了一个响应式对象,当_route发生变化的时候,router-view会重新渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 如果当前组件是子组件,就将我们_root根组件挂载到子组件。
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 为router-view组件关联路由组件
      registerInstance(this, this)
    },
    destroyed () {
    // 取消router - view和路由组件的关联
      registerInstance(this)
    }
  })
  
  // 挂载router在组件实例上,可以在vue组件中直接使用this.router
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  
  // 挂载router在组件实例上,可以在vue组件中直接使用this.router
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
  
  // 引入RouterView 和 RouterLink 两个全局组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  
   //设置路由组件守卫的合并策略
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
  • 通过installed参数避免重复安装

  • 保存install中传入的vue实例

  • 全局mixn,给所有的组件注入 beforeCreate 和 destroyed两个生命周期,并初始化路由

    • 根组件,让route变成一个响应式属性
    • 子组件,把根组件挂载上去
  • 将router 和 route 挂载到vue实例上

  • 注册全局组件router-view、router-link

  • 合并路由守卫策略

VueRouter 实例

// src/router.js 

export default class VueRouter {

    constructor (options: RouterOptions = {}) {
      if (process.env.NODE_ENV !== 'production') {
        warn(this instanceof VueRouter, `Router must be called with the new operator.`)
      }
      this.app = null // 保存挂载实例
      this.apps = [] // VueRouter支持多实例
      this.options = options //接收的参数
      this.beforeHooks = [] // beforeEach hook
      this.resolveHooks = [] // beforeResolve hook
      this.afterHooks = [] // afterEach hook
      this.matcher = createMatcher(options.routes || [], this) // 路由匹配器,传入写好的路由表
    
      let mode = options.mode || 'hash' // 当前路由模式,默认hash
      
      // 当前浏览器不支持history模式,回退到hash
      this.fallback =
        mode === 'history' && !supportsPushState && options.fallback !== false
      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}`)
          }
      }
    }
    
    // 根据路由的原始位置和可选的当前路由和重定向来源,返回一个路由对象
    match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
      return this.matcher.match(raw, current, redirectedFrom)
    }
    
    // 获取当前激活的路由对象
    get currentRoute (): ?Route {
      return this.history && this.history.current
    }
    
    // 初始化路由,接受Vue实例作为参数
    init (app: any) {
      // ...实现省略...
    }
    
    // 添加全局前置守卫
    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)
    }
    
    // 当路由完全准备就绪时调用回调函数
    onReady (cb: Function, errorCb?: Function) {
      // ...实现省略...
    }
    
    // 当路由过程中出错时调用
    onError (errorCb: Function) {
      // ...实现省略...
    }
    
    // 导航到一个新URL,向history栈添加一个新纪录
    push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
      // ...实现省略...
    }
    
    // 导航到一个新URL,替换history栈上的当前纪录
    replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
      // ...实现省略...
    }
    
    // 在history记录中向前或者后移动特定数量的步骤
    go (n: number) {
      // ...实现省略...
    }
    
    // 后退到历史记录的上一步
    back () {
      // ...实现省略...
    }
    
    // 前进到历史记录的下一步
    forward () {
      // ...实现省略...
    }
    
    // 根据给定的路由获取匹配的组件数组(用于服务端渲染)
    getMatchedComponents (to?: RawLocation | Route): Array<any> {
      // ...实现省略...
    }  
    
    // 解析目标路由的路由信息
    resolve (
      to: RawLocation,
      current?: Route,
      append?: boolean
    ): {...} {
        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()
    }
    
    // 动态添加更多的路由规则
    addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
        this.matcher.addRoute(parentOrRoute, route)
        if (this.history.current !== START) {
          this.history.transitionTo(this.history.getCurrentLocation())
        }
    }
    
    // 一次性动态添加多条路由规则
    addRoutes (routes: Array<RouteConfig>) {
        this.matcher.addRoutes(routes)
        if (this.history.current !== START) {
          this.history.transitionTo(this.history.getCurrentLocation())
        }
    }
}

new VueRouter 完成了以下内容:

  • 接收options 参数,主要是routes(路由表)、mode(路由模式)等

  • 保存vue实例

  • 全局路由守卫数组初始化

  • 创建路由匹配器,传入routes路由表和VueRouter实例

  • 确定路由模式

    • 没有传路由模式,则默认时hash模式
    • 如果mode为history,但当前环境不支持,则回退至hash模式
    • 非浏览器环境,强制为abstract模式
  • 根据不同的模式创建不同的History实例

matcher

createMatcher

// src/create-matcher.js

  // 定义Matcher类型,它包含match、addRoutes、addRoute和getRoutes等方法
  export type Matcher = {
    // match方法用于匹配给定的原始路由位置raw,并返回一个Route对象
    match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
    // addRoutes方法用于动态添加多个路由配置
    addRoutes: (routes: Array<RouteConfig>) => void;
    // addRoute方法用于动态添加单个路由配置
    addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
    // getRoutes方法用于获取当前路由器已注册的所有路由记录
    getRoutes: () => Array<RouteRecord>;
  };

// createMatcher函数用于创建Matcher对象
  export function createMatcher (routes: Array<RouteConfig>, router: VueRouter): Matcher {
  
    // 创建一个路由映射表     const { pathList, pathMap, nameMap } = createRouteMap(routes)
    // 添加多个路由配置到路由系统
    function addRoutes (routes) {
      createRouteMap(routes, pathList, pathMap, nameMap)
    }

    // 添加单个路由配置到路由系统
    function addRoute (parentOrRoute, route) {
      // ...
    }

    // 获取当前已注册的所有路由记录
    function getRoutes () {
      // ...
    }

    // 根据给定的原始路由位置raw、当前路由currentRoute和重定向来源redirectedFrom匹配路由
    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 {
      // ...
    }

    // 内部函数,用于创建Route对象
    function _createRoute (
        record: ?RouteRecord,
        location: Location,
        redirectedFrom?: Location
    ): Route {
      // ...
    }

    // 返回Matcher对象,包含上述定义的方法
    return {
      match,
      addRoute,
      getRoutes,
      addRoutes
    }
  }

createMatcher 接收 2 个参数,一个是routes,是传进来的路由表配置,另外一个是vue-router实例

// 用户自定义路由配置
const routes = [{ path: '/home', component: Home },{ path: '/login', component: Login }]

CreateRouterMap()中创建一个路由映射表

  • pathList - 保存所有的path
  • pathMap - path -> RouteRecord
  • pathMap - name -> RouteRecord

返回一个Matcher对象,Matcher对象包含一个用于匹配的match方法、动态添加路由的addRoutes/addRoute方法、获取当前所有路由的getRoutes方法

match

匹配出一个route

declare type RawLocation = string | Location

function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location
): Route {
  // 解析一个location对象,规范化
  const location = normalizeLocation(raw, currentRoute, false, router)
  const { name } = location

  // 如果有命名路由
  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)

    // 如果location中没有params属性,则创建一个空对象
    if (typeof location.params !== 'object') {
      location.params = {}
    }

    // 如果有当前路由并且有路由参数
    if (currentRoute && typeof currentRoute.params === 'object') {
      for (const key in currentRoute.params) {
        // 如果当前路由的参数没有在新location中定义,并且是必需的参数,则从当前路由复制到新location
        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) {
    // 初始化params为空对象
    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)
}
  • 首先执行了 normalizeLocation,解析得到一个location对象

  • 计算出新的 location 后,对 location 的 name 和 path 的两种情况做了处理。

    • name

      • 通过nameMap找到路由记录

        • 如果没有,就返回一个新创建的空路由,返回一个空路径
      • 如果location中没有params属性,则创建一个空对象

      • 拿到 record 对应的 paramNames,再对比 currentRoute 中的 params,把交集部分的 params 添加到 location 中

      • 通过 fillParams 方法根据 record.path 和 location.path 计算出 location.path

      • 调用_createRoute(record, location, redirectedFrom) 去生成一条新路径

    • path

      • 遍历pathList

      • matchRoute匹配到对应记录

      • 调用_createRoute(record, location, redirectedFrom) 去生成一条新路径

接下来看下_createRoute的实现,不考虑重定向和别名路由,最终都会调用createRoute方法

function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    if (record && record.redirect) {
      // 重定向相关,忽略
    }
    if (record && record.matchAs) {
      // 别名相关,忽略
    }
    return createRoute(record, location, redirectedFrom, router)
  }
//src/uitl/route.js
export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

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

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

function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}

createRoute方法其实很简单,就是通过record和location 去创建一个route,formatMatch就是一直往上找到根路由为止。

addRoute / addRoutes

// addRoute函数用于添加单个路由规则
  function addRoute (parentOrRoute, route) {
    // 如果parentOrRoute是一个字符串,则从nameMap中找到对应的父路由记录
    const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
    // 使用createRouteMap函数添加新的路由记录
    createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)
  }
  
// addRoutes函数用于添加多个新的路由规则
// 接收一个路由配置数组routes作为参数
  function addRoutes (routes) {
  // 使用createRouteMap函数添加新的路由记录
    createRouteMap(routes, pathList, pathMap, nameMap)
 }

addRoute和addRoutes非常简单,就是使用createRouteMap去添加路由

createRouteMap

// src/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>
} {
  // 创建或使用旧的路径列表
  const pathList: Array<string> = oldPathList || []
  // 创建或使用旧的路径映射表
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // 创建或使用旧的名称映射表
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  
  // 遍历添加的routes
  routes.forEach(route => {
    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
  }
}

createRouteMap的作用是把用户的配置转换为三个部分(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: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
  // 创建一个新的路由记录对象
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    instances: {},
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }
  
  // 如果路由配置对象有children属性,递归处理子路由
  if (route.children) {
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  
  // 如果pathMap中还没有这个路径的记录,则将其添加到pathList和pathMap中
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  if (route.alias !== undefined) {
  // 处理别名路由
  }
  
  // 如果路由名称存在且nameMap中还没有这个名称的记录
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } 
  }
}
  • 创建一个新的路由记录
  • 递归处理子路由
  • 将path添加到pathList、将path -> record 添加到pathMap
  • 将name -> record 添加到pathMap

路由切换

路由跳转

下面我们讲一下路由的跳转,路由的push/replace都会调用到这个方法

// src/history/base.js

 transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    // 获取要跳转的route信息
    const route = this.router.match(location, this.current)
    
    // 切换路由的核心方法
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          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 => {
    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) {
      handleScroll(this.router, current, route, false)
    }
    return abort(createNavigationDuplicatedError(current, route))
  }
  
  // 拿到需要更新的所有路由(要跳转路由的父级路由),当前路由,待跳转的路由
  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)
  )
  
  // 迭代器函数,用于迭代执行守卫队列
  const iterator = (hook: NavigationGuard, next) => {
      // ...
  }
// 使用iterator迭代器函数按顺序执行queue队列中的所有导航守卫
  runQueue(queue, iterator, () => {
    const enterGuards = extractEnterGuards(activated)
    const queue = enterGuards.concat(this.router.resolveHooks)
    runQueue(queue, iterator, () => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      this.pending = null
      // 路由跳转
      onComplete(route)
      if (this.router.app) {
        this.router.app.$nextTick(() => {
          handleRouteEntered(route)
        })
      }
    })
  })
}
  • 通过match方法获取要跳转的route信息
  • 相同路由则不跳转
  • 按顺序执行路由守卫
  • 调用不同模式的方法跳转路由

路由模式

Hash

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
  
  // 设置监听事件,初始化时调用
  setupListeners () {
    // ...
    window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange',
      () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
    )
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        // hash push
        pushHash(route.fullPath)
      },
      onAbort
    )
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        // hash replace
        replaceHash(route.fullPath)
      },
      onAbort
    )
  }

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

  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }

  getCurrentLocation () {
    return getHash()
  }
}
function pushHash (path) {
// 支持pushState直接使用,直接使用 window .history的api
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
  // 不支持则直接修改hash
    window.location.hash = path
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

// 去掉hash的部分
function getUrl (path) {
  const href = window.location.href
  // 拿到hash值
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}


export function pushState (url?: string, replace?: boolean) {
  const history = window.history
  if (replace) {
      history.replaceState(stateCopy, '', url)
  } else {
       history.pushState({ key: setStateKey(genStateKey()) }, '', url)
  }
}

export function replaceState(url?: string) {
  pushState(url, true)
}
  • 在vueRouter初始化时,会添加监听事件

    • 支持pushState的环境,则监听popstate事件
    • 不支持pushState的环境,则监听hashchange事件
  • go 调用的是window.history.go 方法·

  • replace

    • pushState存在,window.history.pushState
    • pushState不存在 ,window.location.replace
  • push

    • pushStatey存在,window.history.replaceState

    • pushState不存在 * *直接改变hash的值

可以看到,在支持pushState存在的环境下,hash模式,会优先使用history的api,并且是监听popstate事件。

html5

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    super(router, base)

    this._startLocation = getLocation(this.base)
  }
  
  // 设置监听事件,初始化时调用
  setupListeners () {
  // ...
  
  window.addEventListener('popstate', e => {
      const current = this.current
      const location = getLocation(this.base)
      if (this.current === START && location === initLocation) {
        return
      }
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
  }

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

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
    }, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath))
    }, onAbort)
  }

  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }

  getCurrentLocation (): string {
    return getLocation(this.base)
  }
}

export function pushState (url?: string, replace?: boolean) {
  const history = window.history
  if (replace) {
      history.replaceState(stateCopy, '', url)
  } else {
       history.pushState({ key: setStateKey(genStateKey()) }, '', url)
  }
}

export function replaceState(url?: string) {
  pushState(url, true)
}
  • 在vueRouter初始化时,会添加监听事件

    • 监听popstate事件
  • go 调用的是window.history.go 方法·

  • replace调用的是window.history.pushState

  • push调用的是window.history.replaceState

组件

router-link

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

    // 样式处理,省略
    // ...
    
    // 重定向处理,创建一个新的route
    const compareTarget = route.redirectedFrom
      ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
      : route

    // 点击后类名处理
    // ...

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
            // replace 跳转
          router.replace(location, noop)
        } else {
            // push 跳转
          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
    }

    // 插槽相关处理
    // ...

    // 标签处理
    // ...

    return h(this.tag, data, this.$slots.default)
  }
}
  • 解析出location, route, href

  • router-link样式处理

  • 给router-link的事件都绑定上handler 函数

    • 点击router-link实际上就是执行router.push / router.replace 方法

router-view

VueRouter 内置了组件,是用来进行渲染当前路由的组件

// src/components/view.js
export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
      // 标识该组件是通过 route-view 渲染出来的
    data.routerView = true

    const h = parent.$createElement
    const name = props.name
    
    // 获取当前路由对象
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    
    // 计算嵌套的 router-view 深度
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      // 如果是 keep-alive 设置 inactive 为 true
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    // 保存router-view 深度,用于后续的匹配
    data.routerViewDepth = depth
    
    // keep-alive
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      // 渲染keepAlive缓存的组件
      if (cachedComponent) {
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
      // 如果没有缓存的组件,返回空
        return h()
      }
    }
    
    // 根据深度获取需要渲染的路由
    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    if (!matched || !component) {
      cache[name] = null
      return h()
    }
    
    // 缓存当前组件
    cache[name] = { component }
    
    // ...
    
    // 如果路由配置有 props,处理并填充到 data 中的props中
    const configProps = matched.props && matched.props[name]
    if (configProps) {
    // 将route和props也缓存起来
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }

    return h(component, data, children)
  }
}
  • 计算router-view 的深度
  • 通过matched获得具体路由
  • 把路由配置的props 注入到 组件的 data.protos 参数中

image.png

参考资料