乐趣区

关于vue-router:vuerouter源码三理解Vuerouter中的Matcher

前言

【vue-router 源码】系列文章将带你从 0 开始理解 vue-router 的具体实现。该系列文章源码参考 vue-router v4.0.15
源码地址:https://github.com/vuejs/router
浏览该文章的前提是你最好理解 vue-router 的根本应用,如果你没有应用过的话,可通过 vue-router 官网学习下。

该篇文章将带你了解 vue-routermatcher的实现。

matcher 初识

在开始介绍 matcher 实现之前,咱们先理解下 matcher 是什么?它的作用是什么?
vue-router 中,每一个咱们定义的路由都会被解析成一个对应的 matcherRouteRecordMatcher 类型),路由的增删改查都会依附 matcher 来实现。

createRouterMatcher

createRouter 中会通过 createRouterMatcher 创立一个 matcherRouterMatcher 类型)。

export 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 {// ...}

  routes.forEach(route => addRoute(route))

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

createRouterMatcher接管两个参数:routesglobalOptions。其中 routes 为咱们定义的路由表,也就是在 createRouter 时传入的 options.routes,而globalOptions 就是 createRouter 中的 options
createRouterMatcher 中申明了两个变量 matchersmatcherMap,用来存储通过路由表解析的matcherRouteRecordMatcher 类型),而后遍历 routes,对每个元素调用addRoute 办法。最初返回一个对象,该对象有 addRouteresolveremoveRoutegetRoutegetRecordMatcher 几个属性,这几个属性都对应着一个函数。
接下来咱们看下这几个函数:

addRoute

addRoute函数接管三个参数:record(新增的路由)、parent(父matcher)、originalRecord(原始matcher)。

function addRoute(
  record: RouteRecordRaw,
  parent?: RouteRecordMatcher,
  originalRecord?: RouteRecordMatcher
) {
  // used later on to remove by name
  const isRootAdd = !originalRecord
  // 标准化化路由记录
  const mainNormalizedRecord = normalizeRouteRecord(record)
  // aliasOf 示意此记录是否是另一个记录的别名
  mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
  const options: PathParserOptions = mergeOptions(globalOptions, record)
  // 申明一个记录的数组用来解决别名
  const normalizedRecords: typeof mainNormalizedRecord[] = [mainNormalizedRecord,]
  // 如果 record 设置了别名
  if ('alias' in record) {
    // 别名数组
    const aliases =
      typeof record.alias === 'string' ? [record.alias] : record.alias!
    // 遍历别名数组,并依据别名创立记录存储到 normalizedRecords 中
    for (const alias of aliases) {
      normalizedRecords.push(assign({}, mainNormalizedRecord, {
          components: originalRecord
            ? originalRecord.record.components
            : mainNormalizedRecord.components,
          path: alias,
          // 如果有原始记录,aliasOf 为原始记录,如果没有原始记录就是它本人
          aliasOf: originalRecord
            ? originalRecord.record
            : mainNormalizedRecord,
        }) as typeof mainNormalizedRecord
      )
    }
  }

  let matcher: RouteRecordMatcher
  let originalMatcher: RouteRecordMatcher | undefined

  // 遍历 normalizedRecords
  for (const normalizedRecord of normalizedRecords) {
    
    // 解决 normalizedRecord.path 为残缺的 path
    const {path} = normalizedRecord
    // 如果 path 不是以 / 结尾,那么阐明它不是根路由,须要拼接为残缺的 path
    // {path: '/a', children: [ { path: 'b'} ] } -> {path: '/a', children: [ { path: '/a/b'} ] }
    if (parent && path[0] !== '/') {
      const parentPath = parent.record.path
      const connectingSlash =
        parentPath[parentPath.length - 1] === '/' ? '':'/'
      normalizedRecord.path =
        parent.record.path + (path && connectingSlash + path)
    }

    // 提醒 * 应应用正则示意式模式
    if (__DEV__ && normalizedRecord.path === '*') {
      throw new Error('Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
          'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'
      )
    }

    // 创立一个路由记录匹配器
    matcher = createRouteRecordMatcher(normalizedRecord, parent, options)

    // 查看是否有失落的参数
    if (__DEV__ && parent && path[0] === '/')
      checkMissingParamsInAbsolutePath(matcher, parent)

    // 如果有 originalRecord,将 matcher 放入原始记录的 alias 中,以便后续可能删除
    if (originalRecord) {originalRecord.alias.push(matcher)
      // 查看 originalRecord 与 matcher 中动静参数是否雷同
      if (__DEV__) {checkSameParams(originalRecord, matcher)
      }
    } else { // 没有 originalRecord
      // 因为原始记录索引为 0,所以 originalMatcher 为有原始记录所产生的 matcher
      originalMatcher = originalMatcher || matcher
      // 如果 matcher 不是原始记录产生的 matcher,阐明此时 matcher 是由别名记录产生的,此时将 matcher 放入 originalMatcher.alias 中
      if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
      // 如果命名并且仅用于顶部记录,则删除路由(防止嵌套调用)if (isRootAdd && record.name && !isAliasRecord(matcher))
        removeRoute(record.name)
    }

    // 遍历 children,递归 addRoute
    if ('children' in mainNormalizedRecord) {
      const children = mainNormalizedRecord.children
      for (let i = 0; i < children.length; i++) {
        addRoute(children[i],
          matcher,
          originalRecord && originalRecord.children[i]
        )
      }
    }

    originalRecord = originalRecord || matcher
    // 增加 matcher
    insertMatcher(matcher)
  }

  // 返回一个删除原始 matcher 的办法
  return originalMatcher
    ? () => {removeRoute(originalMatcher!)
      }
    : noop
}

