tanstack-router 源码深度解析

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

源码版本:latest
核心文件:packages/router-core/src/ + packages/history/src/


一、History 层实现

1.1 microtask 队列节流

tanstack-router 的 History 实现最激进——用 microtask 队列节流防止高频调用:

// packages/history/src/index.ts
export function createBrowserHistory(opts?): RouterHistory {
  const originalPushState = win.history.pushState
  const originalReplaceState = win.history.replaceState

  let next: [href: string, state: any, isPush: boolean] | undefined
  let currentLocation = parseLocation()
  let rollbackLocation: HistoryLocation | undefined

  // 批量刷新到浏览器
  const flush = () => {
    if (!next) return

    history._ignoreSubscribers = true
    ;(next[2] ? win.history.pushState : win.history.replaceState)(next[1], '', next[0])
    history._ignoreSubscribers = false

    next = undefined
    rollbackLocation = undefined
  }

  // 队列化历史操作
  const queueHistoryAction = (isPush, destHref, state) => {
    const href = createHref(destHref)
    const hasPendingAction = !!next

    if (!hasPendingAction) rollbackLocation = currentLocation

    // 乐观更新内存中的 location
    currentLocation = parseHref(destHref, state)

    // 记录待推送的更新
    next = [href, state, next?.[2] || isPush]

    if (!hasPendingAction) {
      // 用 microtask 批量处理
      queueMicrotask(() => flush())
    }
  }

  // 猴子补丁拦截原生 API
  win.history.pushState = function (...args) {
    const res = originalPushState.apply(win.history, args)
    if (!history._ignoreSubscribers) onPushPop('PUSH')
    return res
  }

  win.history.replaceState = function (...args) {
    const res = originalReplaceState.apply(win.history, args)
    if (!history._ignoreSubscribers) onPushPop('REPLACE')
    return res
  }
}

1.2 为什么需要队列节流?

源码注释里写了原因:

In some browsers, calling history.pushState or history.replaceState in quick succession can cause the browser to ignore subsequent calls.

高频调用会导致浏览器丢弃状态。tanstack-router 用队列 + microtask 来平滑处理。

1.3 State 结构

interface ParsedHistoryState extends HistoryState {
  key?: string
  __TSR_key?: string
  __TSR_index: number
}

设计思路:用 __TSR_ 前缀避免与其他库冲突,index 用于 blocker 计算。

1.4 Blocker 实现

tanstack-router 在 History 层内置了 blocker 支持:

const tryNavigation = async ({ task, navigateOpts, ...actionInfo }) => {
  const ignoreBlocker = navigateOpts?.ignoreBlocker ?? false
  if (ignoreBlocker) {
    task()
    return
  }

  const blockers = opts.getBlockers?.() ?? []
  const isPushOrReplace = actionInfo.type === 'PUSH' || actionInfo.type === 'REPLACE'

  if (typeof document !== 'undefined' && blockers.length && isPushOrReplace) {
    for (const blocker of blockers) {
      const nextLocation = parseHref(actionInfo.path, actionInfo.state)
      const isBlocked = await blocker.blockerFn({
        currentLocation: location,
        nextLocation,
        action: actionInfo.type,
      })
      if (isBlocked) {
        opts.onBlocked?.()
        return
      }
    }
  }

  task()
}

设计思路:在 History 层处理 blocker,可以拦截所有导航,包括用户直接调用 history.pushState() 的情况。


二、路由匹配:Segment Trie + Uint16Array

2.1 段类型定义

// packages/router-core/src/new-process-route-tree.ts
const SEGMENT_TYPE_PATHNAME = 0
const SEGMENT_TYPE_PARAM = 1
const SEGMENT_TYPE_WILDCARD = 2
const SEGMENT_TYPE_OPTIONAL_PARAM = 3

2.2 用 Uint16Array 存储解析结果

type ParsedSegment = Uint16Array & {
  0: SegmentKind // 段类型
  1: number // 前缀结束位置
  2: number // 值开始位置
  3: number // 值结束位置
  4: number // 后缀开始位置
  5: number // 段结束位置
}

