Skip to content

Commit

Permalink
perf(RouterView): avoid parent rerenders when possible
Browse files Browse the repository at this point in the history
Close #1701
  • Loading branch information
posva committed Dec 15, 2023
1 parent 1bbf2a9 commit 1191594
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 82 deletions.
110 changes: 41 additions & 69 deletions packages/playground/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
<script lang="ts" setup>
import { inject, computed, ref } from 'vue'
import { useLink, useRoute } from 'vue-router'
import AppLink from './AppLink.vue'
import SimpleView from './SimpleView.vue'
const route = useRoute()
const state = inject('state')
useLink({ to: '/' })
useLink({ to: '/documents/hello' })
useLink({ to: '/children' })
const currentLocation = computed(() => {
const { matched, ...rest } = route
return rest
})
const nextUserLink = computed(
() => '/users/' + String((Number(route.params.id) || 0) + 1)
)
const simple = ref(false)
</script>

<template>
<div>
<pre>{{ currentLocation }}</pre>
Expand Down Expand Up @@ -37,6 +62,11 @@
<input type="checkbox" v-model="state.cancelNextNavigation" /> Cancel Next
Navigation
</label>

<label>
<input type="checkbox" v-model="simple" /> Use Simple RouterView
</label>

<ul>
<li>
<router-link to="/n/%E2%82%AC">/n/%E2%82%AC</router-link>
Expand Down Expand Up @@ -158,76 +188,18 @@
<li>
<router-link to="/p_1/absolute-a">/p_1/absolute-a</router-link>
</li>
<li>
<RouterLink to="/rerender" v-slot="{ href }">{{ href }}</RouterLink>
</li>
<li>
<RouterLink to="/rerender/a" v-slot="{ href }">{{ href }}</RouterLink>
</li>
<li>
<RouterLink to="/rerender/b" v-slot="{ href }">{{ href }}</RouterLink>
</li>
</ul>
<button @click="toggleViewName">Toggle view</button>
<RouterView :name="viewName" v-slot="{ Component, route }">
<Transition
:name="route.meta.transition || 'fade'"
mode="out-in"
@before-enter="flushWaiter"
@before-leave="setupWaiter"
>
<!-- <KeepAlive> -->
<Suspense>
<template #default>
<component
:is="Component"
:key="route.name === 'repeat' ? route.path : route.meta.key"
/>
</template>
<template #fallback> Loading... </template>
</Suspense>
<!-- </KeepAlive> -->
</Transition>
</RouterView>

<SimpleView :simple="simple"></SimpleView>
</div>
</template>

<script lang="ts">
import { defineComponent, inject, computed, ref } from 'vue'
import { scrollWaiter } from './scrollWaiter'
import { useLink, useRoute } from 'vue-router'
import AppLink from './AppLink.vue'
export default defineComponent({
name: 'App',
components: { AppLink },
setup() {
const route = useRoute()
const state = inject('state')
const viewName = ref('default')
useLink({ to: '/' })
useLink({ to: '/documents/hello' })
useLink({ to: '/children' })
const currentLocation = computed(() => {
const { matched, ...rest } = route
return rest
})
function flushWaiter() {
scrollWaiter.flush()
}
function setupWaiter() {
scrollWaiter.add()
}
const nextUserLink = computed(
() => '/users/' + String((Number(route.params.id) || 0) + 1)
)
return {
currentLocation,
nextUserLink,
state,
flushWaiter,
setupWaiter,
viewName,
toggleViewName() {
viewName.value = viewName.value === 'default' ? 'other' : 'default'
},
}
},
})
</script>
48 changes: 48 additions & 0 deletions packages/playground/src/SimpleView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { scrollWaiter } from './scrollWaiter'
defineProps<{ simple: boolean }>()
const viewName = ref('default')
function flushWaiter() {
scrollWaiter.flush()
}
function setupWaiter() {
scrollWaiter.add()
}
</script>

<template>
<RouterView v-if="simple" v-slot="{ Component, route }">
<component :is="Component" :key="route.meta.key" />
</RouterView>

