vue-router源码分析(中)

本文详细分析了vue-router的实例化过程,包括VueRouter构造函数的参数、初始化、创建matcher、确定路由模式及各个模式的实现,如HTML5History、HashHistory和AbstractHistory。文章揭示了路由模式如何根据浏览器环境选择,并展示了不同模式下的路由更新和页面内容变更机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

路由的概念相信大部分同学并不陌生,我们在用 Vue 开发过实际项目的时候都会用到 Vue-Router 这个官方插件来帮我们解决路由的问题。它的作用就是根据不同的路径映射到不同的视图。本文不再讲述路由的基础使用和API,不清楚的同学可以自行查阅官方文档vue-router3 对应 vue2vue-router4 对应 vue3。今天我们从源码出发以vue-router 3.5.3源码为例,一起来分析下Vue-Router的具体实现。

由于篇幅原因,vue-router源码分析分上、中、下三篇文章讲解。

vue-router源码分析(上)

vue-router源码分析(中)

vue-router源码分析(下)

在上文中我者讲述路路由的一些前置知识,以及路由安装模块的分析。今天我们来讲讲路由的实例化到底都干了些什么事情。

实例化

实例化就是我们new VueRouter({routes})的过程,我们来重点分析下VueRouter的构造函数。

分析VueRouter构造函数

VueRouter的构造函数在src/index.js中。