function parseSegment(
  path: string,
  start: number,
  output: Uint16Array = new Uint16Array(6),
): ParsedSegment {
  const next = path.indexOf('/', start)
  const end = next === -1 ? path.length : next
  const part = path.substring(start, end)

  // 静态路径段
  if (!part || !part.includes('$')) {
    output[0] = SEGMENT_TYPE_PATHNAME
    output[1] = start
    output[2] = start
    output[3] = end
    output[4] = end
    output[5] = end
    return output as ParsedSegment
  }

  // $ 通配符
  if (part === '$') {
    output[0] = SEGMENT_TYPE_WILDCARD
    output[1] = start
    output[2] = start
    output[3] = path.length
    output[4] = path.length
    output[5] = path.length
    return output as ParsedSegment
  }

  // $param 参数
  if (part.charCodeAt(0) === 36) {
    output[0] = SEGMENT_TYPE_PARAM
    output[1] = start
    output[2] = start + 1 // 跳过 $
    output[3] = end
    output[4] = end
    output[5] = end
    return output as ParsedSegment
  }

  // {-$param} 可选参数
  const openBrace = part.indexOf('{')
  let closeBrace
  if (openBrace !== -1 && (closeBrace = part.indexOf('}', openBrace)) !== -1) {
    const firstChar = part.charCodeAt(openBrace + 1)
    if (firstChar === 45) {
      // '-'
      if (part.charCodeAt(openBrace + 2) === 36) {
        // '$'
        output[0] = SEGMENT_TYPE_OPTIONAL_PARAM
        output[1] = start + openBrace
        output[2] = start + openBrace + 3
        output[3] = start + closeBrace
        output[4] = start + closeBrace + 1
        output[5] = end
        return output as ParsedSegment
      }
    }
  }

  // 默认静态
  output[0] = SEGMENT_TYPE_PATHNAME
  output[1] = start
  output[2] = start
  output[3] = end
  output[4] = end
  output[5] = end
  return output as ParsedSegment
}

为什么用 Uint16Array?

减少垃圾回收压力。每次路由匹配都会解析路径,用 TypedArray 可以复用内存,避免频繁创建对象。这是典型的「性能优先」设计。

2.3 Trie 构建

function parseSegments(defaultCaseSensitive, data, route, start, node, depth) {
  let cursor = start
  const fullPath = route.fullPath

  while (cursor < fullPath.length) {
    data = parseSegment(fullPath, cursor, data)
    const kind = data[0]
    const end = data[5]

    if (kind === SEGMENT_TYPE_PATHNAME) {
      // 静态段:查找或创建子节点
      const value = fullPath.substring(data[2], data[3])
      let child = node.children?.find((c) => c.type === SEGMENT_TYPE_PATHNAME && c.value === value)
      if (!child) {
        child = { type: SEGMENT_TYPE_PATHNAME, value, children: [] }
        node.children = node.children || []
        node.children.push(child)
      }
      node = child
    } else if (kind === SEGMENT_TYPE_PARAM) {
      // 参数段
      const name = fullPath.substring(data[2], data[3])
      let child = node.children?.find((c) => c.type === SEGMENT_TYPE_PARAM && c.name === name)
      if (!child) {
        child = { type: SEGMENT_TYPE_PARAM, name, children: [] }
        node.children = node.children || []
        node.children.push(child)
      }
      node = child
    }

    cursor = end + 1
  }

  // 存储路由
  node.route = route
}

三、beforeLoad + loader 数据模型

3.1 beforeLoad

const route = createRoute({
  beforeLoad: ({ params, search, context }) => {
    // 可以返回 context,传递给 loader 和组件
    return { userId: params.id }
  },
})

执行时机:路由加载前,同步/异步都可以。

3.2 loader

const route = createRoute({
  loader: async ({ context, deps, abortController }) => {
    // context 来自 beforeLoad 和父路由
    // deps 来自 loaderDeps
    const user = await fetchUser(context.userId, {
      signal: abortController.signal,
    })
    return user
  },
})

