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 的 useMemo、useEffect 依赖引用比较。如果每次导航都返回新对象,即使数据没变也会触发重渲染。结构化共享保证:数据不变时,引用也不变。
五、类型安全
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 Cache | packages/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/