美文网首页【vue-rouer源码】
【vue-router源码】三、理解Vue-router中的Ma

【vue-router源码】三、理解Vue-router中的Ma

作者: MAXLZ | 来源:发表于2022-06-06 22:19 被阅读0次

    前言

    【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,这里我们举几个例子:
    1. /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,
        }
      ]
    ]
    
    1. /: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'
        }
      ]
    ]
    

    状态转移过程图示:


    tokenizePath状态转移过程.drawio.png

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

    相关文章

      网友评论

        本文标题:【vue-router源码】三、理解Vue-router中的Ma

        本文链接:https://www.haomeiwen.com/subject/vyksmrtx.html