3.3 loaderDeps

const route = createRoute({
  // 声明 loader 依赖
  loaderDeps: ({ search: { page, sort } }) => ({ page, sort }),

  // loader 接收 deps
  loader: async ({ deps }) => {
    // 只有当 page 或 sort 变化时才重新加载
    return fetchUsers({ page: deps.page, sort: deps.sort })
  },
})

设计思路loaderDeps 把「什么时候重新加载」这个问题从框架层下沉到用户层,让用户精确控制依赖关系。这比 react-router 的 shouldRevalidate 更灵活。


四、缓存优化

4.1 LRU Cache

// packages/router-core/src/lru-cache.ts
export function createLRUCache<T>(maxSize: number): LRUCache<T> {
  let cache = new Map<string, T>()

  return {
    get(key) {
      if (!cache.has(key)) return undefined

      // 命中时移到最前
      const value = cache.get(key)!
      cache.delete(key)
      cache.set(key, value)
      return value
    },
    set(key, value) {
      if (cache.has(key)) {
        cache.delete(key)
      } else if (cache.size >= maxSize) {
        // 淘汰最久未使用
        const firstKey = cache.keys().next().value
        cache.delete(firstKey)
      }
      cache.set(key, value)
    },
    has(key) {
      return cache.has(key)
    },
    delete(key) {
      return cache.delete(key)
    },
    clear() {
      cache.clear()
    },
  }
}

// 使用
this.resolvePathCache = createLRUCache(1000)

4.2 WeakMap 缓存

// 避免内存泄漏,路由对象被 GC 时自动清理
private routeBranchCache = new WeakMap<AnyRoute, ReadonlyArray<AnyRoute>>()
private lightweightCache = new WeakMap<ParsedLocation, LightweightRouteMatchCacheEntry>()

设计思路:WeakMap 的 key 是弱引用,当路由对象被垃圾回收时,缓存也会自动清理。

4.3 结构化共享

// packages/router-core/src/structuralSharing.ts
export function replaceEqualDeep<T>(a: any, b: any): T {
  if (a === b) return a

  if (isArray(a) && isArray(b)) {
    const arr = new Array(Math.max(a.length, b.length))
    for (let i = 0; i < arr.length; i++) {
      arr[i] = replaceEqualDeep(a[i], b[i])
    }
    return arr as T
  }

  if (isObject(a) && isObject(b)) {
    const obj = { ...a }
    let equal = true

    for (const key in b) {
      if (key in a) {
        obj[key] = replaceEqualDeep(a[key], b[key])
        if (obj[key] !== a[key]) equal = false
      } else {
        obj[key] = b[key]
        equal = false
      }
    }

    return equal ? a : (obj as T)
  }

  return b
}

为什么需要结构化共享?

React 的 useMemouseEffect 依赖引用比较。如果每次导航都返回新对象,即使数据没变也会触发重渲染。结构化共享保证:数据不变时,引用也不变。


五、类型安全

5.1 ParseRoute 递归类型

// packages/router-core/src/routeInfo.ts
export type ParseRoute<TRouteTree, TAcc = TRouteTree> = TRouteTree extends {
  types: { children: infer TChildren }
}
  ? unknown extends TChildren
    ? TAcc
    : TChildren extends ReadonlyArray<any>
      ? ParseRoute<TChildren[number], TAcc | TChildren[number]>
      : ParseRoute<TChildren[keyof TChildren], TAcc | TChildren[keyof TChildren]>
  : TAcc

设计思路:用 TypeScript 的条件类型 + 递归推导,在编译时构建完整的路由类型映射。

5.2 RoutesById 类型

export type CodeRoutesById<TRouteTree extends AnyRoute> =
  ParseRoute<TRouteTree> extends infer TRoutes extends AnyRoute
    ? {
        [K in TRoutes as K['id']]: K
      }
    : never

