vue-router4源码解析(3)

1,153 阅读9分钟

这是我参与更文挑战的第8天,活动详情查看更文挑战

本文将接着之前分析vue-router4的routers属性和安装VueRouter实例

routes 属性

回到createRouter方法中,可以看到该方法中只有一个地方用到了options.routes,它作为createRouterMatcher参数,执行后返回一个RouterMatcher类型的对象

export function createRouter(options: RouterOptions): Router {
  const matcher = createRouterMatcher(options.routes, options)
  // ....
}

matcher模块的入口是src/matcher/index.ts,该模块提供了路由配置相关的属性和方法,matcher接口定义如下

// src/matcher/index.ts
interface RouterMatcher {
  addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
  removeRoute: {
    (matcher: RouteRecordMatcher): void
    (name: RouteRecordName): void
  }
  getRoutes: () => RouteRecordMatcher[]
  getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined
  resolve: (
    location: MatcherLocationRaw,
    currentLocation: MatcherLocation
  ) => MatcherLocation
}

createRouterMatcher函数的基本逻辑简化后的代码如下

function createRouterMatcher(
  routes: RouteRecordRaw[],
  globalOptions: PathParserOptions
): RouterMatcher {
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  globalOptions = mergeOptions(
    { strict: false, end: true, sensitive: false } as PathParserOptions,
    globalOptions
  )

  function getRecordMatcher(name: RouteRecordName) {
    // ...
  }

  function addRoute(
    record: RouteRecordRaw,
    parent?: RouteRecordMatcher,
    originalRecord?: RouteRecordMatcher
  ) {
    // ...
  }

  function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
    // ...
  }

  function getRoutes() {
    // ...
  }

  function insertMatcher(matcher: RouteRecordMatcher) {
    // ...
  }

  function resolve(
    location: Readonly<MatcherLocationRaw>,
    currentLocation: Readonly<MatcherLocation>
  ): MatcherLocation {
    // ...
  }

  // add initial routes
  routes.forEach(route => addRoute(route))

  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

该函数接收两个参数,第一个参数是路由配置数组,第二个参数是VueRouter初始化时传进来的options。然后声明两个变量matchers和matcherMap,然后是声明一系列方法,在return之前,遍历routes,通过addRoute方法,将路由配置转化为matcher

现在来逐个分析这几个方法

  • addRoute方法
function addRoute(
  record: RouteRecordRaw,
  parent?: RouteRecordMatcher,
  originalRecord?: RouteRecordMatcher
) {
  let isRootAdd = !originalRecord
  let mainNormalizedRecord = normalizeRouteRecord(record)
  mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
  const options: PathParserOptions = mergeOptions(globalOptions, record)
  const normalizedRecords: typeof mainNormalizedRecord[] = [
    mainNormalizedRecord,
  ]
  if ('alias' in record) {
    const aliases =
      typeof record.alias === 'string' ? [record.alias] : record.alias!
    for (const alias of aliases) {
      normalizedRecords.push(
        assign({}, mainNormalizedRecord, {
          components: originalRecord
            ? originalRecord.record.components
            : mainNormalizedRecord.components,
          path: alias,
          aliasOf: originalRecord
            ? originalRecord.record
            : mainNormalizedRecord,
        }) as typeof mainNormalizedRecord
      )
    }
  }

  let matcher: RouteRecordMatcher
  let originalMatcher: RouteRecordMatcher | undefined

  for (const normalizedRecord of normalizedRecords) {
    let { path } = normalizedRecord
    if (parent && path[0] !== '/') {
      let parentPath = parent.record.path
      let connectingSlash =
        parentPath[parentPath.length - 1] === '/' ? '' : '/'
      normalizedRecord.path =
        parent.record.path + (path && connectingSlash + path)
    }
    matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
    if (originalRecord) {
      originalRecord.alias.push(matcher)
    } else {
      originalMatcher = originalMatcher || matcher
      if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
      if (isRootAdd && record.name && !isAliasRecord(matcher))
        removeRoute(record.name)
    }

    if ('children' in mainNormalizedRecord) {
      let children = mainNormalizedRecord.children
      for (let i = 0; i < children.length; i++) {
        addRoute(
          children[i],
          matcher,
          originalRecord && originalRecord.children[i]
        )
      }
    }
    originalRecord = originalRecord || matcher
    insertMatcher(matcher)
  }

  return originalMatcher
    ? () => {
        removeRoute(originalMatcher!)
      }
    : noop
}

