vue-router 源码深度解析

这是「三套路由器」系列的第二篇,深度拆解 vue-router v5 的源码实现。

源码版本:vue-router v5.2.0
核心文件:packages/router/src/


一、History 层实现

1.1 核心入口:createWebHistory

// packages/router/src/history/html5.ts
export function createWebHistory(base?: string): RouterHistory {
  base = normalizeBase(base)

  // 1. 创建状态导航对象
  const historyNavigation = useHistoryStateNavigation(base)

  // 2. 创建监听器
  const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location,
    historyNavigation.replace,
  )

  // 3. 组装 RouterHistory 接口
  const routerHistory = assign(
    { location: '', base, go, createHref: createHref.bind(null, base) },
    historyNavigation,
    historyListeners,
  )

  return routerHistory
}

1.2 State 结构设计

vue-router 在 history.state 中存储完整的导航信息:

interface StateEntry extends HistoryState {
  back: HistoryLocation | null // 上一个位置
  current: HistoryLocation // 当前位置
  forward: HistoryLocation | null // 下一个位置
  position: number // 历史栈位置
  replaced: boolean // 是否被替换
  scroll: _ScrollPositionNormalized | null | false // 滚动位置
}

设计思路:记录前后关系,方便 router.back()router.forward() 的实现。

1.3 push 和 replace 的实现

// push 实现
function push(to: HistoryLocation, data?: HistoryState) {
  // 1. 更新当前状态的 forward
  const currentState = assign({}, historyState.value, {
    forward: to,
    scroll: computeScrollPosition(),
  })
  changeLocation(currentState.current, currentState, true) // replaceState

  // 2. 创建新状态
  const state: StateEntry = assign(
    {},
    buildState(currentLocation.value, to, null),
    { position: currentState.position + 1 },
    data,
  )
  changeLocation(to, state, false) // pushState

  currentLocation.value = to
}

// replace 实现
function replace(to: HistoryLocation, data?: HistoryState) {
  const state: StateEntry = assign(
    {},
    history.state,
    buildState(
      historyState.value.back, // 保持 back 不变
      to, // 更新 current
      historyState.value.forward, // 保持 forward 不变
    ),
    data,
    { position: historyState.value.position }, // 保持位置不变
  )

  changeLocation(to, state, true)
  currentLocation.value = to
}

1.4 popstate 监听

const popStateHandler: PopStateListener = ({ state }) => {
  const to = createCurrentLocation(base, location)
  const from: HistoryLocation = currentLocation.value
  const fromState: StateEntry = historyState.value
  let delta = 0

  if (state) {
    currentLocation.value = to
    historyState.value = state

    // 计算方向
    delta = fromState ? state.position - fromState.position : 0
  } else {
    replace(to) // 没有 state 时用 replace
  }

  // 触发所有监听器
  listeners.forEach((listener) => {
    listener(currentLocation.value, from, {
      delta,
      type: NavigationType.pop,
      direction: delta
        ? delta > 0
          ? NavigationDirection.forward
          : NavigationDirection.back
        : NavigationDirection.unknown,
    })
  })
}

1.5 Safari 降级处理

function changeLocation(to, state, replace) {
  const url = createBaseLocation() + base + to
  try {
    // Safari 限制:30秒内最多 100 次 pushState
    history[replace ? 'replaceState' : 'pushState'](state, '', url)
  } catch (err) {
    // 降级到 location.assign/replace
    location[replace ? 'replace' : 'assign'](url)
  }
}

二、路由匹配器

2.1 Tokenizer:状态机解析路径

// packages/router/src/matcher/pathTokenizer.ts
enum TokenType {
  Static,
  Param,
  Group,
}

enum TokenizerState {
  Static,
  Param,
  ParamRegExp,
  ParamRegExpEnd,
  EscapeNext,
}

function tokenizePath(path: string): Array<Token[]> {
  // /users/:id/posts
  // -> [['users'], [{type: Param, value: 'id'}], ['posts']]

  let state: TokenizerState = TokenizerState.Static
  let buffer: string = ''
  let customRe: string = ''
  const tokens: Array<Token[]> = []
  let segment: Token[] = []

  for (let i = 0; i < path.length; i++) {
    const char = path[i]

    switch (state) {
      case TokenizerState.Static:
        if (char === '/') {
          // 新段
          if (buffer) segment.push({ type: TokenType.Static, value: buffer })
          tokens.push(segment)
          segment = []
          buffer = ''
        } else if (char === ':') {
          // 参数开始
          if (buffer) segment.push({ type: TokenType.Static, value: buffer })
          buffer = ''
          state = TokenizerState.Param
        } else {
          buffer += char
        }
        break

      case TokenizerState.Param:
        if (char === '(') {
          // 自定义正则
          state = TokenizerState.ParamRegExp
        } else if (VALID_PARAM_RE.test(char)) {
          buffer += char
        } else {
          // 参数结束
          segment.push({
            type: TokenType.Param,
            value: buffer,
            regexp: customRe || undefined,
            repeatable: false,
            optional: false,
          })
          buffer = ''
          customRe = ''
          state = TokenizerState.Static
        }
        break
    }
  }

  // 处理最后一段
  if (buffer) segment.push({ type: TokenType.Static, value: buffer })
  tokens.push(segment)

  return tokens
}