export type RoutesById<TRouteTree extends AnyRoute> =
  InferFileRouteTypes<TRouteTree> extends never
    ? CodeRoutesById<TRouteTree>
    : InferFileRouteTypes<TRouteTree>['fileRoutesById']

5.3 路径类型推导

export type RouteByPath<TRouteTree extends AnyRoute, TPath> = Extract<
  RoutesByPath<TRouteTree>[TPath & keyof RoutesByPath<TRouteTree>],
  AnyRoute
>

效果

// 导航时完全类型安全
navigate({
  to: '/users/$id',
  params: { id: 123 }, // 类型错误!应该是 string
  search: { page: '1' }, // 类型错误!应该是 number
})

// 组件中完全推导
function UserPage() {
  const { id } = useParams({ from: '/users/$id' }) // id: string
  const { page } = useSearch({ from: '/users/$id' }) // page: number
}

六、validateSearch + Search Middleware

6.1 validateSearch

const route = createRoute({
  validateSearch: z.object({
    page: z.number().default(1),
    sort: z.enum(['name', 'date']).default('name'),
    filter: z.string().optional(),
  }),
})

效果:搜索参数自动验证和类型推导。

6.2 retainSearchParams

import { retainSearchParams } from '@tanstack/react-router'

const route = createRoute({
  search: {
    middlewares: [
      retainSearchParams(['page', 'sort']), // 跨导航保持参数
    ],
  },
})

实现原理

export function retainSearchParams(keys) {
  return ({ search, next }) => {
    const { search: resultSearch, meta } = next(search, true)

    if (keys === true) {
      // 保留所有参数
      const copy = { ...search, ...resultSearch }
      // 恢复被移除的参数
      for (const key of meta.removed?.keys() || []) {
        if (!meta.explicit?.has(key)) {
          copy[key] = search[key]
        }
      }
      return copy
    }

    // 只保留指定参数
    const copy = { ...resultSearch }
    for (const key of keys) {
      if (key in search && !(key in copy)) {
        copy[key] = search[key]
      }
    }
    return copy
  }
}

6.3 stripSearchParams

import { stripSearchParams } from '@tanstack/react-router'

const route = createRoute({
  search: {
    middlewares: [
      stripSearchParams({ page: 1, sort: 'name' }), // 移除默认值参数
    ],
  },
})

设计思路:当参数等于默认值时,从 URL 中移除,保持 URL 简洁。


七、预加载策略

7.1 三种模式

const router = createRouter({
  // 1. Intent:鼠标悬停时预加载
  defaultPreload: 'intent',
  defaultPreloadDelay: 50,

  // 2. Viewport:进入视口时预加载
  defaultPreload: 'viewport',

  // 3. 手动预加载
})

// 手动预加载
const route = createRoute({
  preloadStaleTime: 30_000, // 30秒内不重复预加载
  preloadIntentProximity: 50, // 鼠标距离触发预加载
})

7.2 实现原理

// Intent 模式:监听鼠标事件
link.addEventListener('mouseenter', () => {
  router.preloadRoute({ to: link.href })
})

link.addEventListener('touchstart', () => {
  router.preloadRoute({ to: link.href }) // 立即预加载
})

// Viewport 模式:使用 IntersectionObserver
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      router.preloadRoute({ to: entry.target.href })
    }
  })
})

八、源码位置速查

功能文件路径
History 实现packages/history/src/index.ts
核心路由器packages/router-core/src/router.ts
路由匹配packages/router-core/src/new-process-route-tree.ts
路径解析packages/router-core/src/path.ts
LRU Cachepackages/router-core/src/lru-cache.ts
结构化共享packages/router-core/src/structuralSharing.ts
类型推导packages/router-core/src/routeInfo.ts
路由定义packages/router-core/src/route.ts
搜索参数packages/router-core/src/searchMiddleware.ts
滚动恢复packages/router-core/src/scroll-restoration.ts

源码版本

tanstack-router latest
源码路径:packages/router-core/src/ + packages/history/src/