addRoute 中,会对 record 进行标准化解决(normalizeRouteRecord),如果存在原始的 matcher,也就是originalRecord,阐明此时要增加的路由是另一记录的别名,这时会将originalRecord.record 存入 mainNormalizedRecord.aliasOf 中。

const isRootAdd = !originalRecord
// 标准化化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record)
// aliasOf 示意此记录是否是另一个记录的别名
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
// 申明一个记录的数组用来解决别名
const normalizedRecords: typeof mainNormalizedRecord[] = [mainNormalizedRecord,]

而后会遍历 record 的别名,向 normalizedRecords 中增加由别名产生的路由:

if ('alias' in record) {
  // 别名数组
  const aliases =
    typeof record.alias === 'string' ? [record.alias] : record.alias!
  // 遍历别名数组,并依据别名创立记录存储到 normalizedRecords 中
  for (const alias of aliases) {
    normalizedRecords.push(assign({}, mainNormalizedRecord, {
        components: originalRecord
          ? originalRecord.record.components
          : mainNormalizedRecord.components,
        path: alias,
        // 如果有原始记录,aliasOf 为原始记录,如果没有原始记录就是它本人
        aliasOf: originalRecord
          ? originalRecord.record
          : mainNormalizedRecord,
      }) as typeof mainNormalizedRecord
    )
  }
}

紧接着会遍历 normalizedRecords:在这个遍历过程中,会首先将path 解决成残缺的 path,而后通过createRouteRecordMatcher 办法创立一个 matcherRouteRecordMatcher 类型),如果 matcher 是由别名产生的,那么 matcher 会被退出由原始记录产生的 matcher 中的 alias 属性中。而后会遍历 mainNormalizedRecordchildren属性,递归调用 addRoute 办法。在最初,调用 insertMatcher 增加新创建的matcher