// index.js

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 = []
  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

  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}`)
      }
  }
} 
构造函数参数

构造函数接收RouterOptions参数,也就是我们new VueRouter({})传递的参数。

具体参数意思如下:

export interface RouterOptions {
  routes?: RouteConfig[] // 路由配置规则列表
  mode?: RouterMode // 模式
  fallback?: boolean // 是否启用回退到hash模式
  base?: string // 路由base url
  linkActiveClass?: string // router-link激活时类名
  linkExactActiveClass?: string // router-link精准激活时类名
  parseQuery?: (query: string) => Object // 自定义解析qs的方法
  stringifyQuery?: (query: Object) => string // 自定义序列化qs的方法
  scrollBehavior?: ( // 控制滚动行为
    to: Route,
    from: Route,
    savedPosition: Position | void ) => PositionResult | Promise<PositionResult> | undefined | null
} 
参数初始化

我们看到在最开始有些参数的初始化,这些参数到底是什么呢?

this.app 用来保存根 Vue 实例。

this.apps 用来保存持有 $options.router 属性的 Vue 实例。

this.options 保存传入的路由配置。

this.beforeHooksthis.resolveHooksthis.afterHooks 表示一些钩子函数,我们之后会介绍。

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

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

创建matcher

通过createMatcher(options.routes || [], this)生成matcher,这个matcher对象就是前面聊的匹配器,负责url匹配,它接收了routesrouter实例

这个非常重要,我们来重点分析。

分析create-matcher
// src/create-matcher.js

...

// 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>;
};

// createMatcher返回Matcher对象
export function createMatcher ( routes: Array<RouteConfig>, // 路由配置列表
  router: VueRouter // VueRouter实例 ): Matcher {

  // 创建路由映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  // 批量添加路由
  function addRoutes (routes) {
    // 所以这里会重新调用createRouteMap方法
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  
  // 添加单个路由
  function addRoute (parentOrRoute, route) {
    ...
  }

  // 获取路由关系数组
  function getRoutes () {
    return pathList.map(path => pathMap[path])
  }
  
  // 传入Location和Route,返回匹配的Route对象
  function match ( raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location ): Route {
    ...
  }

  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
  
  ...
} 

matcher对象不光定义了match、addRoute、addRoutes、getRoutes四个方法供我们调用,而且在最开始通过createRouteMap()方法创建了路由映射表RouteMap

这里的match方法非常重要,后面我们会分析。

下面我们来看看createRouteMap()方法。

分析createRouteMap
// src/create-route-map.js

// 创建路由映射map、添加路由记录
export function createRouteMap ( routes: Array<RouteConfig>, // 路由配置列表
  oldPathList?: Array<string>, // 旧pathList
  oldPathMap?: Dictionary<RouteRecord>, // 旧pathMap
  oldNameMap?: Dictionary<RouteRecord>// 旧nameMap ): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {

  // 若旧的路由相关映射列表及map存在,则使用旧的初始化(借此实现添加路由功能)
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  
  // 遍历路由配置对象,生成/添加路由记录
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  
  // 确保path:*永远在在最后
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  
  // 开发环境,提示非嵌套路由的path必须以/或者*开头
  if (process.env.NODE_ENV === 'development') {
    // warn if routes do not include leading slashes
    const found = pathList
    // check for missing leading slash
      .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')
    if (found.length > 0) {
      const pathNames = found.map(path => `- ${path}`).join('\n')
      warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
    }
  }
  
  return {
    pathList,
    pathMap,
    nameMap
  }
  
  // 添加路由记录,更新pathList、pathMap、nameMap
  function addRouteRecord ( pathList: Array<string>,
    pathMap: Dictionary<RouteRecord>,
    nameMap: Dictionary<RouteRecord>,
    route: RouteConfig,
    parent?: RouteRecord,
    matchAs?: string ) {
    ...
  }
} 

可以看到createRouteMap返回一个对象,它包含pathListpathMapnameMap

  • pathList数组中存储了routes中的所有path

  • pathMap对象维护的是path路由记录RouteRecord的映射

  • nameMap对象维护的是name路由记录RouteRecord的映射

那么 RouteRecord 到底是什么,先来看一下它的数据结构:

export interface RouteRecord {
  path: string  // 规范化后的路径
  regex: RegExp / 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
  components: Dictionary<Component> // 保存路由组件,支持命名视图
  instances: Dictionary<Vue> // 保存router-view实例
  name?: string // 路由名
  parent?: RouteRecord // 父路由
  redirect?: RedirectOption // 重定向的路由配置对象
  matchAs?: string // 别名路由需要使用
  meta: RouteMeta // 路由元信息
  beforeEnter?: ( // 路由钩子
    route: Route,
    redirect: (location: RawLocation) => void,
    next: () => void ) => any
  props: // 动态路由传参
    | boolean
    | Object
    | RoutePropsFunction
    | Dictionary<boolean | Object | RoutePropsFunction>
} 

笔者的路由文件是

const routes = [
  {
    path: "/routertest",
    name: "RouterTest",
    component: () =>
      import(/* webpackChunkName: "router" */ "@/views/Router.vue"),
  },
] 

我们分别输出pathListpathMapnameMap来看一下

image.png

可以看到pathList是一个path数组,pathMapnameMap是两个对象,对象的key分别是pathname,value就是RouteRecord对象。

接下来我们重点看看addRouteRecord()方法。

分析addRouteRecord
// src/create-route-map.js

...

// 添加路由记录,更新pathList、pathMap、nameMap
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') {
  
    // route.path不能为空
    assert(path != null, `"path" is required in a route configuration.`)
    
    // route.component不能为string
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(
        path || name
      )} cannot be a ` + `string id. Use an actual component instead.`
    )
  }
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
    
  // 生成格式化后的path(子路由会拼接上父路由的path)
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
 
 // 匹配规则是否大小写敏感?(默认值:false)
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  
  // 生成一条路由记录
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
    components: route.components || { default: route.component }, // 保存路由组件,支持命名视图https://router.vuejs.org/zh/guide/essentials/named-views.html#命名视图
    instances: {}, // 保存router-view实例
    name, // name
    parent, // 父实例
    matchAs, // 别名路由需要使用
    redirect: route.redirect, // 重定向的路由配置对象
    beforeEnter: route.beforeEnter, // 路由独享的守卫
    meta: route.meta || {}, // 元信息
    props: // 动态路由传参;
      route.props == null
        ? {}
        : route.components // 命名视图的传参规则需要使用route.props指定的规则
          ? route.props
          : { default: route.props }
  }
  
  // 处理有子路由情况
  if (route.children) {
    // 命名路由 && 未使用重定向 && 子路由配置对象path为''或/时,使用父路由的name跳转时,子路由将不会被渲染
    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 => {
      const childMatchAs = matchAs // matchAs若有值,代表当前路由是别名路由,则需要单独生成别名路由的子路由,路径前缀需使用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) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] // alias支持string,和Array<String>
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if (process.env.NODE_ENV !== 'production' && alias === path) { // alias的值和path重复,需要给提示
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }
      
      // 生成别名路由配置对象
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      
      // 添加别名路由记录
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute, // 别名路由
        parent, // 当前路由的父路由,因为是给当前路由取了个别名,所以二者其实是有同个父路由的
        record.path || '/' // matchAs,用来生成别名路由的子路由;
      )
      // ! 总结:当前路由设置了alias后,会单独为当前路由及其所有子路由生成路由记录,且子路由的path前缀为matchAs(即别名路由的path)
    }
  }
  
  // 处理命名路由
  if (name) {
    // 更新nameMap
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      // 路由重名警告,相当于name有重复了
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
} 

