From 1191594e9a95cb855d31046ae0df0443c65cadbf Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 15 Dec 2023 14:27:24 +0100 Subject: [PATCH] perf(RouterView): avoid parent rerenders when possible Close #1701 --- packages/playground/src/App.vue | 110 +++++++----------- packages/playground/src/SimpleView.vue | 48 ++++++++ packages/playground/src/router.ts | 12 ++ .../playground/src/views/RerenderCheck.vue | 11 ++ packages/router/src/RouterView.ts | 39 ++++--- 5 files changed, 138 insertions(+), 82 deletions(-) create mode 100644 packages/playground/src/SimpleView.vue create mode 100644 packages/playground/src/views/RerenderCheck.vue diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index b686c0c54..b4a618e90 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -1,3 +1,28 @@ + + - - diff --git a/packages/playground/src/SimpleView.vue b/packages/playground/src/SimpleView.vue new file mode 100644 index 000000000..0275d952e --- /dev/null +++ b/packages/playground/src/SimpleView.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/playground/src/router.ts b/packages/playground/src/router.ts index 981b4192f..1935666a3 100644 --- a/packages/playground/src/router.ts +++ b/packages/playground/src/router.ts @@ -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() @@ -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() diff --git a/packages/playground/src/views/RerenderCheck.vue b/packages/playground/src/views/RerenderCheck.vue new file mode 100644 index 000000000..5c2668442 --- /dev/null +++ b/packages/playground/src/views/RerenderCheck.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/router/src/RouterView.ts b/packages/router/src/RouterView.ts index ad4d8f720..6e422be77 100644 --- a/packages/router/src/RouterView.ts +++ b/packages/router/src/RouterView.ts @@ -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] @@ -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 @@ -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, { @@ -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)