这个方法要做的事情就是,根据路由配置对象,创建一个matcher对象,然后添加到matchers数组中,最后根据originalMatcher条件return一个移除路由方法或者noop(let noop = () => {})。应用中传进来的路由配置是不完整的,所以需要首先通过normalizeRouteRecord方法对路由配置格式化,生成完整的路由配置对象,其中props属性通过normalizeRecordProps函数格式化,格式化对象是根据路由配置对象中的component或者components生成,如果存在component属性,则props对象包含一个default属性,并赋值为配置中的props,否则取components对象中的key,并从路由配置属性props中取对应的值

function normalizeRouteRecord(
  record: RouteRecordRaw
): RouteRecordNormalized {
  return {
    path: record.path,
    redirect: record.redirect,
    name: record.name,
    meta: record.meta || {},
    aliasOf: undefined,
    beforeEnter: record.beforeEnter,
    props: normalizeRecordProps(record),
    children: record.children || [],
    instances: {},
    leaveGuards: new Set(),
    updateGuards: new Set(),
    enterCallbacks: {},
    components:
      'components' in record
        ? record.components || {}
        : { default: record.component! },
  }
}

function normalizeRecordProps(
  record: RouteRecordRaw
): Record<string, _RouteRecordProps> {
  const propsObject = {} as Record<string, _RouteRecordProps>
  const props = (record as any).props || false
  if ('component' in record) {
    propsObject.default = props
  } else {
    for (let name in record.components)
      propsObject[name] = typeof props === 'boolean' ? props : props[name]
  }

  return propsObject
}

调用normalizeRouteRecord方法格式化路由配置对象后,将处理后的mainNormalizedRecord对象添加到normalizedRecords数组中

接着判断'alias' in record,如果有别名,则添加记录到normalizedRecords数组,基本逻辑是复制mainNormalizedRecord,然后重新设置components,path,aliasOf属性,换句话说,别名的实现原理是通过拷贝记录,并调整部分属性得到新的记录

以上代码都是为接下来创建matcher做准备,继续往下分析代码,首先准备两个变量:matcher、originalMatcher,然后开始遍历normalizedRecords

在遍历的内部,根据路由配置对象,创建matcher,并插入到matchers中,分为以下几步:

  • 1、如果是子路由配置,且path不是以/开头,则将父路由的path和子路由的path加起来生成完整path

  • 2、调用createRouteRecordMatcher方法,创建matcher对象,如果存在parent,则在parent.children中添加当前matcher对象

function createRouteRecordMatcher(
  record: Readonly<RouteRecord>,
  parent: RouteRecordMatcher | undefined,
  options?: PathParserOptions
): RouteRecordMatcher {
  const parser = tokensToParser(tokenizePath(record.path), options)
  const matcher: RouteRecordMatcher = assign(parser, {
    record,
    parent,
    children: [],
    alias: [],
  })

  if (parent) {
    if (!matcher.record.aliasOf === !parent.record.aliasOf)
      parent.children.push(matcher)
  }

  return matcher
}

matcher对象类型为RouteRecordMatcher,它继承自PathParser接口,所以一个matcher对象应该包含以下属性和方法,前面五个属性或方法是通过tokensToParser(tokenizePath(record.path), options)创建的,下面将在该方法中分析这几个属性或方法的实现逻辑

-re: RegExp

  • score: Array<number[]>
  • keys: PathParserParamKey[]
  • parse(path: string): PathParams | null
  • stringify(params: PathParams): string
  • record: RouteRecord 保存格式化后的路由配置记录
  • parent: RouteRecordMatcher | undefined 保存父路由matcher对象
  • children: RouteRecordMatcher[] 子路由,初始化为空数组
  • alias: RouteRecordMatcher[] 别名,初始化为空数组 在分析tokensToParser之前,需要先看一下tokenizePath(record.path),该方法将path转化为一个token数组。
