Skip to content

Commit e9f6ae3

Browse files
skirtles-codeposva
andauthoredJun 10, 2024··
perf: use a binary search for insertMatcher (#2137)
* perf: use a binary search for insertMatcher * Fix matcher ordering in edge cases * chore: docs and logs --------- Co-authored-by: Eduardo San Martin Morote <posva13@gmail.com>
1 parent 7186b74 commit e9f6ae3

File tree

1 file changed

+80
-28
lines changed

1 file changed

+80
-28
lines changed
 

‎packages/router/src/matcher/index.ts

+80-28
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ export function createRouterMatcher(
158158
removeRoute(record.name)
159159
}
160160

161+
// Avoid adding a record that doesn't display anything. This allows passing through records without a component to
162+
// not be reached and pass through the catch all route
163+
if (isMatchable(matcher)) {
164+
insertMatcher(matcher)
165+
}
166+
161167
if (mainNormalizedRecord.children) {
162168
const children = mainNormalizedRecord.children
163169
for (let i = 0; i < children.length; i++) {
@@ -177,17 +183,6 @@ export function createRouterMatcher(
177183
// if (parent && isAliasRecord(originalRecord)) {
178184
// parent.children.push(originalRecord)
179185
// }
180-
181-
// Avoid adding a record that doesn't display anything. This allows passing through records without a component to
182-
// not be reached and pass through the catch all route
183-
if (
184-
(matcher.record.components &&
185-
Object.keys(matcher.record.components).length) ||
186-
matcher.record.name ||
187-
matcher.record.redirect
188-
) {
189-
insertMatcher(matcher)
190-
}
191186
}
192187

193188
return originalMatcher
@@ -223,17 +218,8 @@ export function createRouterMatcher(
223218
}
224219

225220
function insertMatcher(matcher: RouteRecordMatcher) {
226-
let i = 0
227-
while (
228-
i < matchers.length &&
229-
comparePathParserScore(matcher, matchers[i]) >= 0 &&
230-
// Adding children with empty path should still appear before the parent
231-
// https://github.com/vuejs/router/issues/1124
232-
(matcher.record.path !== matchers[i].record.path ||
233-
!isRecordChildOf(matcher, matchers[i]))
234-
)
235-
i++
236-
matchers.splice(i, 0, matcher)
221+
const index = findInsertionIndex(matcher, matchers)
222+
matchers.splice(index, 0, matcher)
237223
// only add the original record to the name map
238224
if (matcher.record.name && !isAliasRecord(matcher))
239225
matcherMap.set(matcher.record.name, matcher)
@@ -525,12 +511,78 @@ function checkMissingParamsInAbsolutePath(
525511
}
526512
}
527513

528-
function isRecordChildOf(
529-
record: RouteRecordMatcher,
530-
parent: RouteRecordMatcher
531-
): boolean {
532-
return parent.children.some(
533-
child => child === record || isRecordChildOf(record, child)
514+
/**
515+
* Performs a binary search to find the correct insertion index for a new matcher.
516+
*
517+
* Matchers are primarily sorted by their score. If scores are tied then we also consider parent/child relationships,
518+
* with descendants coming before ancestors. If there's still a tie, new routes are inserted after existing routes.
519+
*
520+
* @param matcher - new matcher to be inserted
521+
* @param matchers - existing matchers
522+
*/
523+
function findInsertionIndex(
524+
matcher: RouteRecordMatcher,
525+
matchers: RouteRecordMatcher[]
526+
) {
527+
// First phase: binary search based on score
528+
let lower = 0
529+
let upper = matchers.length
530+
531+
while (lower !== upper) {
532+
const mid = (lower + upper) >> 1
533+
const sortOrder = comparePathParserScore(matcher, matchers[mid])
534+
535+
if (sortOrder < 0) {
536+
upper = mid
537+
} else {
538+
lower = mid + 1
539+
}
540+
}
541+
542+
// Second phase: check for an ancestor with the same score
543+
const insertionAncestor = getInsertionAncestor(matcher)
544+
545+
if (insertionAncestor) {
546+
upper = matchers.lastIndexOf(insertionAncestor, upper - 1)
547+
548+
if (__DEV__ && upper < 0) {
549+
// This should never happen
550+
warn(
551+
`Finding ancestor route "${insertionAncestor.record.path}" failed for "${matcher.record.path}"`
552+
)
553+
}
554+
}
555+
556+
return upper
557+
}
558+
559+
function getInsertionAncestor(matcher: RouteRecordMatcher) {
560+
let ancestor: RouteRecordMatcher | undefined = matcher
561+
562+
while ((ancestor = ancestor.parent)) {
563+
if (
564+
isMatchable(ancestor) &&
565+
comparePathParserScore(matcher, ancestor) === 0
566+
) {
567+
return ancestor
568+
}
569+
}
570+
571+
return
572+
}
573+
574+
/**
575+
* Checks if a matcher can be reachable. This means if it's possible to reach it as a route. For example, routes without
576+
* a component, or name, or redirect, are just used to group other routes.
577+
* @param matcher
578+
* @param matcher.record record of the matcher
579+
* @returns
580+
*/
581+
function isMatchable({ record }: RouteRecordMatcher): boolean {
582+
return !!(
583+
record.name ||
584+
(record.components && Object.keys(record.components).length) ||
585+
record.redirect
534586
)
535587
}
536588

0 commit comments

Comments
 (0)
Please sign in to comment.