我们看到addRouteRecord()参数有一个route参数,它的类型是RouteConfig,我们来看看它的数据结构。

interface RouteConfig = {
  path: string,
  component?: Component,
  name?: string, // 命名路由
  components?: { [name: string]: Component }, // 命名视图组件
  redirect?: string | Location | Function,
  props?: boolean | Object | Function,
  alias?: string | Array<string>,
  children?: Array<RouteConfig>, // 嵌套路由
  beforeEnter?: (to: Route, from: Route, next: Function) => void,
  meta?: any,

  // 2.6.0+
  caseSensitive?: boolean, // 匹配规则是否大小写敏感?(默认值:false)
  pathToRegexpOptions?: Object // 编译正则的选项
} 

这个RouteConfig其实就是我们写代码的时候定义的routes数组里面的对象。

addRouteRecord()方法总结

  1. 首先会检查route对象pathcomponent不能为空。

  2. 然后生成格式化后的path(子路由会拼接上父路由的path)

  3. 处理匹配规则是否大小写敏感,(默认值:false)

  4. 生成一条路由记录。

  5. 如果有子路由,则会继续递归调用addRouteRecord,生成子路由记录。

  6. pathMap中不存在当前路径,则更新pathListpathMap

  7. 处理别名路由。当前路由设置了alias后,会单独为当前路由及其所有子路由生成路由记录,且子路由的path前缀为matchAs(即别名路由的path)。

  8. nameMap中不存在当前名字,则会添加到nameMap,如果route对象没有name则不会添加。所以nameMap的长度不一定等于pathListpathMap长度。

我们来重点看看别名和父子组件,笔者的路由文件是

const routes = [
  {
    path: "/father",
    name: "Father",
    component: () =>
      import(/* webpackChunkName: "father" */ "../views/Father.vue"),
    children: [
      {
        path: "fchild1",
        name: "FChild1",
        component: () =>
          import(/* webpackChunkName: "father" */ "@/components/Fchild1.vue"),
        children: [
          {
            path: "fchild1_1",
            name: "FChild1_1",
            component: () =>
              import(
                /* webpackChunkName: "father" */ "@/components/Fchild1_1.vue"
              ),
          },
        ],
      },
      {
        path: "fchild2",
        name: "FChild2",
        component: () =>
          import(/* webpackChunkName: "father" */ "@/components/Fchild2.vue"),
      },
    ],
  },
  {
    path: "/routertest",
    name: "RouterTest",
    alias: "/router",
    component: () =>
      import(/* webpackChunkName: "router" */ "@/views/Router.vue"),
  },
] 

我们分别输出pathListpathMapnameMap来看一下

image.png

可以看到每个子路由都生成了记录,并且取别名的生成了/routertest、/router两条记录。

确定路由模式

路由模式平时都会只说两种,其实在vue-router总共实现了 hashhistoryabstract 3 种模式。

VueRouter会根据options.modeoptions.fallbacksupportsPushStateinBrowser来确定最终的路由模式。

如果没有设置mode就默认是hash模式。

确定fallback值,只有在用户设置了mode:history并且当前环境不支持pushState且用户没有主动声明不需要回退(没设置fallback值位undefined),此时this.fallback才为true,当fallbacktrue时会使用hash模式。(简单理解就是如果不支持history模式并且只要没设置fallbackfalse,就会启用hash模式)

如果最后发现处于非浏览器环境,则会强制使用abstract模式。

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属性值来实例化不同的对象。

我们来看下源码

// index.js

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

VueRouter的三种路由模式,主要由下面的三个核心类实现

  • History

    • 基础类
    • 位于src/history/base.js
  • HTML5History

    • 用于支持pushState的浏览器
    • src/history/html5.js
  • HashHistory

    • 用于不支持pushState的浏览器
    • src/history/hash.js
  • AbstractHistory

    • 用于非浏览器环境(服务端渲染)
    • src/history/abstract.js

