Skip to content

Commit 9b7e3ee

Browse files
danielroeposva
authored andcommitted
perf: avoid depending on the current route to resolve absolute locations
1 parent 47afb19 commit 9b7e3ee

3 files changed

Lines changed: 161 additions & 4 deletions

File tree

packages/router/__tests__/router.spec.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @vitest-environment happy-dom
33
*/
44
import fakePromise from 'faked-promise'
5+
import { computed, effectScope } from 'vue'
56
import type { RouteLocationRaw } from '../src/typed-routes'
67
import { createRouter } from '../src/router'
78
import { createMemoryHistory } from '../src/history/memory'
@@ -1030,6 +1031,93 @@ describe('Router', () => {
10301031
})
10311032
})
10321033

1034+
describe('resolve reactivity', () => {
1035+
it('resolves absolute locations the same regardless of the current location', async () => {
1036+
const { router } = await newRouter()
1037+
const locations: RouteLocationRaw[] = [
1038+
'/',
1039+
'/foo',
1040+
'/foo?q=1#h',
1041+
'/p/abc',
1042+
{ path: '/foo', query: { q: '1' }, hash: '#h' },
1043+
]
1044+
const before = locations.map(to => router.resolve(to))
1045+
await router.push('/parent/child')
1046+
const after = locations.map(to => router.resolve(to))
1047+
expect(after).toStrictEqual(before)
1048+
})
1049+
1050+
it('does not re-run a computed with an absolute location on navigation', async () => {
1051+
const { router } = await newRouter()
1052+
const scope = effectScope()
1053+
let runs = 0
1054+
const route = scope.run(() =>
1055+
computed(() => {
1056+
runs++
1057+
return router.resolve('/foo')
1058+
})
1059+
)!
1060+
expect(route.value.name).toBe('Foo')
1061+
expect(runs).toBe(1)
1062+
await router.push('/search')
1063+
expect(route.value.name).toBe('Foo')
1064+
expect(runs).toBe(1)
1065+
scope.stop()
1066+
})
1067+
1068+
it('re-runs a computed with a relative location on navigation', async () => {
1069+
const { router } = await newRouter()
1070+
const scope = effectScope()
1071+
const route = scope.run(() => computed(() => router.resolve('child')))!
1072+
expect(route.value.path).toBe('/child')
1073+
await router.push('/parent/child')
1074+
expect(route.value.path).toBe('/parent/child')
1075+
scope.stop()
1076+
})
1077+
1078+
it('re-runs a computed with a named location inheriting params on navigation', async () => {
1079+
const { router } = await newRouter()
1080+
await router.push('/p/a')
1081+
const scope = effectScope()
1082+
const route = scope.run(() =>
1083+
computed(() => router.resolve({ name: 'Param' }))
1084+
)!
1085+
expect(route.value.path).toBe('/p/a')
1086+
await router.push('/p/b')
1087+
expect(route.value.path).toBe('/p/b')
1088+
scope.stop()
1089+
})
1090+
1091+
it('re-runs a computed when routes are added or removed', async () => {
1092+
const { router } = await newRouter({ routes: [routes[0]] })
1093+
const scope = effectScope()
1094+
const route = scope.run(() => computed(() => router.resolve('/late')))!
1095+
expect(route.value.matched).toHaveLength(0)
1096+
expect('No match found').toHaveBeenWarned()
1097+
const remove = router.addRoute({
1098+
path: '/late',
1099+
name: 'late',
1100+
component: components.Foo,
1101+
})
1102+
expect(route.value.matched).toHaveLength(1)
1103+
remove()
1104+
expect(route.value.matched).toHaveLength(0)
1105+
router.addRoute({ path: '/late', component: components.Foo })
1106+
expect(route.value.matched).toHaveLength(1)
1107+
router.clearRoutes()
1108+
expect(route.value.matched).toHaveLength(0)
1109+
scope.stop()
1110+
})
1111+
1112+
it('uses an explicitly passed currentLocation', async () => {
1113+
const { router } = await newRouter()
1114+
const current = router.resolve('/parent/child')
1115+
expect(router.resolve('child', current).path).toBe('/parent/child')
1116+
await router.push('/foo')
1117+
expect(router.resolve('child', current).path).toBe('/parent/child')
1118+
})
1119+
})
1120+
10331121
describe('Dynamic Routing', () => {
10341122
it('resolves new added routes', async () => {
10351123
const { router } = await newRouter({ routes: [] })

packages/router/__tests__/useLink.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,42 @@ describe('useLink', () => {
9494
})
9595
})
9696

97+
describe('active state', () => {
98+
it('updates isActive and isExactActive after navigation', async () => {
99+
const router = createRouter({
100+
history: createMemoryHistory(),
101+
routes: [
102+
{ path: '/', component: {} },
103+
{ path: '/a', component: {} },
104+
{ path: '/b', component: {} },
105+
],
106+
})
107+
await router.push('/')
108+
109+
let link!: ReturnType<typeof useLink>
110+
mount(
111+
{
112+
setup() {
113+
link = useLink({ to: '/a' })
114+
return () => ''
115+
},
116+
},
117+
{ global: { plugins: [router] } }
118+
)
119+
120+
expect(link.isActive.value).toBe(false)
121+
expect(link.isExactActive.value).toBe(false)
122+
123+
await router.push('/a')
124+
expect(link.isActive.value).toBe(true)
125+
expect(link.isExactActive.value).toBe(true)
126+
127+
await router.push('/b')
128+
expect(link.isActive.value).toBe(false)
129+
expect(link.isExactActive.value).toBe(false)
130+
})
131+
})
132+
97133
describe('warnings', () => {
98134
mockWarn()
99135

packages/router/src/router.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ export function createRouter(options: RouterOptions): Router {
163163
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
164164
START_LOCATION_NORMALIZED
165165
)
166+
// incremented whenever the route table changes so that `resolve()` can be
167+
// used within `computed()` and still pick up added or removed routes
168+
const routesVersion = shallowRef(0)
166169
let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
167170

168171
// leave the scrollRestoration if no scrollBehavior is provided
@@ -195,18 +198,29 @@ export function createRouter(options: RouterOptions): Router {
195198
record = parentOrRoute
196199
}
197200

198-
return matcher.addRoute(record, parent)
201+
const removeRoute = matcher.addRoute(record, parent)
202+
routesVersion.value++
203+
return () => {
204+
removeRoute()
205+
routesVersion.value++
206+
}
199207
}
200208

201209
function removeRoute(name: NonNullable<RouteRecordNameGeneric>) {
202210
const recordMatcher = matcher.getRecordMatcher(name)
203211
if (recordMatcher) {
204212
matcher.removeRoute(recordMatcher)
213+
routesVersion.value++
205214
} else if (__DEV__) {
206215
diagnostics.VUE_ROUTER_R0002({ name: String(name) })
207216
}
208217
}
209218

219+
function clearRoutes() {
220+
matcher.clearRoutes()
221+
routesVersion.value++
222+
}
223+
210224
function getRoutes() {
211225
return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
212226
}
@@ -221,9 +235,17 @@ export function createRouter(options: RouterOptions): Router {
221235
): RouteLocationResolved {
222236
// const resolve: Router['resolve'] = (rawLocation: RouteLocationRaw, currentLocation) => {
223237
// const objectLocation = routerLocationAsObject(rawLocation)
224-
// we create a copy to modify it later
225-
currentLocation = assign({}, currentLocation || currentRoute.value)
238+
// depend on the route table so `computed()`s using `resolve()` are
239+
// invalidated when routes are added or removed
240+
routesVersion.value
226241
if (typeof rawLocation === 'string') {
242+
// absolute locations do not depend on where the user currently is, so
243+
// we avoid reading `currentRoute` to not track it as a dependency
244+
currentLocation =
245+
currentLocation ||
246+
(rawLocation.startsWith('/')
247+
? START_LOCATION_NORMALIZED
248+
: currentRoute.value)
227249
const locationNormalized = parseURL(
228250
parseQuery,
229251
rawLocation,
@@ -257,6 +279,17 @@ export function createRouter(options: RouterOptions): Router {
257279
return resolve({})
258280
}
259281

282+
// we create a copy to modify it later
283+
currentLocation = assign(
284+
{},
285+
currentLocation ||
286+
(rawLocation.path != null &&
287+
rawLocation.path.startsWith('/') &&
288+
!('name' in rawLocation && rawLocation.name)
289+
? START_LOCATION_NORMALIZED
290+
: currentRoute.value)
291+
)
292+
260293
let matcherLocation: MatcherLocationRaw
261294

262295
// path could be relative in object as well
@@ -995,7 +1028,7 @@ export function createRouter(options: RouterOptions): Router {
9951028

9961029
addRoute,
9971030
removeRoute,
998-
clearRoutes: matcher.clearRoutes,
1031+
clearRoutes,
9991032
hasRoute,
10001033
getRoutes,
10011034
resolve,

0 commit comments

Comments
 (0)