for (const normalizedRecord of normalizedRecords) {
  
  // 解决 normalizedRecord.path 为残缺的 path
  const {path} = normalizedRecord
  // 如果 path 不是以 / 结尾,那么阐明它不是根路由,须要拼接为残缺的 path
  // {path: '/a', children: [ { path: 'b'} ] } -> {path: '/a', children: [ { path: '/a/b'} ] }
  if (parent && path[0] !== '/') {
    const parentPath = parent.record.path
    const connectingSlash =
      parentPath[parentPath.length - 1] === '/' ? '':'/'
    normalizedRecord.path =
      parent.record.path + (path && connectingSlash + path)
  }

  // 提醒 * 应应用正则示意式模式
  if (__DEV__ && normalizedRecord.path === '*') {
    throw new Error('Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
        'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'
    )
  }

  // 创立一个路由记录匹配器
  matcher = createRouteRecordMatcher(normalizedRecord, parent, options)

  // 查看是否有失落的参数
  if (__DEV__ && parent && path[0] === '/')
    checkMissingParamsInAbsolutePath(matcher, parent)

  // 如果有 originalRecord,将 matcher 放入原始记录的 alias 中,以便后续可能删除
  if (originalRecord) {originalRecord.alias.push(matcher)
    // 查看 originalRecord 与 matcher 中动静参数是否雷同
    if (__DEV__) {checkSameParams(originalRecord, matcher)
    }
  } else { // 没有 originalRecord
    // 因为原始记录索引为 0,所以 originalMatcher 为有原始记录所产生的 matcher
    originalMatcher = originalMatcher || matcher
    // 如果 matcher 不是原始记录产生的 matcher,阐明此时 matcher 是由别名记录产生的,此时将 matcher 放入 originalMatcher.alias 中
    if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
    // 如果存在 record.name 并且是顶部记录,则删除路由(防止嵌套调用)if (isRootAdd && record.name && !isAliasRecord(matcher))
      removeRoute(record.name)
  }

  // 遍历 children,递归 addRoute
  if ('children' in mainNormalizedRecord) {
    const children = mainNormalizedRecord.children
    for (let i = 0; i < children.length; i++) {
      addRoute(children[i],
        matcher,
        originalRecord && originalRecord.children[i]
      )
    }
  }
  // 如果 originalRecord 是办法传入的,那么 originalRecord 持续放弃
  // 如果 originalRecord 办法未传入。因为原始的 matcher 总是在索引为 0 的地位,所以如果有别名,那么这些别名的原始 matcher 会始终指向索引为 0 的地位
  originalRecord = originalRecord || matcher
  // 增加 matcher
  insertMatcher(matcher)
}

在最初,addRoute会返回一个删除原始 matcher 的办法。

addRoute 的过程中,会调用 createRouteRecordMatcher 办法来创立 matcher,那么matcher 到底是什么?它是如何被创立的?接下来咱们看下 createRouteRecordMatcher 的实现。那么在看 createRouteRecordMatcher 之前,咱们先来理解 tokenizePathtokensToParser 这两个函数,因为这两个函数是创立 matcher 的外围。
tokenizePath 的作用是 path转为一个 token 数组。而 tokensToParser 会依据 token 数组创立一个门路解析器。这里提到了一个 token 的概念,那么什么是 token 呢?咱们看下 vue-routertoken的类型定义:

token

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

从其类型中咱们能够看出 token 分为三种:

  • TokenStatic:一种动态的 token,阐明token 不可变
  • TokenParam:参数 token,阐明token 是个参数
  • TokenGroup:分组的 token
    为了更好了解token,这里咱们举几个例子:
  • /one/two/three对应的 token 数组:

    [[{ type: TokenType.Static, value: 'one'}],
    [{type: TokenType.Static, value: 'two'}],
    [{type: TokenType.Static, value: 'three'}]
    ]
  1. /user/:id对应的 token 数组是:

    [
      [
     {
       type: TokenType.Static,
       value: 'user',
     },
      ],
      [
     {
       type: TokenType.Param,
       value: 'id',
       regexp: '',
       repeatable: false,
       optional: false,
     }
      ]
    ]
  2. /:id(\\d+)new对应的 token 数组:

    [
      [
     {
       type: TokenType.Param,
       value: 'id',
       regexp: '\\d+',
       repeatable: false,
       optional: false,
     },
     {
       type: TokenType.Static,
       value: 'new'
     }
      ]
    ]

    从下面几个例子能够看出,token数组详细描述了 path 的每一级路由的组成。例如第 3 个例子 /:id(\\d+)new,通过token 数组咱们可能晓得他是一个一级路由(token.lenght = 1),并且它的这级路由是由两局部组成,其中第一局部是参数局部,第二局部是动态的,并且在参数局部还阐明了参数的正则及是否反复、是否可选的配置。

理解了 token 是什么,接下来咱们看下 tokenizePath 是如何将 path 转为 token 的:

tokenizePath

export const enum TokenType {
  Static,
  Param,
  Group,
}

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

