VueRouter源码解析

412 阅读11分钟

前置准备

使用说明文档:router.vuejs.org/zh/guide/#j…

源码下载地址:github.com/vuejs/vue-r…

分支:dev(不断更新中) 版本:3.5.2

思考点

我们在开始使用vue的时候,一定会接触到VueRouter,那同时我们可能会存在一些疑问点,比如:

  • Vue.use(VueRouter) 、new VueRouter()等操作是在做什么事情?
  • beforeRouterEnter中为什么获取不到Vue实例?
  • 使用路由守卫时为什么一定要调用next方法?
  • 为什么hash模式下打开localhost:8080会自动添加/#/?
  • 一次完整导航解析的流程是什么样的?
  • ...

当然,在我们长期使用后,肯定会有了深入的了解,接下来,我们会从源码角度去进行简单的说明。

基础使用

常用的路由功能

  • 路由信息 $route
  • 路由实例 $router
  • 路由跳转 router.pushrouter.push、router.replace
  • 视图渲染容器 router-view
  • 路由导航 router-link
  • 路由守卫 beforeEach...

下面这部分代码是我们对于VueRouter的基础使用示例

// route.ts
import Vue from 'vue'
import VueRouter from 'vue-router'

// 1.注册VueRouter插件 调用VueRouter.install方法
Vue.use(VueRouter)

// 2.创建router实例 传入对应配置
export default router = new VueRouter({
  mode: 'hash',
  routes:[
    { path: '/home', component: Home},
    { path: '/my', component: My}
  ]
})

// main.ts
import Vue from 'vue'
import router from './router'
import App from './App.vue'

// 3.在创建和挂载根实例时,将router实例作为参数传入到Vue中,使得整个应用都可使用该功能
new Vue({
  router,
  render: (h: (arg0: any) => any) => h(App)
}).$mount('#app')
<!-- App.vue -->
<template>
  <div id="app">
      <!-- 路由导航 -->
      <router-link to="/home">首页</router-link>
      <router-link to="/my">我的</router-link>
      <!-- 视图渲染容器 -->
      <router-view />
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

源码分析

插件注册

Vue.use(plugins)是vue注册插件的方法,该方法会检测插件是否有install方法,有则执行。

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

install源码解析

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


// 声明vue 方便其他文件使用
export let _Vue


// 插件注册方法
export function install (Vue) {
  // 避免多次注册插件
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  // 调用RouterView上绑定的data.registerRouteInstance方法
  // 该方法为当前路由记录RouteRecord添加了对应的实例instance
  // 即matched.instances[name] = callVal
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  // 全局混入beforeCreate和destroyed生命周期 为各组件绑定vue根实例
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) { 
        // 根组件 绑定_routerRoot为根实例
        this._routerRoot = this 
        // 声明_router,指向VueRouter实例
        this._router = this.$options.router 
        // 初始化VueRouter
        this._router.init(this) 
        // vue根实例响应式绑定_route 也就是当前路由信息Route 
        // 此处的核心在于:router-view组件依赖了该属性,而该属性是响应式声明,所以在路由变化的时候,视图就会更新
        Vue.util.defineReactive(this, '_route', this._router.history.current) 
      } else {
        // 非根组件 绑定_routerRoot为根实例
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this 
      }
     // 为当前路由记录RouteRecord添加了对应的实例instance
     // *** 保证后续执行beforeRouteEnter中next回调函数时有实例可用
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  // Vue原型初始化属性 $router(路由实例) $route(当前路由信息Route,实时变化)
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 注册路由相关组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats
}

VueRouter.init源码解析

init (app: any /* Vue component instance */) {
    this.apps.push(app)
    // 保证一个vue应用只初始化一次 app表示的是vue根实例
    if (this.app) {
      return
    }
    this.app = app

    const history = this.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)
        }
      }
      // 开启路由监听 history模式监听popstate hash模式支持history则监听popstate,否则监听hashchange
      const setupListeners = routeOrError => {
        // 监听popState或者hashChange事件触发后 重新导航一次 
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      // 完成首次导航,并开启路由监听
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

    // 调用history.listen方法,传入一个回调函数,该回调函数会在路由信息Route更新后执行
    // 即在路由信息Route更新后会同步更新vue根实例上的_route
    // _route属性被劫持,所以会通知相关依赖,其中包括RouterView组件,达到视图更新的效果
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
}