HTML5HistoryHashHistoryAbstractHistory三者都是继承于基础类History

三者不光能访问History类的所有属性和方法,他们还都实现了基础类中声明的需要子类实现的5个接口(go、push、replace、ensureURL、getCurrentLocation)

首先我们来看看History类

History类
// src/history/base.js

...

export class History {
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) => void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  listeners: Array<Function>
  cleanupListeners: Function

  // 子类需要实现的方法
  +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
    // 格式化base,保证base是以/开头
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
    this.listeners = []
  }
} 

可以看到,History类构造函数中主要干了下面几件事

  1. 保存了router实例

  2. 规范化了base,确保base是以/开头

  3. 初始化了当前路由指向,默认指向START初始路由;在路由跳转时,this.current代表的是fromSTART定义在src/utils/route.js中,会返回一个默认Route对象。

  4. 初始化了路由跳转时的下个路由pending,默认为null;在路由跳转时,this.pending代表的是to

  5. 初始化了一些回调相关的属性

关于Route对象,我们来看看它的结构

export interface Route {
  path: string
  name?: string | null
  hash: string
  query: Dictionary<string | (string | null)[]>
  params: Dictionary<string>
  fullPath: string
  matched: RouteRecord[]
  redirectedFrom?: string
  meta?: RouteMeta
} 

再来看看HTML5History类

HTML5History类
// src/history/html5.js

...

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    // 调用父类构造函数初始化
    super(router, base)

    this._startLocation = getLocation(this.base)
  }

  // 设置监听,主要是监听popstate方法来自动触发transitionTo
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    
    // 若支持scroll,初始化scroll相关逻辑
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current

      // 某些浏览器,会在打开页面时触发一次popstate 
      // 此时如果初始路由是异步路由,就会出现`popstate`先触发,初始路由后解析完成,进而导致route未更新 
      // 所以需要避免
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }
      
      // 路由地址发生变化,则跳转,如需滚动则在跳转后处理滚动
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    
    // 监听popstate事件
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }

  // 可以看到 history模式go方法其实是调用的window.history.go(n)
  go (n: number) {
    window.history.go(n)
  }

  // push方法会主动调用transitionTo进行跳转
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  // replace方法会主动调用transitionTo进行跳转
  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, 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 getLocation (base: string): string {
  let path = window.location.pathname
  const pathLowerCase = path.toLowerCase()
  const baseLowerCase = base.toLowerCase()
  // base="/a" shouldn't turn path="/app" into "/a/pp"
  // https://github.com/vuejs/vue-router/issues/3555
  // so we ensure the trailing slash in the base
  if (base && ((pathLowerCase === baseLowerCase) ||
    (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
} 

可以看到HTML5History类主要干了如下几件事。

  1. 继承于History类,并调用父类构造函数初始化。

  2. 实现了setupListeners方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑,监听了popstate事件,并在popstate触发时自动调用transitionTo方法。

  3. 实现了go、push、replace等方法,我们可以看到,history模式其实就是使用的新的history api

// 可以看到 history模式go方法其实是调用的window.history.go(n)
go (n: number) {
  window.history.go(n)
}

// push、replace调用的是util/push-state.js,里面实现了push和replace方法
// 实现原理也是使用的history api,并且在不支持history api的情况下使用location api

export function pushState (url?: string, replace?: boolean) {
  ...
  const history = window.history
  try {
    if (replace) {
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      // 调用的 history.replaceState
      history.replaceState(stateCopy, '', url)
    } else {
      // 调用的 history.pushState
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
} 

总结

所以history模式的原理就是在js中路由的跳转(也就是使用pushreplace方法)都是通过history新的apihistory.pushStatehistory.replaceState两个方法完成,通过这两个方法我们知道了路由的变化,然后根据路由映射关系来实现页面内容的更新。

对于直接点击浏览器的前进后退按钮或者js调用 this.$router.go()this.$router.forward()this.$router.back()、或者原生js方法history.back()history.go()history.forward()的,都会触发popstate事件,通过监听这个事件我们就可以知道路由发生了哪些变化然后来实现更新页面内容。

注意history.pushStatehistory.replaceState这两个方法并不会触发popstate事件。

接下来我们再来看看HashHistory类

HashHistory类
//src/history/hash.js

...

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }

  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)
        }
      })
    }
    // 事件优先使用 popstate
    // 判断supportsPushState就是通过return window.history && typeof window.history.pushState === 'function'
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
  
  // 其实也是优先使用history的pushState方法来实现,不支持再使用location修改hash值
  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
    )
  }

  // 其实也是优先使用history的replaceState方法来实现,不支持再使用location修改replace方法
  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
    )
  }

  // 也是使用的history go方法
  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 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
}

