Skip to content

Commit 1191594

Browse files
committed
perf(RouterView): avoid parent rerenders when possible
Close #1701
1 parent 1bbf2a9 commit 1191594

File tree

5 files changed

+138
-82
lines changed

5 files changed

+138
-82
lines changed

packages/playground/src/App.vue

+41-69
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
<script lang="ts" setup>
2+
import { inject, computed, ref } from 'vue'
3+
import { useLink, useRoute } from 'vue-router'
4+
import AppLink from './AppLink.vue'
5+
import SimpleView from './SimpleView.vue'
6+
7+
const route = useRoute()
8+
const state = inject('state')
9+
10+
useLink({ to: '/' })
11+
useLink({ to: '/documents/hello' })
12+
useLink({ to: '/children' })
13+
14+
const currentLocation = computed(() => {
15+
const { matched, ...rest } = route
16+
return rest
17+
})
18+
19+
const nextUserLink = computed(
20+
() => '/users/' + String((Number(route.params.id) || 0) + 1)
21+
)
22+
23+
const simple = ref(false)
24+
</script>
25+
126
<template>
227
<div>
328
<pre>{{ currentLocation }}</pre>
@@ -37,6 +62,11 @@
3762
<input type="checkbox" v-model="state.cancelNextNavigation" /> Cancel Next
3863
Navigation
3964
</label>
65+
66+
<label>
67+
<input type="checkbox" v-model="simple" /> Use Simple RouterView
68+
</label>
69+
4070
<ul>
4171
<li>
4272
<router-link to="/n/%E2%82%AC">/n/%E2%82%AC</router-link>
@@ -158,76 +188,18 @@
158188
<li>
159189
<router-link to="/p_1/absolute-a">/p_1/absolute-a</router-link>
160190
</li>
191+
<li>
192+
<RouterLink to="/rerender" v-slot="{ href }">{{ href }}</RouterLink>
193+
</li>
194+
<li>
195+
<RouterLink to="/rerender/a" v-slot="{ href }">{{ href }}</RouterLink>
196+
</li>
197+
<li>
198+
<RouterLink to="/rerender/b" v-slot="{ href }">{{ href }}</RouterLink>
199+
</li>
161200
</ul>
162201
<button @click="toggleViewName">Toggle view</button>
163-
<RouterView :name="viewName" v-slot="{ Component, route }">
164-
<Transition
165-
:name="route.meta.transition || 'fade'"
166-
mode="out-in"
167-
@before-enter="flushWaiter"
168-
@before-leave="setupWaiter"
169-
>
170-
<!-- <KeepAlive> -->
171-
<Suspense>
172-
<template #default>
173-
<component
174-
:is="Component"
175-
:key="route.name === 'repeat' ? route.path : route.meta.key"
176-
/>
177-
</template>
178-
<template #fallback> Loading... </template>
179-
</Suspense>
180-
<!-- </KeepAlive> -->
181-
</Transition>
182-
</RouterView>
202+
203+
<SimpleView :simple="simple"></SimpleView>
183204
</div>
184205
</template>
185-
186-
<script lang="ts">
187-
import { defineComponent, inject, computed, ref } from 'vue'
188-
import { scrollWaiter } from './scrollWaiter'
189-
import { useLink, useRoute } from 'vue-router'
190-
import AppLink from './AppLink.vue'
191-
192-
export default defineComponent({
193-
name: 'App',
194-
components: { AppLink },
195-
setup() {
196-
const route = useRoute()
197-
const state = inject('state')
198-
const viewName = ref('default')
199-
200-
useLink({ to: '/' })
201-
useLink({ to: '/documents/hello' })
202-
useLink({ to: '/children' })
203-
204-
const currentLocation = computed(() => {
205-
const { matched, ...rest } = route
206-
return rest
207-
})
208-
209-
function flushWaiter() {
210-
scrollWaiter.flush()
211-
}
212-
function setupWaiter() {
213-
scrollWaiter.add()
214-
}
215-
216-
const nextUserLink = computed(
217-
() => '/users/' + String((Number(route.params.id) || 0) + 1)
218-
)
219-
220-
return {
221-
currentLocation,
222-
nextUserLink,
223-
state,
224-
flushWaiter,
225-
setupWaiter,
226-
viewName,
227-
toggleViewName() {
228-
viewName.value = viewName.value === 'default' ? 'other' : 'default'
229-
},
230-
}
231-
},
232-
})
233-
</script>
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<script lang="ts" setup>
2+
import { ref } from 'vue'
3+
import { scrollWaiter } from './scrollWaiter'
4+
5+
defineProps<{ simple: boolean }>()
6+
const viewName = ref('default')
7+
8+
function flushWaiter() {
9+
scrollWaiter.flush()
10+
}
11+
function setupWaiter() {
12+
scrollWaiter.add()
13+
}
14+
</script>
15+
16+
<template>
17+
<RouterView v-if="simple" v-slot="{ Component, route }">
18+
<component :is="Component" :key="route.meta.key" />
19+
</RouterView>
20+
21+
<RouterView
22+
v-else
23+
:name="viewName"
24+
v-slot="{ Component, route }"
25+
key="not-simple"
26+
>
27+
<Transition
28+
:name="route.meta.transition || 'fade'"
29+
mode="out-in"
30+
@before-enter="flushWaiter"
31+
@before-leave="setupWaiter"
32+
>
33+
<!-- <KeepAlive> -->
34+
<!-- <Suspense>
35+
<template #default> -->
36+
<!-- <div v-if="route.path.endsWith('/a')">A</div>
37+
<div v-else>B</div> -->
38+
<component
39+
:is="Component"
40+
:key="route.name === 'repeat' ? route.path : route.meta.key"
41+
/>
42+
<!-- </template>
43+
<template #fallback> Loading... </template>
44+
</Suspense> -->
45+
<!-- </KeepAlive> -->
46+
</Transition>
47+
</RouterView>
48+
</template>

