Skip to content

Commit f89a842

Browse files
committed
perf: parseURL minor improvements
1 parent 3b893cf commit f89a842

File tree

4 files changed

+93
-25
lines changed

4 files changed

+93
-25
lines changed

packages/router/__tests__/location.spec.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ describe('parseURL', () => {
134134
})
135135
})
136136

137-
it('parses ? after the hash', () => {
137+
it('avoids ? after the hash', () => {
138138
expect(parseURL('/foo#?a=one')).toEqual({
139139
fullPath: '/foo#?a=one',
140140
path: '/foo',
@@ -149,11 +149,75 @@ describe('parseURL', () => {
149149
})
150150
})
151151

152+
it('works with empty query', () => {
153+
expect(parseURL('/foo?#hash')).toEqual({
154+
fullPath: '/foo?#hash',
155+
path: '/foo',
156+
hash: '#hash',
157+
query: {},
158+
})
159+
expect(parseURL('/foo?')).toEqual({
160+
fullPath: '/foo?',
161+
path: '/foo',
162+
hash: '',
163+
query: {},
164+
})
165+
})
166+
167+
it('works with empty hash', () => {
168+
expect(parseURL('/foo#')).toEqual({
169+
fullPath: '/foo#',
170+
path: '/foo',
171+
hash: '#',
172+
query: {},
173+
})
174+
expect(parseURL('/foo?#')).toEqual({
175+
fullPath: '/foo?#',
176+
path: '/foo',
177+
hash: '#',
178+
query: {},
179+
})
180+
})
181+
182+
it('works with a relative paths', () => {
183+
expect(parseURL('foo', '/parent/bar')).toEqual({
184+
fullPath: '/parent/foo',
185+
path: '/parent/foo',
186+
hash: '',
187+
query: {},
188+
})
189+
expect(parseURL('./foo', '/parent/bar')).toEqual({
190+
fullPath: '/parent/foo',
191+
path: '/parent/foo',
192+
hash: '',
193+
query: {},
194+
})
195+
expect(parseURL('../foo', '/parent/bar')).toEqual({
196+
fullPath: '/foo',
197+
path: '/foo',
198+
hash: '',
199+
query: {},
200+
})
201+
202+
expect(parseURL('#foo', '/parent/bar')).toEqual({
203+
fullPath: '/parent/bar#foo',
204+
path: '/parent/bar',
205+
hash: '#foo',
206+
query: {},
207+
})
208+
expect(parseURL('?o=o', '/parent/bar')).toEqual({
209+
fullPath: '/parent/bar?o=o',
210+
path: '/parent/bar',
211+
hash: '',
212+
query: { o: 'o' },
213+
})
214+
})
215+
152216
it('calls parseQuery', () => {
153217
const parseQuery = vi.fn()
154218
originalParseURL(parseQuery, '/?é=é&é=a')
155219
expect(parseQuery).toHaveBeenCalledTimes(1)
156-
expect(parseQuery).toHaveBeenCalledWith('é=é&é=a')
220+
expect(parseQuery).toHaveBeenCalledWith('?é=é&é=a')
157221
})
158222
})
159223

packages/router/__tests__/router.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import { START_LOCATION_NORMALIZED } from '../src/location'
1414
import { vi, describe, expect, it, beforeAll } from 'vitest'
1515
import { mockWarn } from './vitest-mock-warn'
1616

17-
declare var __DEV__: boolean
18-
1917
const routes: RouteRecordRaw[] = [
2018
{ path: '/', component: components.Home, name: 'home' },
2119
{ path: '/home', redirect: '/' },
@@ -173,7 +171,7 @@ describe('Router', () => {
173171
const parseQuery = vi.fn(_ => ({}))
174172
const { router } = await newRouter({ parseQuery })
175173
const to = router.resolve('/foo?bar=baz')
176-
expect(parseQuery).toHaveBeenCalledWith('bar=baz')
174+
expect(parseQuery).toHaveBeenCalledWith('?bar=baz')
177175
expect(to.query).toEqual({})
178176
})
179177

packages/router/src/location.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,37 +50,43 @@ export function parseURL(
5050
searchString = '',
5151
hash = ''
5252

53-
// Could use URL and URLSearchParams but IE 11 doesn't support it
54-
// TODO: move to new URL()
53+
// NOTE: we could use URL and URLSearchParams but they are 2 to 5 times slower than this method
5554
const hashPos = location.indexOf('#')
56-
let searchPos = location.indexOf('?')
57-
// the hash appears before the search, so it's not part of the search string
58-
if (hashPos < searchPos && hashPos >= 0) {
59-
searchPos = -1
60-
}
55+
// let searchPos = location.indexOf('?')
56+
let searchPos =
57+
hashPos >= 0
58+
? // find the query string before the hash to avoid including a ? in the hash
59+
// e.g. /foo#hash?query -> has no query
60+
location.lastIndexOf('?', hashPos)
61+
: location.indexOf('?')
6162

62-
if (searchPos > -1) {
63+
if (searchPos >= 0) {
6364
path = location.slice(0, searchPos)
64-
searchString = location.slice(
65-
searchPos + 1,
66-
hashPos > -1 ? hashPos : location.length
67-
)
65+
searchString =
66+
'?' +
67+
location.slice(searchPos + 1, hashPos > 0 ? hashPos : location.length)
6868

6969
query = parseQuery(searchString)
7070
}
7171

72-
if (hashPos > -1) {
72+
if (hashPos >= 0) {
73+
// TODO(major): path ||=
7374
path = path || location.slice(0, hashPos)
7475
// keep the # character
7576
hash = location.slice(hashPos, location.length)
7677
}
7778

78-
// no search and no query
79-
path = resolveRelativePath(path != null ? path : location, currentLocation)
80-
// empty path means a relative query or hash `?foo=f`, `#thing`
79+
// TODO(major): path ?? location
80+
path = resolveRelativePath(
81+
path != null
82+
? path
83+
: // empty path means a relative query or hash `?foo=f`, `#thing`
84+
location,
85+
currentLocation
86+
)
8187

8288
return {
83-
fullPath: path + (searchString && '?') + searchString + hash,
89+
fullPath: path + searchString + hash,
8490
path,
8591
query,
8692
hash: decode(hash),
@@ -207,11 +213,12 @@ export function resolveRelativePath(to: string, from: string): string {
207213
return to
208214
}
209215

216+
// resolve '' with '/anything' -> '/anything'
210217
if (!to) return from
211218

212219
const fromSegments = from.split('/')
213220
const toSegments = to.split('/')
214-
const lastToSegment = toSegments[toSegments.length - 1]
221+
const lastToSegment: string | undefined = toSegments[toSegments.length - 1]
215222

216223
// make . and ./ the same (../ === .., ../../ === ../..)
217224
// this is the same behavior as new URL()

packages/router/src/query.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ export function parseQuery(search: string): LocationQuery {
5656
// avoid creating an object with an empty key and empty value
5757
// because of split('&')
5858
if (search === '' || search === '?') return query
59-
const hasLeadingIM = search[0] === '?'
60-
const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&')
59+
const searchParams = (search[0] === '?' ? search.slice(1) : search).split('&')
6160
for (let i = 0; i < searchParams.length; ++i) {
6261
// pre decode the + into space
6362
const searchParam = searchParams[i].replace(PLUS_RE, ' ')

0 commit comments

Comments
 (0)