export const enum TokenType {
  Static,
  Param,
  Group,
}

const enum TokenizerState {
  Static,
  Param,
  ParamRegExp, // custom re for a param
  ParamRegExpEnd, // check if there is any ? + *
  EscapeNext,
}

interface TokenStatic {
  type: TokenType.Static
  value: string
}

interface TokenParam {
  type: TokenType.Param
  regexp?: string
  value: string
  optional: boolean
  repeatable: boolean
}

interface TokenGroup {
  type: TokenType.Group
  value: Exclude<Token, TokenGroup>[]
}

export type Token = TokenStatic | TokenParam | TokenGroup

const ROOT_TOKEN: Token = {
  type: TokenType.Static,
  value: '',
}

const VALID_PARAM_RE = /[a-zA-Z0-9_]/
// After some profiling, the cache seems to be unnecessary because tokenizePath
// (the slowest part of adding a route) is very fast

// const tokenCache = new Map<string, Token[][]>()

export function tokenizePath(path: string): Array<Token[]> {
  if (!path) return [[]]
  if (path === '/') return [[ROOT_TOKEN]]
  if (!path.startsWith('/')) {
    throw new Error(
      __DEV__
        ? `Route paths should start with a "/": "${path}" should be "/${path}".`
        : `Invalid path "${path}"`
    )
  }

  // if (tokenCache.has(path)) return tokenCache.get(path)!

  function crash(message: string) {
    throw new Error(`ERR (${state})/"${buffer}": ${message}`)
  }

  let state: TokenizerState = TokenizerState.Static
  let previousState: TokenizerState = state
  const tokens: Array<Token[]> = []
  // the segment will always be valid because we get into the initial state
  // with the leading /
  let segment!: Token[]

  function finalizeSegment() {
    if (segment) tokens.push(segment)
    segment = []
  }

  // index on the path
  let i = 0
  // char at index
  let char: string
  // buffer of the value read
  let buffer: string = ''
  // custom regexp for a param
  let customRe: string = ''

  function consumeBuffer() {
    if (!buffer) return

    if (state === TokenizerState.Static) {
      segment.push({
        type: TokenType.Static,
        value: buffer,
      })
    } else if (
      state === TokenizerState.Param ||
      state === TokenizerState.ParamRegExp ||
      state === TokenizerState.ParamRegExpEnd
    ) {
      if (segment.length > 1 && (char === '*' || char === '+'))
        crash(
          `A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`
        )
      segment.push({
        type: TokenType.Param,
        value: buffer,
        regexp: customRe,
        repeatable: char === '*' || char === '+',
        optional: char === '*' || char === '?',
      })
    } else {
      crash('Invalid state to consume buffer')
    }
    buffer = ''
  }

  function addCharToBuffer() {
    buffer += char
  }

  while (i < path.length) {
    char = path[i++]

    if (char === '\\' && state !== TokenizerState.ParamRegExp) {
      previousState = state
      state = TokenizerState.EscapeNext
      continue
    }

    switch (state) {
      case TokenizerState.Static:
        if (char === '/') {
          if (buffer) {
            consumeBuffer()
          }
          finalizeSegment()
        } else if (char === ':') {
          consumeBuffer()
          state = TokenizerState.Param
        } else {
          addCharToBuffer()
        }
        break

      case TokenizerState.EscapeNext:
        addCharToBuffer()
        state = previousState
        break

      case TokenizerState.Param:
        if (char === '(') {
          state = TokenizerState.ParamRegExp
        } else if (VALID_PARAM_RE.test(char)) {
          addCharToBuffer()
        } else {
          consumeBuffer()
          state = TokenizerState.Static
          // go back one character if we were not modifying
          if (char !== '*' && char !== '?' && char !== '+') i--
        }
        break

      case TokenizerState.ParamRegExp:
        // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
        // it already works by escaping the closing )
        // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
        // is this really something people need since you can also write
        // /prefix_:p()_suffix
        if (char === ')') {
          // handle the escaped )
          if (customRe[customRe.length - 1] == '\\')
            customRe = customRe.slice(0, -1) + char
          else state = TokenizerState.ParamRegExpEnd
        } else {
          customRe += char
        }
        break

      case TokenizerState.ParamRegExpEnd:
        // same as finalizing a param
        consumeBuffer()
        state = TokenizerState.Static
        // go back one character if we were not modifying
        if (char !== '*' && char !== '?' && char !== '+') i--
        customRe = ''
        break

      default:
        crash('Unknown state')
        break
    }
  }

  if (state === TokenizerState.ParamRegExp)
    crash(`Unfinished custom RegExp for param "${buffer}"`)

  consumeBuffer()
  finalizeSegment()

  // tokenCache.set(path, tokens)

  return tokens
}