packages/playground/src/router.ts

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import ComponentWithData from './views/ComponentWithData.vue'
1515
import { globalState } from './store'
1616
import { scrollWaiter } from './scrollWaiter'
1717
import RepeatedParams from './views/RepeatedParams.vue'
18+
import RerenderCheck from './views/RerenderCheck.vue'
19+
import { h } from 'vue'
20+
1821
let removeRoute: (() => void) | undefined
1922

2023
export const routerHistory = createWebHistory()
@@ -159,6 +162,15 @@ export const router = createRouter({
159162
{ path: 'settings', component },
160163
],
161164
},
165+
166+
{
167+
path: '/rerender',
168+
component: RerenderCheck,
169+
children: [
170+
{ path: 'a', component: { render: () => h('div', 'Child A') } },
171+
{ path: 'b', component: { render: () => h('div', 'Child B') } },
172+
],
173+
},
162174
],
163175
async scrollBehavior(to, from, savedPosition) {
164176
await scrollWaiter.wait()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts" setup>
2+
import { onUpdated } from 'vue'
3+
let count = 0
4+
onUpdated(() => {
5+
console.log(`RerenderCheck.vue render: ${++count}`)
6+
})
7+
</script>
8+
9+
<template>
10+
<RouterView key="fixed" />
11+
</template>

packages/router/src/RouterView.ts

+26-13
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,30 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
135135
{ flush: 'post' }
136136
)
137137

138+
let matchedRoute: RouteLocationMatched | undefined
139+
let currentName: string
140+
// Since in Vue the entering view mounts first and then the leaving unmounts,
141+
// we need to keep track of the last route in order to use it in the unmounted
142+
// event
143+
let lastMatchedRoute: RouteLocationMatched | undefined
144+
let lastCurrentName: string
145+
146+
const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
147+
// remove the instance reference to prevent leak
148+
if (lastMatchedRoute && vnode.component!.isUnmounted) {
149+
lastMatchedRoute.instances[lastCurrentName] = null
150+
}
151+
}
152+
138153
return () => {
139154
const route = routeToDisplay.value
155+
lastMatchedRoute = matchedRoute
156+
lastCurrentName = currentName
140157
// we need the value at the time we render because when we unmount, we
141158
// navigated to a different location so the value is different
142-
const currentName = props.name
143-
const matchedRoute = matchedRouteRef.value
159+
currentName = props.name
160+
matchedRoute = matchedRouteRef.value
161+
144162
const ViewComponent =
145163
matchedRoute && matchedRoute.components![currentName]
146164

@@ -149,7 +167,8 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
149167
}
150168

151169
// props from route configuration
152-
const routePropsOption = matchedRoute.props[currentName]
170+
// matchedRoute exists since we check with if (ViewComponent)
171+
const routePropsOption = matchedRoute!.props[currentName]
153172
const routeProps = routePropsOption
154173
? routePropsOption === true
155174
? route.params
@@ -158,13 +177,6 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
158177
: routePropsOption
159178
: null
160179

161-
const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
162-
// remove the instance reference to prevent leak
163-
if (vnode.component!.isUnmounted) {
164-
matchedRoute.instances[currentName] = null
165-
}
166-
}
167-
168180
const component = h(
169181
ViewComponent,
170182
assign({}, routeProps, attrs, {
@@ -181,9 +193,10 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
181193
// TODO: can display if it's an alias, its props
182194
const info: RouterViewDevtoolsContext = {
183195
depth: depth.value,
184-
name: matchedRoute.name,
185-
path: matchedRoute.path,
186-
meta: matchedRoute.meta,
196+
// same as above: ensured with if (ViewComponent) above
197+
name: matchedRoute!.name,
198+
path: matchedRoute!.path,
199+
meta: matchedRoute!.meta,
187200
}
188201

189202
const internalInstances = isArray(component.ref)

0 commit comments

Comments
 (0)