export function tokenizePath(path: string): Array<Token[]> {if (!path) return [[]]
  if (path === '/') return [[ROOT_TOKEN]]
  // 如果 path 不是以 / 结尾,抛出谬误
  if (!path.startsWith('/')) {
    throw new Error(
      __DEV__
        ? `Route paths should start with a "/": "${path}" should be "/${path}".`
        : `Invalid path "${path}"`
    )
  }
  
  function crash(message: string) {throw new Error(`ERR (${state})/"${buffer}": ${message}`)
  }

  // token 所处状态
  let state: TokenizerState = TokenizerState.Static
  // 前一个状态
  let previousState: TokenizerState = state
  const tokens: Array<Token[]> = []
  //  申明一个片段,该片段最终会被存入 tokens 中
  let segment!: Token[]

  // 增加 segment 至 tokens 中,同时 segment 从新变为空数组
  function finalizeSegment() {if (segment) tokens.push(segment)
    segment = []}

  let i = 0
  let char: string
  let buffer: string = ''
  // custom regexp for a param
  let customRe: string = ''

  // 生产 buffer,即生成 token 增加到 segment 中
  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}

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

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

    switch (state) {
      case TokenizerState.Static:
        if (char === '/') {if (buffer) {consumeBuffer()
          }
          // char === / 时阐明曾经遍历完一层路由,这时须要将 segment 增加到 tokens 中
          finalizeSegment()} else if (char === ':') { // char 为: 时,因为此时状态是 TokenizerState.Static,所以: 后是参数,此时要把 state 变为 TokenizerState.Param
          consumeBuffer()
          state = TokenizerState.Param
        } else { // 其余状况拼接 buffer
          addCharToBuffer()}
        break

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

      case TokenizerState.Param:
        if (char === '(') { // 碰到(,因为此时 state 为 TokenizerState.Param,阐明前面是正则表达式,所以批改 state 为 TokenizerState.ParamRegExp
          state = TokenizerState.ParamRegExp
        } else if (VALID_PARAM_RE.test(char)) {addCharToBuffer()
        } else { // 例如 /:id/one,当遍历到第二个 / 时,生产 buffer,state 变为 Static,并让 i 回退,回退后进入 Static
          consumeBuffer()
          state = TokenizerState.Static
          if (char !== '*' && char !== '?' && char !== '+') i--
        }
        break

      case TokenizerState.ParamRegExp: 
        // it already works by escaping the closing )
        // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
        // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
        // is this really something people need since you can also write
        // /prefix_:p()_suffix
        if (char === ')') {// 如果是 \\)的状况,customRe = customRe 去掉 \\ + char
          if (customRe[customRe.length - 1] == '\\')
            customRe = customRe.slice(0, -1) + char
          else state = TokenizerState.ParamRegExpEnd // 如果不是 \\)阐明正则表达式曾经遍历完
        } else {customRe += char}
        break

      case TokenizerState.ParamRegExpEnd: // 正则表达式曾经遍历完
        // 生产 buffer
        consumeBuffer()
        // 重置 state 为 Static
        state = TokenizerState.Static
        // 例如 /:id(\\d+)new,当遍历到 n 时,使 i 回退,下一次进入 Static 分支中解决
        if (char !== '*' && char !== '?' && char !== '+') i--
        customRe = ''
        break

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

  // 如果遍历完结后,state 还是 ParamRegExp 状态,阐明正则是没有完结的,可能漏了)
  if (state === TokenizerState.ParamRegExp)
    crash(`Unfinished custom RegExp for param "${buffer}"`)

  // 遍历完 path,进行最初一次生产 buffer
  consumeBuffer()
  // 将 segment 放入 tokens
  finalizeSegment()

  // 最初返回 tokens
  return tokens
}

