(一)小菜鸡Vue-Router源码阅读——Vue-Router的初始化和挂载

477 阅读5分钟

本次阅读的vue-router源码版本是3.5.3

0.目录结构

  1. compoents下是自带的两个组件
    1. <route-link>
    2. <route-view>
  2. history下是根据不同的mode来创建不同的路由实例
  3. util是工具函数

image.png

1.入口

index.js

export default class VueRouter {}

VueRouter.install = install
VueRouter.version = '__VERSION__'
VueRouter.isNavigationFailure = isNavigationFailure
VueRouter.NavigationFailureType = NavigationFailureType
VueRouter.START_LOCATION = START

1. Vue.use(VueRouter)

一下是官方给出的简单使用范例

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

Vue.use(VueRouter)

const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

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

// 创建路由实例
const router = new VueRouter({
  routes 
})

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

Vue.use(VueRouter)是执行他的VueRouter.install方法,在之前的源码阅读中已说明,这里不作赘述

install

  1. 防止重复注册
  2. Vue.mixin混入beforeCreatedestroyed全局钩子
  3. 代理vue实例上的$router$route
  4. 注册组件<route-link><route-view>
  5. 获取到vue的合并策略,给vue-router自己的钩子函数,这样在Vue初始化过程mergeOptions过程中会合并路由守卫钩子
// src/install.js
export let _Vue

