Skip to content

Commit

Permalink
perf: faster handling of static paths (#2)
Browse files Browse the repository at this point in the history
* perf: faster handling of static paths

* Fix problems combining `end: false` and `strict: false`

* Fix problems with resolving paths for `sensitive` and `end` options

* Fix problems caused by merging 'main'
  • Loading branch information
skirtles-code authored Sep 4, 2024
1 parent 6952fea commit f2f9bbe
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 97 deletions.
106 changes: 13 additions & 93 deletions packages/router/src/matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isRouteName,
} from '../types'
import { createRouterError, ErrorTypes, MatcherError } from '../errors'
import { createMatcherTree, isMatchable } from './matcherTree'
import { createRouteRecordMatcher, RouteRecordMatcher } from './pathMatcher'
import { RouteRecordNormalized } from './types'

Expand All @@ -14,8 +15,6 @@ import type {
_PathParserOptions,
} from './pathParserRanker'

import { comparePathParserScore } from './pathParserRanker'

import { warn } from '../warning'
import { assign, noop } from '../utils'
import type { RouteRecordNameGeneric, _RouteRecordProps } from '../typed-routes'
Expand Down Expand Up @@ -58,8 +57,8 @@ export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher {
// normalized ordered array of matchers
const matchers: RouteRecordMatcher[] = []
// normalized ordered tree of matchers
const matcherTree = createMatcherTree()
const matcherMap = new Map<
NonNullable<RouteRecordNameGeneric>,
RouteRecordMatcher
Expand Down Expand Up @@ -203,28 +202,24 @@ export function createRouterMatcher(
const matcher = matcherMap.get(matcherRef)
if (matcher) {
matcherMap.delete(matcherRef)
matchers.splice(matchers.indexOf(matcher), 1)
matcherTree.remove(matcher)
matcher.children.forEach(removeRoute)
matcher.alias.forEach(removeRoute)
}
} else {
const index = matchers.indexOf(matcherRef)
if (index > -1) {
matchers.splice(index, 1)
if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
matcherRef.children.forEach(removeRoute)
matcherRef.alias.forEach(removeRoute)
}
matcherTree.remove(matcherRef)
if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
matcherRef.children.forEach(removeRoute)
matcherRef.alias.forEach(removeRoute)
}
}

function getRoutes() {
return matchers
return matcherTree.toArray()
}

function insertMatcher(matcher: RouteRecordMatcher) {
const index = findInsertionIndex(matcher, matchers)
matchers.splice(index, 0, matcher)
matcherTree.add(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 @@ -297,7 +292,7 @@ export function createRouterMatcher(
)
}

matcher = matchers.find(m => m.re.test(path))
matcher = matcherTree.find(path)
// matcher should have a value after the loop

if (matcher) {
Expand All @@ -310,7 +305,7 @@ export function createRouterMatcher(
// match by name or path of current route
matcher = currentLocation.name
? matcherMap.get(currentLocation.name)
: matchers.find(m => m.re.test(currentLocation.path))
: matcherTree.find(currentLocation.path)
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
location,
Expand Down Expand Up @@ -345,7 +340,7 @@ export function createRouterMatcher(
routes.forEach(route => addRoute(route))

function clearRoutes() {
matchers.length = 0
matcherTree.clear()
matcherMap.clear()
}

Expand Down Expand Up @@ -528,79 +523,4 @@ function checkMissingParamsInAbsolutePath(
}
}

/**
* Performs a binary search to find the correct insertion index for a new matcher.
*
* Matchers are primarily sorted by their score. If scores are tied then we also consider parent/child relationships,
* with descendants coming before ancestors. If there's still a tie, new routes are inserted after existing routes.
*
* @param matcher - new matcher to be inserted
* @param matchers - existing matchers
*/
function findInsertionIndex(
matcher: RouteRecordMatcher,
matchers: RouteRecordMatcher[]
) {
// First phase: binary search based on score
let lower = 0
let upper = matchers.length

while (lower !== upper) {
const mid = (lower + upper) >> 1
const sortOrder = comparePathParserScore(matcher, matchers[mid])

if (sortOrder < 0) {
upper = mid
} else {
lower = mid + 1
}
}

// Second phase: check for an ancestor with the same score
const insertionAncestor = getInsertionAncestor(matcher)

if (insertionAncestor) {
upper = matchers.lastIndexOf(insertionAncestor, upper - 1)

if (__DEV__ && upper < 0) {
// This should never happen
warn(
`Finding ancestor route "${insertionAncestor.record.path}" failed for "${matcher.record.path}"`
)
}
}

return upper
}

function getInsertionAncestor(matcher: RouteRecordMatcher) {
let ancestor: RouteRecordMatcher | undefined = matcher

while ((ancestor = ancestor.parent)) {
if (
isMatchable(ancestor) &&
comparePathParserScore(matcher, ancestor) === 0
) {
return ancestor
}
}

return
}

/**
* Checks if a matcher can be reachable. This means if it's possible to reach it as a route. For example, routes without
* a component, or name, or redirect, are just used to group other routes.
* @param matcher
* @param matcher.record record of the matcher
* @returns
*/
function isMatchable({ record }: RouteRecordMatcher): boolean {
return !!(
record.name ||
(record.components && Object.keys(record.components).length) ||
record.redirect
)
}

export type { PathParserOptions, _PathParserOptions }
Loading

0 comments on commit f2f9bbe

Please sign in to comment.