// 获取 # 后面的内容
export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf('#')
  // empty path
  if (index < 0) return ''

  href = href.slice(index + 1)

  return href
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

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

可以看到HashHistory类主要干了如下几件事。

  1. 继承于History类,并调用父类构造函数初始化。这里比HTML5History多了回退操作,所以,需要将history模式的url替换成hash模式,即添加上#,这个逻辑是由checkFallback实现的

  2. 实现了setupListeners方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑。 监听了popstate事件或hashchange事件,并在相应事件触发时,调用transitionTo方法实现跳转。

通过const eventType = supportsPushState ? 'popstate' : 'hashchange'我们可以发现就算是hash模式优先使用的还是popstate事件。

  1. 实现了go、push、replace等方法。

我们可以看到,hash模式实现的push、replace方法其实也是优先使用history里面的方法,也就是新的history api

// 可以看到 hash 模式go方法其实是调用的window.history.go(n)
go (n: number) {
  window.history.go(n)
}

// 在支持新的history api情况下优先使用history.pushState实现
// 否则使用location api
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

// 在支持新的history api情况下优先使用history.replaceState实现
// 否则使用location api
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
} 

总结

在浏览器链接里面我们改变hash值是不会重新向后台发送请求的,也就不会刷新页面。并且每次 hash 值的变化,还会触发hashchange 这个事件。

所以hash模式的原理就是通过监听hashchange事件,通过这个事件我们就可以知道 hash 值发生了哪些变化然后根据路由映射关系来实现页面内容的更新。(这里hash值的变化不管是通过js修改的还是直接点击浏览器的前进后退按钮都会触发hashchange事件)

上面说的其实是在不支持historyapi情况下的实现原理。如果是支持historyapi情况下,hash模式的实现其实是和history模式一样的。

我们再来看看AbstractHistory类

AbstractHistory类
// src/history/abstract.js

...

export class AbstractHistory extends History {
  index: number
  stack: Array<Route>

  constructor (router: Router, base: ?string) {
    super(router, base)
    this.stack = []
    this.index = -1
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(
      location,
      route => {
        this.stack = this.stack.slice(0, this.index + 1).concat(route)
        this.index++
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(
      location,
      route => {
        this.stack = this.stack.slice(0, this.index).concat(route)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  go (n: number) {
    const targetIndex = this.index + n
    if (targetIndex < 0 || targetIndex >= this.stack.length) {
      return
    }
    const route = this.stack[targetIndex]
    this.confirmTransition(
      route,
      () => {
        const prev = this.current
        this.index = targetIndex
        this.updateRoute(route)
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })
      },
      err => {
        if (isNavigationFailure(err, NavigationFailureType.duplicated)) {
          this.index = targetIndex
        }
      }
    )
  }

  getCurrentLocation () {
    const current = this.stack[this.stack.length - 1]
    return current ? current.fullPath : '/'
  }

  ensureURL () {
    // noop
  }
} 

可以看到AbstractHistory类主要干了如下几件事。

  1. 继承于History类,并调用父类构造函数初始化。并对indexstack做了初始化。前面说过,非浏览器环境,是没有历史记录栈的,所以使用indexstack来模拟历史记录栈。

  2. 实现了go、push、replace等方法。

总结

可以看到,abstract模式并没有使用浏览器相关api,所以它不依赖于浏览器环境。路由历史记录的存储都是使用数据结构来实现的。一般用服务端渲染。

如果发现没有浏览器的 API,路由会自动强制进入这个模式。

总结

那么到此为止,我们分析了 Vue-Router 的实例化过程,首先初始化了一些参数。然后创建了matcher,并根据routes初始化了路由映射表RouteMap。最后确定路由模式来实例化路由。

后面我们再来分析初始化部分。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值