前言

【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

tokenizePath的过程就是利用有限状态自动机生成token数组。

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数组实现。


MAXLZ
9 声望17 粉丝