1
1
import { RouteRecordMatcher } from './pathMatcher'
2
2
import { comparePathParserScore } from './pathParserRanker'
3
+ import { warn } from '../warning'
3
4
4
- type MatcherTree = {
5
+ type MatcherNode = {
5
6
add : ( matcher : RouteRecordMatcher ) => void
6
7
remove : ( matcher : RouteRecordMatcher ) => void
7
8
find : ( path : string ) => RouteRecordMatcher | undefined
8
9
toArray : ( ) => RouteRecordMatcher [ ]
9
10
}
10
11
12
+ type MatcherTree = MatcherNode & {
13
+ clear : ( ) => void
14
+ }
15
+
11
16
function normalizePath ( path : string ) {
12
17
// We match case-insensitively initially, then let the matcher check more rigorously
13
18
path = path . toUpperCase ( )
@@ -37,9 +42,8 @@ function chooseBestMatcher(
37
42
}
38
43
39
44
export function createMatcherTree ( ) : MatcherTree {
40
- const root = createMatcherNode ( )
41
- const exactMatchers : Record < string , RouteRecordMatcher [ ] > =
42
- Object . create ( null )
45
+ let root = createMatcherNode ( )
46
+ let exactMatchers : Record < string , RouteRecordMatcher [ ] > = Object . create ( null )
43
47
44
48
return {
45
49
add ( matcher ) {
@@ -66,6 +70,11 @@ export function createMatcherTree(): MatcherTree {
66
70
}
67
71
} ,
68
72
73
+ clear ( ) {
74
+ root = createMatcherNode ( )
75
+ exactMatchers = Object . create ( null )
76
+ } ,
77
+
69
78
find ( path ) {
70
79
const matchers = exactMatchers [ normalizePath ( path ) ]
71
80
@@ -87,8 +96,8 @@ export function createMatcherTree(): MatcherTree {
87
96
}
88
97
}
89
98
90
- function createMatcherNode ( depth = 1 ) : MatcherTree {
91
- let segments : Record < string , MatcherTree > | null = null
99
+ function createMatcherNode ( depth = 1 ) : MatcherNode {
100
+ let segments : Record < string , MatcherNode > | null = null
92
101
let wildcards : RouteRecordMatcher [ ] | null = null
93
102
94
103
return {
@@ -191,29 +200,83 @@ function insertMatcher(
191
200
matchers . splice ( index , 0 , matcher )
192
201
}
193
202
203
+ /**
204
+ * Performs a binary search to find the correct insertion index for a new matcher.
205
+ *
206
+ * Matchers are primarily sorted by their score. If scores are tied then we also consider parent/child relationships,
207
+ * with descendants coming before ancestors. If there's still a tie, new routes are inserted after existing routes.
208
+ *
209
+ * @param matcher - new matcher to be inserted
210
+ * @param matchers - existing matchers
211
+ */
194
212
function findInsertionIndex (
195
213
matcher : RouteRecordMatcher ,
196
214
matchers : RouteRecordMatcher [ ]
197
215
) {
198
- let i = 0
199
- while (
200
- i < matchers . length &&
201
- comparePathParserScore ( matcher , matchers [ i ] ) >= 0 &&
202
- // Adding children with empty path should still appear before the parent
203
- // https://github.com/vuejs/router/issues/1124
204
- ( matcher . record . path !== matchers [ i ] . record . path ||
205
- ! isRecordChildOf ( matcher , matchers [ i ] ) )
206
- )
207
- i ++
216
+ // First phase: binary search based on score
217
+ let lower = 0
218
+ let upper = matchers . length
219
+
220
+ while ( lower !== upper ) {
221
+ const mid = ( lower + upper ) >> 1
222
+ const sortOrder = comparePathParserScore ( matcher , matchers [ mid ] )
223
+
224
+ if ( sortOrder < 0 ) {
225
+ upper = mid
226
+ } else {
227
+ lower = mid + 1
228
+ }
229
+ }
230
+
231
+ // Second phase: check for an ancestor with the same score
232
+ const insertionAncestor = getInsertionAncestor ( matcher )
233
+
234
+ if ( insertionAncestor ) {
235
+ upper = matchers . lastIndexOf ( insertionAncestor , upper - 1 )
236
+
237
+ if ( __DEV__ && upper < 0 ) {
238
+ // This should never happen
239
+ warn (
240
+ `Finding ancestor route "${ insertionAncestor . record . path } " failed for "${ matcher . record . path } "`
241
+ )
242
+ }
243
+ }
244
+
245
+ return upper
246
+ }
247
+
248
+ function getInsertionAncestor ( matcher : RouteRecordMatcher ) {
249
+ let ancestor : RouteRecordMatcher | undefined = matcher
250
+
251
+ while ( ( ancestor = ancestor . parent ) ) {
252
+ if (
253
+ isMatchable ( ancestor ) &&
254
+ matcher . staticTokens . length === ancestor . staticTokens . length &&
255
+ comparePathParserScore ( matcher , ancestor ) === 0 &&
256
+ ancestor . staticTokens . every (
257
+ ( token , index ) =>
258
+ matcher . staticTokens [ index ] . toUpperCase ( ) === token . toUpperCase ( )
259
+ )
260
+ ) {
261
+ return ancestor
262
+ }
263
+ }
208
264
209
- return i
265
+ return
210
266
}
211
267
212
- function isRecordChildOf (
213
- record : RouteRecordMatcher ,
214
- parent : RouteRecordMatcher
215
- ) : boolean {
216
- return parent . children . some (
217
- child => child === record || isRecordChildOf ( record , child )
268
+ /**
269
+ * Checks if a matcher can be reachable. This means if it's possible to reach it as a route. For example, routes without
270
+ * a component, or name, or redirect, are just used to group other routes.
271
+ * @param matcher
272
+ * @param matcher.record record of the matcher
273
+ * @returns
274
+ */
275
+ // TODO: This should probably live elsewhere
276
+ export function isMatchable ( { record } : RouteRecordMatcher ) : boolean {
277
+ return ! ! (
278
+ record . name ||
279
+ ( record . components && Object . keys ( record . components ) . length ) ||
280
+ record . redirect
218
281
)
219
282
}
0 commit comments