VueRouter实例

我们在创建VueRouter实例时,传入了一些配置,比如mode(路由模式)、routes(路由相关配置)等。那这些配置的实际用途是什么呢?接下来我们根据源码来了解下。

import VueRouter from 'vue-router'

export default router = new VueRouter({
  mode: 'hash',
  routes:[
    { path: '/home', component: Home},
    { path: '/my', component: My}
  ]
}

VueRouter实例源码解析

通过源码,我们可以看到在构造VueRouter实例时,我们会判断当前浏览器是否支持history模式,不支持则会回退到hash模式。同时,我们调用createMatcher方法创建路由映射表并返回matcher对象。

export default class VueRouter {
  // ...
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // ※※※ 创建路由映射表
    this.matcher = createMatcher(options.routes || [], this)
    // 处理路由模式 并创建对应模式的history实例
    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}`)
        }
    }
  }
}

matcher源码解析

createMatcher方法中调用createRouteMap方法,生成了pathList、pathMap、nameMap三个路由映射表。

  • pathList 路径列表 示例:['/my','/my/name','/my/detail/:id']
  • pathMap 路径与路由记录RouteRecord的映射 示例:{'/my':MyRoute,'/my/name':NameRoute}
  • nameMap 路由名称与路由记录RouteRecord的映射 示例:{'my':MyRoute,'name':NameRoute} 所以我们在命名路由名称时保持一个不重复的原则

matcher中包含了四个属性

  • match 根据目标跳转路由与当前路由信息Route,在路由映射表中进行匹配,并返回匹配到的路由信息Route
  • addRoute 动态添加单个路由,即往pathList、pathMap、nameMap路由映射表中添加数据
  • addRoutes 动态添加多个路由
  • getRoutes 获取路由记录RouteRecord列表 返回示例:[MyRoute,NameRoute]

该示例为实际的路由记录RouteRecord对象结构

image.png

该示例为实际的路由信息Route对象结构

image.png

createRouteMap

创建路由映射表,维护pathList、pathMap、nameMap数据。

addRoute和addRoutes动态添加路由方法的核心就是createRouteMap,也就是往路由映射表里新增配置数据。

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>,
  parentRoute?: RouteRecord
){
  // 若是首次创建路由映射表则直接默认为空,若是后期动态添加路由则需要传入历史的路由映射表,以保证在原有基础上添加
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  // 遍历创建VueRouter实例时传入的配置信息routes 进行路由记录RouteRecord的添加
  // 此处的route可能是嵌套路由即存在children,所以我们需要递归调用addRouteRecord
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })

  // 确保通配符*路由一定在pathList的最后
  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
  }
}
  

addRouteRecord

生成路由记录RouteRecord并添加到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 || {}
  // 格式化路径 若为一级路由则直接返回path 若非一级路由则父子路由拼接成完整可访问path
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  
  // *** 路由记录RouteRecord
  const record: RouteRecord = {
    path: normalizedPath,
    // 根据path解析出来的正则表达式扩展 在match方法中使用path匹配路由信息时用到
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), 
    // router.ts中routes配置的路由对应渲染的组件
    components: route.components || { default: route.component },
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    // 组件实例
    instances: {},
    // 收集beforeRouteEnter守卫中next的回调函数
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }

  // 若存在子路由则递归调用addRouteRecord方法
  // 从此处能看出来,我们是先递归添加子路由,再添加父路由,所以pathList的顺序是先子后父
  if (route.children) {
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 若不存在子路由 则直接向pathList、pathMap、nameMap中添加数据
  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
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/'
      )
    }
  }

  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    }
  }
}

match

macth方法根据参数raw(目标路由)以及currentRoute(当前路由信息Route)在nameMap或者pathMap中查找到对应的路由记录RouteRecord,调用_createRoute生成路由信息Route并返回。

raw是我们实际调用push或者replace等跳转路由方法传入的参数
形如'/my?param=123'或者是{name:'My',params:{param:123}}等。

function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    // 根据入参生成格式化后的目标路径信息 
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location
    
    // 我们会优先使用name去匹配 没有配置name再使用path匹配
    if (name) {
      const record = nameMap[name]
      // 未匹配到路由 _createRoute第一个参数是匹配到的路由记录RouteRecord
      // 此处传null最后生成对路由信息Route中的matched为[],也就是没有组件需要渲染,即页面空白
      if (!record) return _createRoute(null, location)

     // 获取所有必须的params。如果optional为true说明params不是必须的
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

      if (typeof location.params !== 'object') {
        location.params = {}
      }

      if (currentRoute && typeof currentRoute.params === 'object') {
        for (const key in currentRoute.params) {
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key]
          }
        }
      }
      // path和params整合返回一个真实的路径
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      // 返回匹配的路由信息Route
      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]
        // 使用路由记录RouteRecord的regex对目标路由进行匹配 若匹配则返回路由信息Route
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }    
    // 未匹配到路由
    return _createRoute(null, location)
}

看完match方法源码的解释,你是否会有一个疑问,为什么使用name直接去nameMap中匹配路由即可,而使用path匹配路由却需要遍历pathList呢?
这是因为我们配置中存在形如'/my/:id'的path路径,而这种路径在实际跳转时访问的是类似于'/my/123'带有实际参数的路径,如果我们直接使用pathMap去匹配是匹配不到的,所以我们需要遍历pathList,根据每一个path的正则以及目标路由的path和params去进行匹配,得到'/my/:id'对应的RouteRecord就是'/my/123'的对应的RouteRecord。

History

image.png

HashRouter实例

在创建HashRouter实例时,我们会检查url中是否包含/#/,没有则进行添加。同时HashRouter为我们提供了路由跳转能力,比如push、replace、go等,之后我们会一一说明。

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // 若是采用降级方案采用的hash模式 则会判断当前路径是否有/#/
    // 没有则会添加,并使用window.location.replace切换路径
    if (fallback && checkFallback(this.base)) {
      return
    }
    
    // 没有采用降级方案,直接采用的hash模式
    ensureSlash()
  }
}

结合上下两部分代码,我们能看到,首次创建HashRouter实例时,会调用checkFallback或者ensureSlash来确保hash模式下url中有/#/。 同时,这也就解释了为什么hash模式下打开localhost:8080会自动添加/#/。

function ensureSlash (): boolean {
  // 获取#后的字符串
  const path = getHash()
  // 若第一个字符是/ 则表示为正确的路由且有# 则不做处理
  if (path.charAt(0) === '/') {
    return true
  }
  // 若第一个字符是/ 则表示当前路由没有#或者#/字符
  // 拼接好目标path 调用replaceHash来完成路径切换
  replaceHash('/' + path)
  return false
}

HistoryRouter实例

HistoryRouter和HashRouter实现基本一致,差异在于HistoryRouter不会做容错处理,不会判断是否支持historyApi,而是直接使用。

路由导航

上面的内容,都是在做路由能力的准备工作,接下来,我们将依托于这些准备工作,来分析路由导航的实现流程。

go、forward、back

这三个方法最终调用的都是window.history.go(n)。那么这个方法是如何更新的视图呢?还记得我们在分析插件注册时,有说过VueRouter.init完成了首次导航,并开启路由监听吗?这就是它们更新视图的原因。

我们监听了window上的popstate或者是hashchange事件,这样在路由变化时,就会调用transtionTo方法完成对_route路由信息的更新,从而触发视图渲染。所以,在这种情况下,浏览器地址变化是在视图更新前的。

go (n) {
  this.history.go(n)
}


back () {
  this.go(-1)
}


forward () {
  this.go(1)
}

push、replace

push和replace方法的核心是类似的,也就是transitionTo,唯一不同之处在于导航切换完成后的回调中,push调用pushHash更新路由,replace调用replaceHash更新路由。

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        // 最后调用的是window.location.push或者history.pushState
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        // 最后调用的是window.location.replace或者history.replaceState
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
 }

transitionTo

不管是浏览器地址变化,还是调用push、replace方法跳转,核心都在于transitionTo方法,接下来,我们将重点分析该方法。

 transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    // 根据目标路由以及当前的路由信息Route调用match方法进行路由匹配
    // 返回目标路由的Route对象
    route = this.router.match(location, this.current)


    // 调用confirmTransition
    this.confirmTransition(
      route,
      () => {
        // ... 成功回调
      },
      err => {
        // ...失败回调
      }
    )
}

核心工具方法

首先,我们先分析整个流程中用到的核心方法,后续再分析流程时就不需要再打断思路了。

1.resolveQueue 该方法是我们根据传入的当前和目标路由RouteRecord来获取updated(需要更新的路由记录数组)、deactivated(需要销毁的路由记录数组)、activated(需要激活的路由记录数组)的。而这些数组是我们判断执行哪些路由守卫的依据。

image.png

function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  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)
  }
}

2.extractLeaveGuards、extractUpdateHooks、extractEnterGuards

这几个方法是我们用来获取beforeRouteLeave、beforeRouteUpdate、beforeRouteEnter守卫的。

// 虽然在处理beforeRouteLeave、beforeRouteUpdate和beforeRouteEnter
// 都调用了extractGuards提取守卫,但是传入的参数是不一致的
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

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

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

// 提取路由守卫
function extractGuards (
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
    // 获取到所有的守卫 调用flatMapComponents遍历records,然后将参数传给回调函数
    // def组件 instance组件实例 match路由记录RouteRecord数组 key组件名默认为default
    const guards = flatMapComponents(records, (def, instance, match, key) => {
    // 获取组件options下对应命名的路由守卫
    const guard = extractGuard(def, name)
    // 绑定this指向
    if (guard) {
      return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  return flatten(reverse ? guards.reverse() : guards)
}

// beforeRouteLeave、beforeRouteUpdate调用该方法
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
  if (instance) {
    return function boundRouteGuard () {
      // 绑定this指向为组件实例
      return guard.apply(instance, arguments)
    }
  }
}

// beforeRouteEnter调用该方法 
function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string
): NavigationGuard {
  return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {
      // 执行beforeRouteEnter守卫时,我们判断next方法传入的参数是否为函数
      // 若是函数 则将该回调函数维护在对应路由记录RouteRecord.enteredCbs中
      if (typeof cb === 'function') {
        if (!match.enteredCbs[key]) {
          match.enteredCbs[key] = []
        }
        match.enteredCbs[key].push(cb)
      }
      next(cb)
    })
}

为什么beforeRouterEnter守卫需要做特殊处理,且支持next传入回调函数呢?
众所周知,我们在该路由守卫中是没有办法获取到vue实例的,但我们是可以在next回调函数中获取到的。所以我们在提取beforeRouteEnter守卫时需要先将其next回调函数存储起来留作后续使用。

3.handleRouteRntered

批量处理beforeRouteEnter守卫中next传入的回调函数。

// route为目标路由信息
function handleRouteEntered (route) {
  // 遍历目标路由匹配到的所有RouteRecord
  for (var i = 0; i < route.matched.length; i++) {
    var record = route.matched[i];
    // 实例存在则处理
    for (var name in record.instances) {
      var instance = record.instances[name];
      // 获取到我们之前在bindEnterGuard中存储的回调函数
      var cbs = record.enteredCbs[name];
      if (!instance || !cbs) { continue }
      delete record.enteredCbs[name];
      // 若回调函数存在 则调用,且传入instance也就是组件实例作为入参
      // 所以这就是为什么我们能在beforeRouteEnter的next回调函数中获取到组件实例的原因
      for (var i$1 = 0; i$1 < cbs.length; i$1++) {
        if (!instance._isBeingDestroyed) { cbs[i$1](instance); }
      }
    }
  }
}

4.runQueue

该方法将传入的路由守卫队列同步依次执行。

// queue路由守卫队列 fn迭代器 cb是队列执行完毕后执行的回调函数
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        // 当路由守卫存在时,则执行迭代器 传入路由守卫函数以及进入下一个迭代的回调函数
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

// 迭代器 hook路由守卫函数 next进入下一个迭代的回调函数
const iterator = (hook: NavigationGuard, next) => {
  if (this.pending !== route) {
    return abort(createNavigationCancelledError(current, route))
  }
  try {
    // 路由守卫函数 目标路由信息 当前路由信息 next回调函数
    // *** 假设我们在调用路由守卫时不执行next,那就导致我们没有调用runQueue的step方法
    // 队列也就停止往后执行。所以这就是我们为什么一定要执行next的原因
    hook(route, current, (to: any) => {
      if (to === false) {
        this.ensureURL(true)
        abort(createNavigationAbortedError(current, route))
      } else if (isError(to)) {
        this.ensureURL(true)
        abort(to)
      } else if (
        typeof to === 'string' ||
        (typeof to === 'object' &&
          (typeof to.path === 'string' || typeof to.name === 'string'))
      ) {
        abort(createNavigationRedirectedError(current, route))
        if (typeof to === 'object' && to.replace) {
          this.replace(to)
        } else {
          this.push(to)
        }
      } else {
        // *** 调用next方法使得队列进入下一个迭代
        next(to)
      }
    })
  } catch (e) {
    abort(e)
  }
}

5.updateRoute

更新当前路由信息Route。

updateRoute (route: Route) {
    // 更新当前路由信息
    this.current = route
    // 你是否还记得,在插件注册时调用了VueRouter.init方法,其中调用了history.listen
    // 将其回调函数赋值给了cb 所以此处我们执行的就是app._route=route
    // 由于_route是响应式声明的,所以会通知对应的依赖
    // 其中包含RouterView,也就完成了视图的渲染更新
    this.cb && this.cb(route)
}

// VueRouter
init(){
    // ...
    history.listen(route => {
          this.apps.forEach(app => {
            app._route = route
          })
    })
}

confirmTransition

1.判断是否为相同路由,若是则取消导航。

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()
  return abort(createNavigationDuplicatedError(current, route))
}

2.调用resolveQueue方法,获取updated、deactivated、activated。

const { updated, deactivated, activated } = resolveQueue(
    this.current.matched,
    route.matched
)

3.逐一提取路由守卫,并将他们合并到同一个队列queue中。

通过下面这段源码,我们能清晰的分析出来,各个路由守卫应该写在哪里

  • 组件内部:beforeRouteLeave、beforeRouteUpdate
  • VueRouter实例上:beforeEach
  • 路由配置routes中:beforeEnter
const queue: Array<?NavigationGuard> = [].concat(
      // 销毁组件的beforeRouteLeave守卫
      extractLeaveGuards(deactivated),
      // 全局beforeEach守卫
      this.router.beforeHooks,
      // 更新组件的beforeRouteUpdate守卫
      extractUpdateHooks(updated),
      // 激活组件的beforeEnter守卫
      activated.map(m => m.beforeEnter),
      // 异步加载的激活组件
      resolveAsyncComponents(activated)
)

4.调用runQueue方法顺序依次执行queue队列。

// 传入执行队列、迭代器、成功回调参数
// 将队列中每一个路由守卫函数传给迭代器,在迭代器中执行路由守卫
// 并且路由守卫中必须调用next方法,队列才会进入下一个迭代
// 迭代完成后,调用该成功回调
runQueue(queue, iterator, () => {
      // 激活组件可能包含需要异步加载的,为了保证获取到所有激活组件beforeRouteEnter守卫
      // 我们在第一个队列迭代完成后,再开启一个新的队列进行迭代
      const enterGuards = extractEnterGuards(activated)
      // beforeRouterEnter、beforeResolve路由守卫
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null
        // 执行成功回调 更新当前路由信息Route
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            // 执行beforeRouteEnter路由守卫中next传入的回调函数
            handleRouteEntered(route)
          })
        }
      })
})

5.两个队列迭代完成后,调用confirmTransition的成功回调函数。

this.confirmTransition(
      route,
      () => {
        // 更新路由信息 触发视图更新
        this.updateRoute(route)
        // 调用transitionTo回调函数:
        // 即调用pushHash/replaceHash方法切换路由,执行push传入的成功回调函数
        onComplete && onComplete(route)
        this.ensureURL()
        // 执行afterEach路由守卫
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })


        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        // ...    
      }
 })

流程图

至此,我们就把push或者replace的流程说完了。现在我们来回顾下push方法的整个调用流程,如图:

push和replace是先更新的_route及视图,再更新的浏览器地址;而go是先更新的浏览器地址,再更新的_route及视图。

image.png

导航解析完成流程

beforeRouteEnter中获取不到实例原因解析:
beforeRouteEnter路由守卫在视图更新前执行,registerInstance方法声明在RouterView,而我们调用registerInstance方法是在视图更新后,beforeCreate中。
所以我们在beforeRouteEnter中肯定没有办法获取到实例instance。而执行beforeRouteEnter路由守卫中next回调是在$nextTick中,也就是异步执行,这个时候beforeCreate已经执行完毕,所以此时可以获取到实例instance。

image.png

总结

image.png

至此,VueRouter源码部分已经大致分析完成,最后梳理出一张思维导图供给大家参考。当然vue3现在也已经走入我们的视野,相应的VueRouter版本也进行了升级,使用方法上也有了一定的变化,感兴趣的可以做下两个版本的比较和学习。