为了更好了解 tokenizePath 的过程。咱们以 path = '/:id(\\d+)new' 例,咱们看一下 tokenizePath 的过程:

  1. 初始状态:state=TokenizerState.Static; previousState=TokenizerState.Static; tokens=[]; segment; buffer=''; i=0; char=''; customRe='';
  2. i=0 时,进入 TokenizerState.Static 分支,此时char='/'; buffer='';,不会执行consumeBuffer,执行finalizeSegment,该轮完结后发生变化的是segment=[]; i=1; char='/';
  3. i=1 时,进入 TokenizerState.Static 分支,此时 char=':'; buffer='';,执行consumeBuffer,因为buffer='',所以consumeBuffer 中什么都没做,最初state=TokenizerState.Param,该轮完结后发生变化的是state=TokenizerState.Param; i=2; char=':';
  4. i=2 时,进入 TokenizerState.Param 分支,此时char='i'; buffer='';,执行addCharToBuffer,该轮完结后发生变化的是buffer='i'; i=3; char='i';
  5. i=3 时,过程同 4,该轮完结后发生变化的是buffer='id'; i=4; char='d';
  6. i=4 时,进入 TokenizerState.Param 分支,此时 char='('; buffer='id';,此时会将state 变为 TokenizerState.ParamRegExp,阐明( 前面是正则,该轮完结后发生变化的是state=TokenizerState.ParamRegExp; i=5; char='(';
  7. i=5 时,进入 TokenizerState.ParamRegExp 分支,此时char='\\'; buffer='id';,执行customRe+=char,该轮完结后发生变化的是i=6; char='\\'; customRe='\\'
  8. i=6i=7 时,过程同 5,最终发生变化的是i=8; char='+'; customRe='\\d+'
  9. i=8 时,进入 TokenizerState.ParamRegExp 分支,此时 char=')'; buffer='id'; customRe='\\d+'state 变为TokenizerState.ParamRegExpEnd,代表正则完结,该轮完结后发生变化的是state=TokenizerState.ParamRegExpEnd; i=9; char=')';
  10. i=9 时,进入 TokenizerState.ParamRegExpEnd 分支,此时 char='n'; buffer='id'; customRe='\\d+',执行consumeBuffer,在consumeBuffer 中会向 segment 增加一条 token 并将 buffer 置为空字符串,该 token{type: TokenType.Param, value: 'id', regexp: '\\d+', repeatable: false, optional: false},执行完 consumeBuffer 后,state重置为 StaticcustomRe 重置为空字符串,i回退 1,该轮完结后发生变化的是segment=[{...}]; state=TokenizerState.Static; buffer=''; customRe=''; char='n';,留神此时i=9
  11. 上一轮完结后 i=9,进入TokenizerState.Static 分支,此时此时 char='n'; buffer='';,执行addCharToBuffer 办法,该轮完结后发生变化的是buffer='n'; i=10; char='n'
  12. i=10i=11 时,过程同 11,完结后发生变化的是buffer='new'; i=12; char='w'
  13. i=12,完结遍历,执行consumeBuffer,向segment 增加 {type: TokenType.Static, value: 'new'} 一条记录并将 buffer 置为空字符串。而后执行 finalizeSegment,将segment 增加到 tokens 中,并将 segment 置为空数组。最初返回的 tokens 如下:

    [
      [
     {
       type: TokenType.Param,
       value: 'id',
       regexp: '\\d+',
       repeatable: false,
       optional: false,
     },
     {
       type: TokenType.Static,
       value: 'new'
     }
      ]
    ]

tokensToParser

tokensToParser函数接管一个 token 数组和一个可选的 extraOptions,在函数中会结构出path 对应的正则表达式、动静参数列表 keystoken 对应的分数(相当于权重,该分数在后续 path 的比拟中会用到)、一个能够从 path 中提取动静参数的函数(parse)、一个能够依据传入的动静参数生成 path 的函数(stringify),最初将其组成一个对象返回。

const enum PathScore {
  _multiplier = 10,
  Root = 9 * _multiplier, // 只有一个 / 时的分数
  Segment = 4 * _multiplier, // segment 的根底分数
  SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment
  Static = 4 * _multiplier, // type=TokenType.Static 时的分数
  Dynamic = 2 * _multiplier, // 动静参数分数 /:someId
  BonusCustomRegExp = 1 * _multiplier, // 用户自定义正则的分数 /:someId(\\d+) 
  BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp
  BonusRepeatable = -2 * _multiplier, // 当正则是可反复时的分数 /:w+ or /:w*
  BonusOptional = -0.8 * _multiplier, // 当正则是可抉择时的分数 /:w? or /:w*
  // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
  BonusStrict = 0.07 * _multiplier, // options.strict: true 时的分数
  BonusCaseSensitive = 0.025 * _multiplier, // options.strict:true 时的分数
}
const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
  sensitive: false,
  strict: false,
  start: true,
  end: true,
}
const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g
export function tokensToParser(segments: Array<Token[]>,
  extraOptions?: _PathParserOptions
): PathParser {const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)

  // 除了根段“/”之外,分数的数量与 segments 的长度雷同
  const score: Array<number[]> = []
  // 正则的字符串模式
  let pattern = options.start ? '^' : ''
  // 保留路由中的动静参数
  const keys: PathParserParamKey[] = []

  for (const segment of segments) {
    // 用一个数组保留 token 的分数,如果 segment.length 为 0,应用 PathScore.Root
    const segmentScores: number[] = segment.length ? [] : [PathScore.Root]

    // options.strict 代表是否禁止尾部 /,如果禁止了 pattern 追加 /
    if (options.strict && !segment.length) pattern += '/'
    // 开始遍历每个 token
    for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {const token = segment[tokenIndex]
      // 以后子片段(单个 token)的分数:根底分数 + 辨别大小写 ? PathScore.BonusCaseSensitive : 0
      let subSegmentScore: number =
        PathScore.Segment +
        (options.sensitive ? PathScore.BonusCaseSensitive : 0)

      if (token.type === TokenType.Static) {
        // 在开始一个新的片段(tokenIndex !== 0)前 pattern 须要增加 /
        if (!tokenIndex) pattern += '/'
        // 将 token.value 追加到 pattern 后。追加前 token.value 中的.、+、*、?、^、$ 等字符后面加上 \\
        // 对于 replace,参考 MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace
        pattern += token.value.replace(REGEX_CHARS_RE, '\\$&')
        subSegmentScore += PathScore.Static
      } else if (token.type === TokenType.Param) {const { value, repeatable, optional, regexp} = token
        // 增加参数
        keys.push({
          name: value,
          repeatable,
          optional,
        })
        const re = regexp ? regexp : BASE_PARAM_PATTERN
        // 用户自定义的正则须要验证正则的正确性
        if (re !== BASE_PARAM_PATTERN) {
          subSegmentScore += PathScore.BonusCustomRegExp
          // 应用前确保正则是正确的
          try {new RegExp(`(${re})`)
          } catch (err) {
            throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
                (err as Error).message
            )
          }
        }

        // /:chapters*
        // 如果是反复的,必须留神反复的前导斜杠
        let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`

        // prepend the slash if we are starting a new segment
        if (!tokenIndex)
          subPattern =
            // avoid an optional / if there are more segments e.g. /:p?-static
            // or /:p?-:p2
            optional && segment.length < 2
              ? `(?:/${subPattern})`
              : '/' + subPattern
        if (optional) subPattern += '?'

        pattern += subPattern

        subSegmentScore += PathScore.Dynamic
        if (optional) subSegmentScore += PathScore.BonusOptional
        if (repeatable) subSegmentScore += PathScore.BonusRepeatable
        if (re === '.*') subSegmentScore += PathScore.BonusWildcard
      }

      segmentScores.push(subSegmentScore)
    }

    score.push(segmentScores)
  }

  // only apply the strict bonus to the last score
  if (options.strict && options.end) {
    const i = score.length - 1
    score[i][score[i].length - 1] += PathScore.BonusStrict
  }

  // TODO: dev only warn double trailing slash
  if (!options.strict) pattern += '/?'

  if (options.end) pattern += '$'
  // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
  else if (options.strict) pattern += '(?:/|$)'

  // 依据组装好的 pattern 创立正则表达式,options.sensitive 决定是否辨别大小写
  const re = new RegExp(pattern, options.sensitive ? '':'i')

  // 依据 path 获取动静参数对象
  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
  }

  // 依据传入的动静参数对象,转为对应的 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 and there are more segments, we don't need to
              // care about the optional param
              if (segment.length < 2 && segments.length > 1) {
                // 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
  }

  return {
    re,
    score,
    keys,
    parse,
    stringify,
  }
}

当初咱们理解了 tokensToParsertokenizePath,而后咱们来看 createRouteRecordMatcher 的实现:

createRouteRecordMatcher

export function createRouteRecordMatcher(
  record: Readonly<RouteRecord>,
  parent: RouteRecordMatcher | undefined,
  options?: PathParserOptions
): RouteRecordMatcher {
  // 生成 parser 对象
  const parser = tokensToParser(tokenizePath(record.path), options)

  // 如果有反复的动静参数命名进行提醒
  if (__DEV__) {const existingKeys = new Set<string>()
    for (const key of parser.keys) {if (existingKeys.has(key.name))
        warn(`Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`
        )
      existingKeys.add(key.name)
    }
  }

  // 将 record,parent 合并到 parser 中,同时新增 children,alias 属性,默认值为空数组
  const matcher: RouteRecordMatcher = assign(parser, {
    record,
    parent,
    // these needs to be populated by the parent
    children: [],
    alias: [],})

  if (parent) {
    // 两者都是 alias 或两者都不是 alias
    if (!matcher.record.aliasOf === !parent.record.aliasOf)
      parent.children.push(matcher)
  }

  return matcher
}

resolve

resolve依据传入的 location 进行路由匹配,找到对应的 matcher 的路由信息。办法接管一个 locationcurrentLocation参数,返回一个 MatcherLocation 类型的对象,该对象的属性蕴含:namepathparamsmatchedmeta

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) { // 如果 location 存在 name 属性,可依据 name 从 matcherMap 获取 matcher
      matcher = matcherMap.get(location.name)

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

      name = matcher.record.name
      // 合并 location.params 和 currentLocation 中的 params
      params = assign(
        paramsFromLocation(
          currentLocation.params,
          matcher.keys.filter(k => !k.optional).map(k => k.name)
        ),
        location.params
      )
      // 如果不能通过 params 转为 path 抛出谬误
      path = matcher.stringify(params)
    } else if ('path' in location) { // 如果 location 存在 path 属性,依据 path 从 matchers 获取对应 matcher
      path = location.path

      if (__DEV__ && !path.startsWith('/')) {
        warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/router.`
        )
      }

      matcher = matchers.find(m => m.re.test(path))

      if (matcher) {
        // 通过 parse 函数获取 params
        params = matcher.parse(path)!
        name = matcher.record.name
      }
    } else { // 如果 location 中没有 name、path 属性,就应用 currentLocation 的 name 或 path 获取 matcher
      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 参数,removeRoute会将 matcherRef 对应的 matchermatcherMapmatchers 中删除,并清空 matcherRef 对应 matcherchildrenalias 属性。因为 matcherRef 对应的 matcher 被删除后,其子孙及别名也就没用了,也须要把他们从 matcherMap 中和 matchers 中删除。

function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  // 如果是路由名字:string 或 symbol
  if (isRouteName(matcherRef)) {const matcher = matcherMap.get(matcherRef)
    if (matcher) {
      // 删除 matcher
      matcherMap.delete(matcherRef)
      matchers.splice(matchers.indexOf(matcher), 1)
      // 清空 matcher 中的 children 与 alias,matcher.children.forEach(removeRoute)
      matcher.alias.forEach(removeRoute)
    }
  } else {const 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

获取所有matcher

function getRoutes() {return matchers}

getRecordMatcher

依据路由名获取对应matcher

function getRecordMatcher(name: RouteRecordName) {return matcherMap.get(name)
}

insertMatcher

在增加 matcher 时,并不是间接 matchers.add,而是依据matcher.score 进行排序。比拟分数时依据数组中的每一项挨个比拟,不是比拟总分。

function insertMatcher(matcher: RouteRecordMatcher) {
  let i = 0
  while (
    i < matchers.length &&
    // matcher 与 matchers[i]比拟,matchers[i]应该在后面
    comparePathParserScore(matcher, matchers[i]) >= 0 &&
    // matcher 的 path 与 matchers[i]不同或 matcher 不是 matchers[i]的孩子
    (matcher.record.path !== matchers[i].record.path ||
      !isRecordChildOf(matcher, matchers[i]))
  )
    i++
  // 插入 matcher
  matchers.splice(i, 0, matcher)
  // 只增加原始 matcher 到 map 中
  if (matcher.record.name && !isAliasRecord(matcher))
    matcherMap.set(matcher.record.name, matcher)
}
// 返回 0 示意 a 与 b 相等;返回 >0,b 先排序;返回 <0,a 先排序
export function comparePathParserScore(a: PathParser, b: PathParser): number {
  let i = 0
  const aScore = a.score
  const bScore = b.score
  while (i < aScore.length && i < bScore.length) {const comp = compareScoreArray(aScore[i], bScore[i])
    if (comp) return comp

    i++
  }

  return bScore.length - aScore.length
}

function compareScoreArray(a: number[], b: number[]): number {
  let i = 0
  while (i < a.length && i < b.length) {const diff = b[i] - a[i]
    // 一旦 a 与 b 对位索引对应的值有差值,间接返回
    if (diff) return diff

    i++
  }
  if (a.length < b.length) {
      // 如果 a.length 为 1 且第一个值的分数为 PathScore.Static + PathScore.Segment,返回 -1,示意 a 先排序,否则返回 1,示意 b 先排序
    return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment
      ? -1
      : 1
  } else if (a.length > b.length) {
    // 如果 b.length 为 1 且第一个值的分数为 PathScore.Static + PathScore.Segment,返回 -1,示意 b 先排序,否则返回 1,示意 a 先排序
    return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment
      ? 1
      : -1
  }

  return 0
}

假如 matcherA 是须要增加的,matchers中此时只有一个 matcherBmatcherA.score=[[1, 2]]matcherB.score=[[1,3]],那么matcherA 是怎么增加到 matchers 中的呢?过程如下:

  1. 初始化 matchers 索引i=0
  2. 首先比拟 matcherA.score[0][0]matcherB.score[0][0]matcherB.score[0][0]-matcherA.score[0][0] === 0持续比拟
  3. matcherA.score[0][1]matcherB.score[0][1],因为matcherB.score[0][1]-matcherA.score[0][1] > 0i++
  4. i=1时,因为i=matchers.length,完结循环
  5. 执行 matchers.splice(i, 0, matcher),此时i=1, 所以matcherA 会被增加到索引为 1 的地位

如果 matcherA.score=[[1,3,4]] 呢?在比拟时因为前两个索引对应的值都是一样的,这时会进入 compareScoreArray 的以下分支:

if (a.length > b.length) {return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment
    ? 1
    : -1
}

以上后果返回 -1,matcherA会被增加到索引为 0 的地位。

如果 matcherA.score=[[1]],进入compareScoreArray 的以下分支:

if (a.length < b.length) {return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment
    ? -1
    : 1
}

因为 matcherA.score[0].length === 1,这时就须要思考token 的类型里,假如 token 是个 Static 类型的,那么返回 -1,matcherA增加到索引为 0 的地位。如果 token 不是 Static 类型的,返回 1,matcherA增加到索引为 1 的地位。

所以 insertMatcher,会将权重高的matcher 放在 matchers 后面;matcherMap中只寄存原始matcher

总结

通过下面剖析,咱们晓得了 matcher 是什么,如何实现的

vue-router 通过 matcher 实现路由的匹配、增删改查等操作,其中会应用 matchersmatcherMap来存储 matchermatchers 中权重(分数)高的 matcher 优先;matcherMap中的 key 是注册路由时路由表的name,只寄存原始matcher

matcher中蕴含了路由 path 对应的正则 re、路由的分数score、动静参数列表keys、可从path 中提取动静参数的 parse(path) 函数、可传入参数对象将其转为对应 pathstringify(param)函数、父matcherparent)、路由的标准化版本record、子matcherchildren)、由别名产生的matcheralias

export interface PathParser {
  re: RegExp
  score: Array<number[]>
  keys: PathParserParamKey[]
  parse(path: string): PathParams | null
  stringify(params: PathParams): string
}
export interface RouteRecordMatcher extends PathParser {
  record: RouteRecord
  parent: RouteRecordMatcher | undefined
  children: RouteRecordMatcher[]
  // aliases that must be removed when removing this record
  alias: RouteRecordMatcher[]}

在生成 matcher 的过程中会将 path 转换成 token 数组(二维数组,第一维度中每个维度代表一级路由,第二维度中每个维度代表路由的组成),路由正则的生成、动静参数的提取、分数的计算、stringify全都依靠这个 token 数组实现。

退出移动版