这个函数的目的是将path字符串转换为数组,方便后续的处理。 比如/user会转换为[[{type: 0, value: 'user'}]], /user/:id则会转换成:

[
    [{type: 0, value: "user"}],
    [{type: 1, value: "id", regexp: "", repeatable: false, optional: false}],
]

再回到tokensToParser函数进行分析PathParser是如何生成的

  • re 就是一个正则表达式,通过从参数传进来的path的tokens以及一些列的条件判断,将token转换为匹配path的正则表达式
const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
  sensitive: false,
  strict: false,
  start: true,
  end: true,
}
function tokensToParser(
  segments: Array<Token[]>,
  extraOptions?: _PathParserOptions
): PathParser {
  const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)
  let pattern = options.start ? '^' : ''
  for (const segment of segments) {
      // 遍历tokens逐步完善正则表达式
      // TODO:这里挖个坑,以后再分析如何生成正则表达式的
  }
  if (!options.strict) pattern += '/?'
  if (options.end) pattern += '$'
  else if (options.strict) pattern += '(?:/|$)'
  const re = new RegExp(pattern, options.sensitive ? '' : 'i')
  // ...
}
  • score 给当前path计算一个分数,在后续path之间进行比较时,可以用到score的值,相当于权重的比较
let score: Array<number[]> = []
for (const segment of segments) {
    const segmentScores: number[] = segment.length ? [] : [PathScore.Root]
    // ...
    score.push(segmentScores)
}
if (options.strict && options.end) {
    const i = score.length - 1
    score[i][score[i].length - 1] += PathScore.BonusStrict
}
  • keys 保存路由中的动态参数
const keys: PathParserParamKey[] = []
for (const segment of segments) {
    // ...
    if (token.type === TokenType.Param) {
        const { value, repeatable, optional, regexp } = token
        keys.push({
          name: value,
          repeatable,
          optional,
        })
    }
    // ...
}
  • parse 传入path参数,然后根据re,然后遍历得到的结果,获取动态参数对象
function parse(path: string): PathParams | null {
    const match = path.match(re)
    const params: PathParams = {}

    if (!match) return null

    for (let i = 1; i < match.length; i++) {
      const value: string = match[i] || ''
      const key = keys[i - 1]
      params[key.name] = value && key.repeatable ? value.split('/') : value
    }

    return params
}
  • stringify 该方法传入params对象,然后返回参数对象结合path组成的替换了参数值的path
function stringify(params: PathParams): string {
    let path = ''
    // for optional parameters to allow to be empty
    let avoidDuplicatedSlash: boolean = false
    for (const segment of segments) {
      if (!avoidDuplicatedSlash || !path.endsWith('/')) path += '/'
      avoidDuplicatedSlash = false

      for (const token of segment) {
        if (token.type === TokenType.Static) {
          path += token.value
        } else if (token.type === TokenType.Param) {
          const { value, repeatable, optional } = token
          const param: string | string[] = value in params ? params[value] : ''

          if (Array.isArray(param) && !repeatable)
            throw new Error(
              `Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`
            )
          const text: string = Array.isArray(param) ? param.join('/') : param
          if (!text) {
            if (optional) {
              // if we have more than one optional param like /:a?-static we
              // don't need to care about the optional param
              if (segment.length < 2) {
                // remove the last slash as we could be at the end
                if (path.endsWith('/')) path = path.slice(0, -1)
                // do not append a slash on the next iteration
                else avoidDuplicatedSlash = true
              }
            } else throw new Error(`Missing required param "${value}"`)
          }
          path += text
        }
      }
    }

    return path
}