<RouterView
v-else
:name="viewName"
v-slot="{ Component, route }"
key="not-simple"
>
<Transition
:name="route.meta.transition || 'fade'"
mode="out-in"
@before-enter="flushWaiter"
@before-leave="setupWaiter"
>
<!-- <KeepAlive> -->
<!-- <Suspense>
<template #default> -->
<!-- <div v-if="route.path.endsWith('/a')">A</div>
<div v-else>B</div> -->
<component
:is="Component"
:key="route.name === 'repeat' ? route.path : route.meta.key"
/>
<!-- </template>
<template #fallback> Loading... </template>
</Suspense> -->
<!-- </KeepAlive> -->
</Transition>
</RouterView>
</template>
12 changes: 12 additions & 0 deletions packages/playground/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import ComponentWithData from './views/ComponentWithData.vue'
import { globalState } from './store'
import { scrollWaiter } from './scrollWaiter'
import RepeatedParams from './views/RepeatedParams.vue'
import RerenderCheck from './views/RerenderCheck.vue'
import { h } from 'vue'

let removeRoute: (() => void) | undefined

export const routerHistory = createWebHistory()
Expand Down Expand Up @@ -159,6 +162,15 @@ export const router = createRouter({
{ path: 'settings', component },
],
},

{
path: '/rerender',
component: RerenderCheck,
children: [
{ path: 'a', component: { render: () => h('div', 'Child A') } },
{ path: 'b', component: { render: () => h('div', 'Child B') } },
],
},
],
async scrollBehavior(to, from, savedPosition) {
await scrollWaiter.wait()
Expand Down
11 changes: 11 additions & 0 deletions packages/playground/src/views/RerenderCheck.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { onUpdated } from 'vue'
let count = 0
onUpdated(() => {
console.log(`RerenderCheck.vue render: ${++count}`)
})
</script>

<template>
<RouterView key="fixed" />
</template>
39 changes: 26 additions & 13 deletions packages/router/src/RouterView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,30 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
{ flush: 'post' }
)

let matchedRoute: RouteLocationMatched | undefined
let currentName: string
// Since in Vue the entering view mounts first and then the leaving unmounts,
// we need to keep track of the last route in order to use it in the unmounted
// event
let lastMatchedRoute: RouteLocationMatched | undefined
let lastCurrentName: string

const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
// remove the instance reference to prevent leak
if (lastMatchedRoute && vnode.component!.isUnmounted) {
lastMatchedRoute.instances[lastCurrentName] = null
}
}

return () => {
const route = routeToDisplay.value
lastMatchedRoute = matchedRoute
lastCurrentName = currentName
// we need the value at the time we render because when we unmount, we
// navigated to a different location so the value is different
const currentName = props.name
const matchedRoute = matchedRouteRef.value
currentName = props.name
matchedRoute = matchedRouteRef.value

const ViewComponent =
matchedRoute && matchedRoute.components![currentName]

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

// props from route configuration
const routePropsOption = matchedRoute.props[currentName]
// matchedRoute exists since we check with if (ViewComponent)
const routePropsOption = matchedRoute!.props[currentName]
const routeProps = routePropsOption
? routePropsOption === true
? route.params
Expand All @@ -158,13 +177,6 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
: routePropsOption
: null

const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
// remove the instance reference to prevent leak
if (vnode.component!.isUnmounted) {
matchedRoute.instances[currentName] = null
}
}

const component = h(
ViewComponent,
assign({}, routeProps, attrs, {
Expand All @@ -181,9 +193,10 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
// TODO: can display if it's an alias, its props
const info: RouterViewDevtoolsContext = {
depth: depth.value,
name: matchedRoute.name,
path: matchedRoute.path,
meta: matchedRoute.meta,
// same as above: ensured with if (ViewComponent) above
name: matchedRoute!.name,
path: matchedRoute!.path,
meta: matchedRoute!.meta,
}

const internalInstances = isArray(component.ref)
Expand Down

0 comments on commit 1191594

Please sign in to comment.