2.2 PathParser:正则编译与评分

// packages/router/src/matcher/pathParserRanker.ts
const enum PathScore {
  _multiplier = 10,
  Root = 9 * _multiplier, // just /
  Static = 4 * _multiplier, // /static
  Dynamic = 2 * _multiplier, // /:someId
  BonusCustomRegExp = 1 * _multiplier, // /:someId(\\d+)
  BonusWildcard = -4 * _multiplier - BonusCustomRegExp,
  BonusRepeatable = -2 * _multiplier,
  BonusOptional = -0.8 * _multiplier,
}

function tokensToParser(segments: Array<Token[]>): PathParser {
  const score: Array<number[]> = []
  let pattern = '^'
  const keys: PathParserParamKey[] = []

  for (const segment of segments) {
    const segmentScores: number[] = segment.length ? [] : [PathScore.Root]

    for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
      const token = segment[tokenIndex]
      let subSegmentScore = PathScore.Segment

      if (token.type === TokenType.Static) {
        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 || '[^/]+?'
        let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`

        if (optional) subPattern += '?'
        pattern += '/' + subPattern

        subSegmentScore += PathScore.Dynamic
        if (optional) subSegmentScore += PathScore.BonusOptional
        if (repeatable) subSegmentScore += PathScore.BonusRepeatable
      }

      segmentScores.push(subSegmentScore)
    }

    score.push(segmentScores)
  }

  pattern += '/?$'
  const re = new RegExp(pattern, 'i')

  return {
    re,
    score,
    keys,
    parse(path) {
      const match = path.match(re)
      if (!match) return null
      const params: PathParams = {}
      for (let i = 1; i < match.length; i++) {
        const key = keys[i - 1]
        params[key.name] = match[i] && key.repeatable ? match[i].split('/') : match[i] || ''
      }
      return params
    },
    stringify(params) {
      // 反向生成路径
    },
  }
}

2.3 评分比较

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 = compareArrayScore(aScore[i], bScore[i])
    if (comp !== 0) return comp
    i++
  }

  // 深度越大优先级越高
  if (Math.abs(bScore.length - aScore.length) === 1) {
    return aScore.length > bScore.length
      ? aScore[aScore.length - 1][0] - PathScore.Root
      : PathScore.Root - bScore[bScore.length - 1][0]
  }

  return bScore.length - aScore.length
}

三、导航守卫系统

3.1 六层守卫执行顺序

// packages/router/src/router.ts
function navigate(to, from) {
  const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from)

  return (
    runGuardQueue(guards)
      // 1. beforeRouteLeave(离开当前路由)
      .then(() => {
        guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from)
        return runGuardQueue(guards)
      })
      // 2. beforeEach(全局前置守卫)
      .then(() => {
        guards = beforeGuards.list().map((g) => guardToPromiseFn(g, to, from))
        return runGuardQueue(guards)
      })
      // 3. beforeRouteUpdate(路由复用时更新)
      .then(() => {
        guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from)
        return runGuardQueue(guards)
      })
      // 4. beforeEnter(路由独享守卫)
      .then(() => {
        guards = enteringRecords.flatMap((record) => {
          if (record.beforeEnter) {
            return isArray(record.beforeEnter)
              ? record.beforeEnter.map((g) => guardToPromiseFn(g, to, from))
              : [guardToPromiseFn(record.beforeEnter, to, from)]
          }
          return []
        })
        return runGuardQueue(guards)
      })
      // 5. beforeRouteEnter(组件进入守卫)
      .then(() => {
        guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from)
        return runGuardQueue(guards)
      })
      // 6. beforeResolve(全局解析守卫)
      .then(() => {
        guards = beforeResolveGuards.list().map((g) => guardToPromiseFn(g, to, from))
        return runGuardQueue(guards)
      })
  )
}

3.2 守卫队列执行器

function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
  return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve())
}

3.3 守卫转 Promise

function guardToPromiseFn(guard, to, from) {
  return () =>
    new Promise((resolve, reject) => {
      const next = (valid) => {
        if (valid === false) {
          reject(createRouterError(ErrorTypes.NAVIGATION_ABORTED, { from, to }))
        } else if (valid instanceof Error) {
          reject(valid)
        } else if (isRouteLocation(valid)) {
          reject(createRouterError(ErrorTypes.NAVIGATION_REDIRECTED, { from, to: valid }))
        } else {
          resolve()
        }
      }

      // 调用守卫函数
      const guardReturn = guard.call(guardInstance, to, from, next)

      // 处理 Promise 返回值
      let guardCall = Promise.resolve(guardReturn)
      if (guard.length < 3) guardCall = guardCall.then(next)
      guardCall.catch((err) => reject(err))
    })
}

四、滚动恢复

4.1 位置存储

// packages/router/src/scrollBehavior.ts
export const scrollPositions = new Map<string, _ScrollPositionNormalized | null>()

export function saveScrollPosition(key: string) {
  scrollPositions.set(key, computeScrollPosition())
}

export function getSavedScrollPosition(key: string) {
  const scroll = scrollPositions.get(key)
  scrollPositions.delete(key) // 读取后删除,避免重复使用
  return scroll
}

4.2 位置计算

export const computeScrollPosition = (): _ScrollPositionNormalized | null =>
  history.scrollRestoration === 'manual' ? { left: window.scrollX, top: window.scrollY } : null

4.3 滚动执行

export function scrollToPosition(position: ScrollPosition): void {
  let scrollToOptions: ScrollPositionCoordinates

  if ('el' in position) {
    // 元素选择器
    const el =
      typeof position.el === 'string' ? document.getElementById(position.el.slice(1)) : position.el

    if (!el) return

    const docRect = document.documentElement.getBoundingClientRect()
    const elRect = el.getBoundingClientRect()

    scrollToOptions = {
      behavior: position.behavior,
      left: elRect.left - docRect.left - (position.left || 0),
      top: elRect.top - docRect.top - (position.top || 0),
    }
  } else {
    scrollToOptions = position
  }

  window.scrollTo(scrollToOptions)
}

五、URL 编码

vue-router 对不同位置的字符采用不同编码策略:

// packages/router/src/encoding.ts
// 路径编码:编码 # ?
export function encodePath(text) {
  return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F')
}

// 参数编码:额外编码 /
export function encodeParam(text) {
  return encodePath(text).replace(SLASH_RE, '%2F')
}

// Query 编码:空格转 +,编码 # & =
export function encodeQueryValue(text) {
  return commonEncode(text)
    .replace(PLUS_RE, '%2B')
    .replace(ENC_SPACE_RE, '+')
    .replace(HASH_RE, '%23')
    .replace(AMPERSAND_RE, '%26')
}

// Hash 编码:保留 {} ^
export function encodeHash(text) {
  return commonEncode(text)
    .replace(ENC_CURLY_OPEN_RE, '{')
    .replace(ENC_CURLY_CLOSE_RE, '}')
    .replace(ENC_CARET_RE, '^')
}

设计思路:不同上下文有不同的编码规则,vue-router 严格区分,确保 URL 的正确性。


六、Hash 模式实现

vue-router 的 Hash 模式复用了 HTML5 模式,只是 base 带 #

// packages/router/src/history/hash.ts
export function createWebHashHistory(base?: string): RouterHistory {
  base = location.host ? base || location.pathname + location.search : ''
  if (!base.includes('#')) base += '#'

  // 复用 createWebHistory!
  return createWebHistory(base)
}

巧妙之处:通过在 base 中包含 #,让 HTML5 模式自动处理 Hash 路由。


七、Memory 模式实现

用于 SSR 和测试环境:

// packages/router/src/history/memory.ts
export function createMemoryHistory(base: string = ''): RouterHistory {
  let queue: [url, state][] = [[START, {}]]
  let position: number = 0

  return {
    get location() {
      return queue[position][0]
    },
    get state() {
      return queue[position][1]
    },

    push(to, state) {
      position++
      if (position !== queue.length) queue.splice(position)
      queue.push([to, state])
    },

    replace(to, state) {
      queue.splice(position--, 1)
      queue.splice(++position, 0, [to, state])
    },

    go(delta) {
      position = Math.max(0, Math.min(position + delta, queue.length - 1))
    },
  }
}

八、源码位置速查

功能文件路径
HTML5 Historysrc/history/html5.ts
Hash Historysrc/history/hash.ts
Memory Historysrc/history/memory.ts
History 接口src/history/common.ts
路由匹配器src/matcher/index.ts
路径分词器src/matcher/pathTokenizer.ts
路径评分src/matcher/pathParserRanker.ts
导航守卫src/navigationGuards.ts
滚动恢复src/scrollBehavior.ts
URL 编码src/encoding.ts
RouterLinksrc/RouterLink.ts
RouterViewsrc/RouterView.ts
核心路由器src/router.ts

源码版本

vue-router v5.2.0
源码路径:packages/router/src/