经过以上复杂的步骤,就得到了一个完整的matcher对象,实属不易

  • 3、然后处理originalMatcher属性,如果是首次赋值,则将originalMatcher赋值为matcher,后面的遍历中,不再重新赋值,而是将matcher添加到originalRecord.alias数组中

  • 4、接着根据'children' in mainNormalizedRecord条件判断是否有子路由,如果有子路由,则遍历mainNormalizedRecord.children数组,并调用addRoute方法,参数为:children[i], matcher, originalRecord && originalRecord.children[i]

  • 5、最后调用insertMatcher(matcher)方法,将matcher添加到matchers中,并更新matcherMap

function insertMatcher(matcher: RouteRecordMatcher) {
  let i = 0
  while (
    i < matchers.length &&
    comparePathParserScore(matcher, matchers[i]) >= 0
  )
    i++
  matchers.splice(i, 0, matcher)
  if (matcher.record.name && !isAliasRecord(matcher))
    matcherMap.set(matcher.record.name, matcher)
}

addRoute方法执行完毕

  • resolve方法 resolve方法返回MatcherLocation对象,该对象包含的属性为:name、path、params、matched、meta,作用就是根据传入的location进行路由匹配,找到location对应的matcher对应的路由信息
function resolve(
  location: Readonly<MatcherLocationRaw>,
  currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
  let matcher: RouteRecordMatcher | undefined
  let params: PathParams = {}
  let path: MatcherLocation['path']
  let name: MatcherLocation['name']

  if ('name' in location && location.name) {
    matcher = matcherMap.get(location.name)

    if (!matcher)
      throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
        location,
      })

    name = matcher.record.name
    params = assign(
      paramsFromLocation(
        currentLocation.params,
        matcher.keys.filter(k => !k.optional).map(k => k.name)
      ),
      location.params
    )
    path = matcher.stringify(params)
  } else if ('path' in location) {
    path = location.path

    matcher = matchers.find(m => m.re.test(path))
    if (matcher) {
      params = matcher.parse(path)!
      name = matcher.record.name
    }
  } else {
    matcher = currentLocation.name
      ? matcherMap.get(currentLocation.name)
      : matchers.find(m => m.re.test(currentLocation.path))
    if (!matcher)
      throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
        location,
        currentLocation,
      })
    name = matcher.record.name
    params = assign({}, currentLocation.params, location.params)
    path = matcher.stringify(params)
  }

  const matched: MatcherLocation['matched'] = []
  let parentMatcher: RouteRecordMatcher | undefined = matcher
  while (parentMatcher) {
    matched.unshift(parentMatcher.record)
    parentMatcher = parentMatcher.parent
  }

  return {
    name,
    path,
    params,
    matched,
    meta: mergeMetaFields(matched),
  }
}
  • removeRoute方法 该方法接受一个参数matcherRef,参数类型可以传入路由name属性或者matcher对象,然后通过matcherRef找到对应的matcher或者matcher索引,在matcherMap、matchers中删除对应的matcher,然后再递归删除matcher.children和matcher.alias中对该matcher对象的引用
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  if (isRouteName(matcherRef)) {
    const matcher = matcherMap.get(matcherRef)
    if (matcher) {
      matcherMap.delete(matcherRef)
      matchers.splice(matchers.indexOf(matcher), 1)
      matcher.children.forEach(removeRoute)
      matcher.alias.forEach(removeRoute)
    }
  } else {
    let index = matchers.indexOf(matcherRef)
    if (index > -1) {
      matchers.splice(index, 1)
      if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
      matcherRef.children.forEach(removeRoute)
      matcherRef.alias.forEach(removeRoute)
    }
  }
}
  • getRoutes方法

该方法直接返回matchers数组

function getRoutes() {
    return matchers
}
  • getRecordMatcher方法 该方法十分简单,就是通过路由名称从matcherMap中获取对应的matcher对象
function getRecordMatcher(name: RouteRecordName) {
    return matcherMap.get(name)
}

安装VueRouter实例

通过上面的分析,我们了解到:

通过createRouter(options)方法创建VueRouter对象 该方法的参数为一个配置对象,该对象必须提供两个属性:history和routes history根据需要可以通过VueRouter提供的三种方法创建不同类型的history对象,该对象提供了路由属性和路由跳转方法

routes为路由配置,VueRouter根据routes配置创建了matcher对象,通过matcher对象,可以为VueRouter实例提供了诸如添加路由、匹配路由、移除路由等路由配置相关的属性和方法

接下来我们继续分析创建了VueRouter实例对象后,如何将VueRouter实例和Vue实例关联起来的

在文档示例代码中,通过 app.use 将VueRouter实例对象添加到Vue实例对象中

// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使整个应用支持路由。
app.use(router)

app.use(router)执行时,实际上调用了VueRouter实例的install方法

export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = {
  path: '/',
  name: undefined,
  params: {},
  query: {},
  hash: '',
  fullPath: '/',
  matched: [],
  meta: {},
  redirectedFrom: undefined,
}

export function createRouter(options: RouterOptions): Router {
  // shallowRef: 创建一个 ref,它跟踪自己的 .value 更改,但不会使其值成为响应式的。
  const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
    START_LOCATION_NORMALIZED
  )

  let routerHistory = options.history

  const router: Router = {

    install(app: App) {
      const router = this
  
      // 在vue实例中,注册全局路由组件 RouterLink 和 RouterView
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
  
      // 在vue实例给 config.globalProperties.$router 赋值为当前 VueRouter 实例
      app.config.globalProperties.$router = router
      /**
       * 当读取 app.config.globalProperties.$route 时,
       * 返回 unref(currentRoute), 即当前路由信息,初始值为path为`/`的对象
       */
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute),
      })

      if (
        isBrowser &&
        // 避免在多个应用中使用router时多次 push,只有在首次install时stated才是false
        !started &&
        currentRoute.value === START_LOCATION_NORMALIZED
      ) {
        started = true
        // 跳转到浏览器url中对应的路由
        push(routerHistory.location)
      }
      
      // 复制currentRoute对象并转为响应式对象reactiveRoute,该对象可以在组件中通过inject routeLocationKey 获取
      const reactiveRoute = {} as {
        [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
          RouteLocationNormalizedLoaded[k]
        >
      }
      for (let key in START_LOCATION_NORMALIZED) {
        // @ts-ignore: the key matches
        reactiveRoute[key] = computed(() => currentRoute.value[key])
      }
      
      // 向vue实例中注入 router 相关 provider,组件使用 inject 来接收这几个值
      // 这几个值的属性都是 Symbol 类型,可以通过 src/injectionSymbols.ts 源码了解
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      app.provide(routerViewLocationKey, currentRoute)
  
      // 拦截vue实例unmount方法,当vue实例卸载时,重置某些属性和事件解绑,然后再执行vue实例卸载方法
      let unmountApp = app.unmount
      installedApps.add(app)
      app.unmount = function () {
        installedApps.delete(app)
        if (installedApps.size < 1) {
          removeHistoryListener()
          currentRoute.value = START_LOCATION_NORMALIZED
          started = false
          ready = false
        }
        unmountApp()
      }
    },
  }
}

总的来说,install方法做了以下几件事:

  • 注册两个路由组件为vue全局组件
  • 在app.config.globalProperties上添加routerrouter和route属性,routerVueRouter实例对象本身,router是VueRouter实例对象本身,route为当前location对应的路由对象
  • 如果是首次install,则通过push方法跳转到url对应的路由
  • 注入三个VueRouter相关的provider
  • 拦截vue实例的unmount方法,在unmount方法调用之前,先执行VueRouter相关的卸载工作

总结

在阅读源码的过程中,Typescript使阅读源码相对少吃点力,通过类型定义对理解每个变量的作用提供了很好的帮助。从VueRouter实例对象的创建开始分析,对路由的基本实现原理有了一定的理解,也认识到要实现一个功能完善的路由功能,不是一件很简单的事情,需要考虑的边界问题很多。由于能力有限,也还存在很多细节不能够理解。阅读源码,始终是让人受益匪浅的一件事,不管目前能够理解多少。