export function install (Vue) {
  // 防止重复注册
  if (install.installed && _Vue === Vue) return
  install.installed = true
  _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)
    }
  }

  // 混入全局 beforeCreate,destoryed
  Vue.mixin({
    beforeCreate () {
      // 根组件创建的时候会传router
      if (isDef(this.$options.router)) {
        // vm实例
        this._routerRoot = this
        // 这里是 new VueRouter的实例
        this._router = this.$options.router
        // 用该实例的init方法初始化,后面分析改方法
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 不是根组件就往上找
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  // 代理$router,$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)

  // 注册合并策略,这里就会用vue内置的created的合并策略去合并vue-router内置的钩子
  const strats = Vue.config.optionMergeStrategies
  strats.beforeRouteEnter 
  = strats.beforeRouteLeave 
  = strats.beforeRouteUpdate
  = strats.created
}

2. new VueRouter()

VueRouter类

该类声明在index.js中,且它正是默认导出

构造函数

  1. 声明初始化内部变量
  2. mode根据环境做调整
  3. 根据mode值创建相关实例挂载到this.history
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)

        // 对mode做处理,默认hash
        let mode = options.mode || 'hash'
        // 浏览器不支持pushState时对history做退化处理
        this.fallback =
          mode === 'history' && !supportsPushState && options.fallback !== false
        if (this.fallback) {
          mode = 'hash'
        }
        // 不是浏览器环境
        if (!inBrowser) {
          mode = 'abstract'
        }
        this.mode = 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}`)
            }
        }
     }
      
  /* install 方法会调用 init 来初始化 */
  init(app: any /* Vue组件实例 */) { }
  
  /* createMatcher 方法返回的 match 方法 */
  match(raw: RawLocation, current?: Route, redirectedFrom?: Location) { }
  
  /* 当前路由对象 */
  get currentRoute() { }
  
  /* 注册 beforeHooks 事件 */
  beforeEach(fn: Function): Function { }
  
  /* 注册 resolveHooks 事件 */
  beforeResolve(fn: Function): Function { }
  
  /* 注册 afterHooks 事件 */
  afterEach(fn: Function): Function { }
  
  /* onReady 事件 */
  onReady(cb: Function, errorCb?: Function) { }
  
  /* onError 事件 */
  onError(errorCb: Function) { }
  
  /* 调用 transitionTo 跳转路由 */
  push(location: RawLocation, onComplete?: Function, onAbort?: Function) { }
  
  /* 调用 transitionTo 跳转路由 */
  replace(location: RawLocation, onComplete?: Function, onAbort?: Function) { }
  
  /* 跳转到指定历史记录 */
  go(n: number) { }
  
  /* 后退 */
  back() { }
  
  /* 前进 */
  forward() { }
  
  /* 获取路由匹配的组件 */
  getMatchedComponents(to?: RawLocation | Route) { }
  
  /* 根据路由对象返回浏览器路径等信息 */
  resolve(to: RawLocation, current?: Route, append?: boolean) { }
  
  /* 动态添加路由 */
  addRoutes(routes: Array<RouteConfig>) { }

}

我们知道初始化实例的时候routes路由表是最重要的东西,在构造函数中通过creareMatcher函数创建了一个matcher挂载到了实例属性matcher上,其实这个时候routes已经通过闭包的方式保存了,下面深入到createMatcher

this.matcher = createMatcher(options.routes || [], this)

createMatcher 实际就声明了一些方法然后将四个方法暴露出来,通过createRouteMaproutes路由表转化后闭包保存,所以继续深入到createRouteMap

// src/create-matcher.js
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  
  function addRoutes (routes) {}
  function addRoute (parentOrRoute, route) {}
  function getRoutes () {}
  function match (){}
  function redirect (){}
  function alias (){}
  function _createRoute (){}
  
  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

createRouteMap

  1. 保存更新前状态
  2. 迭代路由表用addRouteRecord构造路由,所以继续深入到addRouteRecord
  3. *通配符匹配路由放到最后
  4. 返回pathListpathMapnameMap
// src/create-route-map
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.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
  }
}

addRouteRecord

  1. 标准化path
  2. 创建当前的路由对象record,这里动态路由匹配的情况需要创建一个正则表达式,这里vue-router 使用 path-to-regexp (opens new window)作为路径匹配引擎,所以支持很多高级的匹配模式,这里不做深入探讨
  3. 递归创建嵌套的子路由
  4. 注册pathMappathList
  5. 注册别名路由
  6. 注册nameMap

我们知道pushreplace的编程式导航中的第一个参数location对象可以是namepath来跳转到指定路由,那么分别对应了nameMappathMap两个映射表

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
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
  // 匹配大小写是否敏感
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  // 创建路由对象
  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
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }

  // 嵌套路由递归调用
  if (route.children) {
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      // 第五个参数是parent,指向当前record
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  // 看到这里我们明白了,原来pathMap是保存path到路由对象的映射的
  if (!pathMap[record.path]) {
    // 这个数组相当于是Object.keys(pathMap)
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  // 注册别名
  // 别名可以让路由的配置不局限于嵌套的路由,可以规范化url的配置
  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
      )
    }
  }

  // nameMap是路由表name到record的映射
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    }
  }
}

import Regexp from 'path-to-regexp'
function compileRouteRegex (
  path: string,
  pathToRegexpOptions: PathToRegexpOptions
): RouteRegExp {
  const regex = Regexp(path, [], pathToRegexpOptions)
  return regex
}

function normalizePath (
  path: string,
  parent?: RouteRecord,
  strict?: boolean
): string {
  if (!strict) path = path.replace(/\/$/, '')
  if (path[0] === '/') return path
  if (parent == null) return path
  return cleanPath(`${parent.path}/${path}`)
}

init方法

前面我们知道在install中的Vue.mixinbeforeCreate钩子中执行了该方法

  1. 维护apps属性,是一个vue实例数组,注册时添加,vue实例销毁时删除
  2. 防止重复调用init
  3. 跳转到history.getCurrentLocation()然后调用setupListeners,后面分析history实例的时候会讲到该方法的实现
init (app: any /* Vue component instance */) {
    // app实际是vue实例
    this.apps.push(app)
    app.$once('hook:destroyed', () => {
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      if (this.app === app) this.app = this.apps[0] || null
      if (!this.app) this.history.teardown()
    })
    // 防止重复初始化
    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)
        }
      }
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

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

编程式导航

官方文档中给出的编程式导航就是该实例的三个方法

  1. router.push(location, onComplete?, onAbort?)
  2. router.replace(location, onComplete?, onAbort?)
  3. router.go(n) 当我们这三个API的实现实际上是依赖于不同mode下创建的history实例的API来的,所以我们研究这三个API还是得深入到各个类(AbstractHistory,HashHistory,HTML5History)中的方法,以下贴出VueRouter中的原型方法

push

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

replace

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
  }

go

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

HTML5History类

构造函数

构造函数中初始化了_startLocation

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    super(router, base)
    this._startLocation = getLocation(this.base)
  }

  setupListeners () {}

  go (n: number) {}

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {}

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {}

  ensureURL (push?: boolean) {}

  getCurrentLocation (): string {}
}

export function getLocation (base: string): string {
  let path = window.location.pathname
  const pathLowerCase = path.toLowerCase()
  const baseLowerCase = base.toLowerCase()

  if (base && ((pathLowerCase === baseLowerCase) ||
    (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
    path = path.slice(base.length)
  }
  // 这里的path去掉base
  return (path || '/') + window.location.search + window.location.hash
}

setupListeners

该函数主要做的工作是监听了popstate事件(popstate - Web API 接口参考 | MDN (mozilla.org)),history.pushStatehistory.replaceState不会触发该事件,所以该事件实际上是为了实现监听用户点击浏览器的回退前进按钮的

 setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    // 看下面setupScroll的实现,讲解push和replace会解释为什么要全局维护这个key
    // 因为它和每个页面的position信息相关
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current
      // 这里会获取到当前的地址栏url
      const location = getLocation(this.base)
      // 有些浏览器在第一次进入就触发popState事件,这不是我们想要的
      if (this.current === START && location === this._startLocation) {
        return
      }
      // 跳转到url
      this.transitionTo(location, route => {
        if (supportsScroll) {
          // 处理滚动
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }
  
// src/util/scroll.js
export function setupScroll () {
  if ('scrollRestoration' in window.history) {
    window.history.scrollRestoration = 'manual'
  }
   
  const protocolAndPath = window.location.protocol + '//' + window.location.host
  const absolutePath = window.location.href.replace(protocolAndPath, '')
  const stateCopy = extend({}, window.history.state)
  stateCopy.key = getStateKey()
  window.history.replaceState(stateCopy, '', absolutePath)
  window.addEventListener('popstate', handlePopState)
  return () => {
    window.removeEventListener('popstate', handlePopState)
  }
}

function handlePopState (e) {
  saveScrollPosition()
  if (e.state && e.state.key) {
    setStateKey(e.state.key)
  }
}

在此顺便说一下这个this.listeners.push()什么时候会调用,看History父类定义的teardown,像是一个简单的发布订阅模式,但是我们看一下这个数组中存到函数都是做什么的?

teardown () {
    this.listeners.forEach(cleanupListener => {
      cleanupListener()
    })
    this.listeners = []
    this.current = START
    this.pending = null
  }

还记得在init初始化中调用了teardown,相当于当所有的注册的vue实例都销毁了那么就把当前vuerouter实例也重置了,将监听事件取消,所以listeners中的函数都是取消监听的函数

init(app){
      app.$once('hook:destroyed', () => {
          const index = this.apps.indexOf(app)
          if (index > -1) this.apps.splice(index, 1)
          if (this.app === app) this.app = this.apps[0] || null
          if (!this.app) this.history.teardown()
      })
}

push、replace、go

pushreplace唯一的不同就是一个调用的replaceState一个是pushState

import { pushState, replaceState, supportsPushState } from '../util/push-state'

 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 (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)
  }
  
  // 会触发popState事件
  go (n: number) {
    window.history.go(n)
  }

下面看一下pushStatereplaceState的实现,主要是用的原生historypushStatereplaceState的两个API,这个两个API特点是会修改浏览器地址栏内容,但是不会发出实质的请求,而且会修改页面栈,这也history模式实现的关键。 这两个原生API的使用方法

  1. History.pushState() - Web API 接口参考 | MDN (mozilla.org)
  2. History.replaceState() - Web API 接口参考 | MDN (mozilla.org)
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()

  const history = window.history
  try {
    if (replace) {
      // 这个状态用户可能原来就有东西,我们只是在原来的对象上增加一个唯一的key值
      // 所以需要先将原来的state进行拷贝
      const stateCopy = extend({}, history.state)
      // replace时key值不变
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      // push需要改变key值
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    // 异常处理用localtion来强制跳转
    // 因为有些浏览器可能会限制页面栈数量
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

// 通过key值来map滚动状态,这个key值经过模块化封装是全局唯一的
export function saveScrollPosition () {
  const key = getStateKey()
  if (key) {
    positionStore[key] = {
      x: window.pageXOffset,
      y: window.pageYOffset
    }
  }
}

// src/util/state-key.js
export function genStateKey (): string {
  return Time.now().toFixed(3)
}

let _key: string = genStateKey()

export function getStateKey () {
  return _key
}

export function setStateKey (key: string) {
  return (_key = key)
}

transitionTo

pushreplace方法都是基于transitionTo方法,这个方法定义在其父类History

  1. match到需要跳转的路由对象
  2. this.confirmTransition,继续深入这个方法
// src/history/base.js
transitionTo (location: RawLocation,onComplete?: Function,onAbort?: Function) {
    let route
    try {
      // 通过location来匹配到我们要跳转的路由
      route = this.router.match(location, this.current)
    } catch (e) {
      // 执行异常回调函数
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      throw e
    }
    // 保存跳转前路由
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(route)
        // 执行用户传入的回调
        onComplete && onComplete(route)
        this.ensureURL()
        // 执行全局后置钩子,对应下面提到的(10)
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })

        
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          // 执行用户的回调
          onAbort(err)
        }
        if (err && !this.ready) {
          if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
            this.ready = true
            this.readyErrorCbs.forEach(cb => {
              cb(err)
            })
          }
        }
      }
    )
  }

confirmTransition

这个函数中前面是一对const的声明定义,最后执行了一个runQueue,我们可以先看一下runQueue的实现,可以看我的注释中说明了,实际上是一个迭代器,又因为传入的fn实际上调用了vue$nextTick使得这个队列实际上是异步执行的,这也就说明了为什么定义runQueue方法的文件名叫async.js

runQueue

// src/util/async.js
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    // 队列执行完毕,执行回调函数
    if (index >= queue.length) {
      cb()
    } else {
      // 队列依次执行
      if (queue[index]) {
        // 用传入的fn执行,执行完毕后执行队列的下一个
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        // 队列中元素undefined直接跳过
        step(index + 1)
      }
    }
  }
  step(0)
}

先看一下官方给出的完整导航解析流程

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

runQueue的三个参数来分析

  1. queue:其实是一堆钩子依次执行,我们发现resolveQueue函数的返回值中可以解构出updated, deactivated, activated,换句话说我们通过比较当前路由目标路由matched才能确定到底该执行哪些钩子,因为这两个路由有可能存在嵌套关系,这样一来我们需要决定该执行哪些组件钩子的beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave,可以看下面对该函数的注释分析
    1. extractLeaveGuards(deactivated):对应上述(2)
    2. this.router.beforeHooks:对应上述(3)
    3. extractUpdateHooks(updated):对应上述(4)
    4. activated.map(m => m.beforeEnter):对应上述(5)
    5. resolveAsyncComponents(activated):对应上述(6)
  2. fn:就是一个迭代器,在该迭代器中包裹try...catch来使用abort回调,执行hook(to,from,next),且主要对next参数做了一些判断处理
  3. cb:在hook迭代结束后最后执行,在这里重新执行了两组钩子,执行完成后执行完成后的回调函数,也就是在这个时候才真正的pushState或replaceState了
    1. extractEnterGuards(activated):对应上述(7)
    2. this.router.resolveHooks:对应上述(8)
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
   
    // 当要跳转的路由和当前一样
    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 
    }

    // 判断执行哪些组件内的钩子
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )

    const queue: Array<?NavigationGuard> = [].concat(
      // 当前组件失活钩子
      extractLeaveGuards(deactivated),
      // 全局路由前置钩子,通过router.beforeEach注册
      this.router.beforeHooks,
      // 组件更新钩子
      extractUpdateHooks(updated),
      // 组件激活钩子
      activated.map(m => m.beforeEnter),
      // 全局resolve钩子
      resolveAsyncComponents(activated)
    )

    // 为了代码清晰我删除了一些abort的回调
    const iterator = (hook: NavigationGuard, next) => {
      // ...
      try {
        // 这里对应着钩子定义的to,from,next参数
        hook(route, current, (to: any) => {
          if (to === false) {
            // 这意味着终止跳转
            this.ensureURL(true)
          } else if (isError(to)) {
            this.ensureURL(true)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // 下一个钩子
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }

    // 第三个是钩子全都执行完毕之后的回调
    runQueue(queue, iterator, () => {
      // 组件激活钩子对应上述(7)
      const enterGuards = extractEnterGuards(activated)
      // 全局解析后守卫对应上述(8)
      const queue = enterGuards.concat(this.router.resolveHooks)
      // 对这两组钩子再进行循环
      runQueue(queue, iterator, () => {
        // 终于完成了,执行完成的回调函数
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            handleRouteEntered(route)
          })
        }
      })
    })
  }
  
  
function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  // 要找到两个matched数组的最大值
  const max = Math.max(current.length, next.length)
  // 对匹配到的全部路径进行迭代
  for (i = 0; i < max; i++) {
    // 一旦匹配到不一致跳出循环,这也就意味着i记录着两个路由最后分道扬镳的路口
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    // 也就是说前面相同的部分是更新
    updated: next.slice(0, i),
    // 后面不同的部分是第一次进入
    activated: next.slice(i),
    // 当前后面的部分即将失活
    deactivated: current.slice(i)
  }
}

HashHistory类

同样继承自History类,且实现了其声明的接口

构造函数

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    if (fallback && checkFallback(this.base)) {
      return
    }
    // 初始化跳转
    ensureSlash()
  }
}

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  let href = window.location.href
  const index = href.indexOf('#')
  if (index < 0) return ''
  href = href.slice(index + 1)
  return href
}

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

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

setupListeners

基本和history模式差不多,当popState不支持的时候用hashChange事件代替

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)
        }
      })
    }
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }

push、replace、go

history基本一样,也就是最后用的API在不支持pushState的时候换了一下

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

3.总结

主要有以下类

  1. VueRouter
    • history:在构造函数中创建了相应模式的对象
    • matcher:闭包方式存储了routes路由表,和返回了三个方法
    • match:就是调用matcher.match
    • init:在Vue钩子beforeCreate中调用,注册popstatehashchange事件,注册vue实例
    • pushreplacego:实际上都是在调用history的方法
    • backforwardgo的“语法糖”
    • install(静态方法):Vue混入、注册组件、注册组件钩子、代理$router$route对象
  2. History: 作为HTML5History、HashHistory、AbstractHistory的父类
    • transitionTo:获取到匹配到的路由,调用confirmTransition
    • confirmTransition:调用不同的钩子,执行pushreplace的回调
  3. HTML5History
    • setupListeners:监听popstate事件
    • push:pushState
    • replace:replaceState
    • go:window.history.go
  4. HashHistory:同上
    • setupListeners:监听hashchange事件
    • push:window.location.hash
    • replace:window.location.replace
    • go:window.history.go
  5. AbstractHistory
    • push
    • replace
    • go

至于router.matcher中实现的方法有时间再做深入分析

因为笔者是边看边写的笔记,所以看的肯定是不足的望大佬指正,下一篇会再看Vue-Router内置的两个组件~