Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: router matcher sort cache #2136

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/router/src/index.ts
@@ -1,7 +1,7 @@
export { createWebHistory } from './history/html5'
export { createMemoryHistory } from './history/memory'
export { createWebHashHistory } from './history/hash'
export { createRouterMatcher } from './matcher'
export { createRouterMatcher, createRouterMatcherSortCache } from './matcher'
export type { RouterMatcher } from './matcher'

export { parseQuery, stringifyQuery } from './query'
Expand Down
79 changes: 65 additions & 14 deletions packages/router/src/matcher/index.ts
Expand Up @@ -7,8 +7,12 @@ import {
_RouteRecordProps,
} from '../types'
import { createRouterError, ErrorTypes, MatcherError } from '../errors'
import { createRouteRecordMatcher, RouteRecordMatcher } from './pathMatcher'
import { RouteRecordNormalized } from './types'
import {
createRouteRecordCacheKey,
createRouteRecordMatcher,
RouteRecordMatcher,
} from './pathMatcher'
import { RouteRecord, RouteRecordNormalized } from './types'

import type {
PathParams,
Expand Down Expand Up @@ -61,8 +65,15 @@ export function createRouterMatcher(
// normalized ordered array of matchers
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
let restoreSort = globalOptions.sortCache != null

globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
{
strict: false,
end: true,
sensitive: false,
sortCache: undefined,
} as PathParserOptions,
globalOptions
)

Expand Down Expand Up @@ -223,17 +234,21 @@ export function createRouterMatcher(
}

function insertMatcher(matcher: RouteRecordMatcher) {
let i = 0
while (
i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// Adding children with empty path should still appear before the parent
// https://github.com/vuejs/router/issues/1124
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i]))
)
i++
matchers.splice(i, 0, matcher)
if (restoreSort) {
matchers.push(matcher)
} else {
let i = 0
while (
i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// Adding children with empty path should still appear before the parent
// https://github.com/vuejs/router/issues/1124
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i]))
)
i++
matchers.splice(i, 0, matcher)
}
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher)
Expand Down Expand Up @@ -348,9 +363,45 @@ export function createRouterMatcher(
// add initial routes
routes.forEach(route => addRoute(route))

// restore matcher order from cache
if (globalOptions.sortCache != null) {
try {
matchers.sort(
(a, b) =>
globalOptions!.sortCache![a.cacheKey!] -
globalOptions!.sortCache![b.cacheKey!]
)

for (const entry of matchers) {
delete entry.cacheKey
}
} catch {
console.log('Restore failure.')
}
}

restoreSort = false

return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

/**
* Creates a router matcher sort cache
*
* @internal
*/
export function createRouterMatcherSortCache(
records: RouteRecord[]
): Record<string, number> {
const sortCache: Record<string, number> = {}

for (let i = 0; i < records.length; i++) {
sortCache[createRouteRecordCacheKey(records[i])] = i
}

return sortCache
}

function paramsFromLocation(
params: MatcherLocation['params'],
keys: string[]
Expand Down
10 changes: 10 additions & 0 deletions packages/router/src/matcher/pathMatcher.ts
Expand Up @@ -14,6 +14,12 @@ export interface RouteRecordMatcher extends PathParser {
children: RouteRecordMatcher[]
// aliases that must be removed when removing this record
alias: RouteRecordMatcher[]
// defined when sort cache is present
cacheKey?: string
}

export function createRouteRecordCacheKey(record: Readonly<RouteRecord>) {
return String(record?.name) + '___' + record.path
}

export function createRouteRecordMatcher(
Expand Down Expand Up @@ -43,6 +49,10 @@ export function createRouteRecordMatcher(
alias: [],
})

if (options?.sortCache) {
matcher.cacheKey = createRouteRecordCacheKey(record)
}

if (parent) {
// both are aliases or both are not aliases
// we don't want to mix them because the order is used when
Expand Down
15 changes: 13 additions & 2 deletions packages/router/src/matcher/pathParserRanker.ts
Expand Up @@ -79,17 +79,28 @@ export interface _PathParserOptions {
* @defaultValue `true`
*/
end?: boolean

/**
* A precomputed object that maps matchers to its sorted index
*
* @internal
* @defaultValue `undefined`
*/
sortCache?: Record<string, number>
}

export type PathParserOptions = Pick<
_PathParserOptions,
'end' | 'sensitive' | 'strict'
'end' | 'sensitive' | 'strict' | 'sortCache'
>

// default pattern for a param: non-greedy everything but /
const BASE_PARAM_PATTERN = '[^/]+?'

const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
const BASE_PATH_PARSER_OPTIONS: Omit<
Required<_PathParserOptions>,
'sortCache'
> = {
sensitive: false,
strict: false,
start: true,
Expand Down