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 History | src/history/html5.ts |
| Hash History | src/history/hash.ts |
| Memory History | src/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 |
| RouterLink | src/RouterLink.ts |
| RouterView | src/RouterView.ts |
| 核心路由器 | src/router.ts |
源码版本
vue-router v5.2.0
源码路径:packages/router/src/