diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap index 3d13c4066d9..6660865a523 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap @@ -8,7 +8,7 @@ return function render(_ctx, _cache) { const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue return _cache[0] || ( - _setBlockTracking(-1), + _setBlockTracking(-1, true), (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, _setBlockTracking(1), _cache[0] @@ -28,7 +28,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ _cache[0] || ( - _setBlockTracking(-1), + _setBlockTracking(-1, true), (_cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, _setBlockTracking(1), _cache[0] @@ -47,7 +47,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ _cache[0] || ( - _setBlockTracking(-1), + _setBlockTracking(-1, true), (_cache[0] = _createElementVNode("div", { id: foo }, null, 8 /* PROPS */, ["id"])).cacheIndex = 0, _setBlockTracking(1), _cache[0] @@ -66,7 +66,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ _cache[0] || ( - _setBlockTracking(-1), + _setBlockTracking(-1, true), (_cache[0] = _renderSlot($slots, "default")).cacheIndex = 0, _setBlockTracking(1), _cache[0] @@ -85,7 +85,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, [ _cache[0] || ( - _setBlockTracking(-1), + _setBlockTracking(-1, true), (_cache[0] = _createElementVNode("div")).cacheIndex = 0, _setBlockTracking(1), _cache[0] diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index cfd5fee2569..2d6df9d9010 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -418,6 +418,7 @@ export interface CacheExpression extends Node { index: number value: JSChildNode needPauseTracking: boolean + inVOnce: boolean needArraySpread: boolean } @@ -774,12 +775,14 @@ export function createCacheExpression( index: number, value: JSChildNode, needPauseTracking: boolean = false, + inVOnce: boolean = false, ): CacheExpression { return { type: NodeTypes.JS_CACHE_EXPRESSION, index, value, needPauseTracking: needPauseTracking, + inVOnce, needArraySpread: false, loc: locStub, } diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 6b6f24b3a30..70116cfb61a 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -1017,7 +1017,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) { push(`_cache[${node.index}] || (`) if (needPauseTracking) { indent() - push(`${helper(SET_BLOCK_TRACKING)}(-1),`) + push(`${helper(SET_BLOCK_TRACKING)}(-1`) + if (node.inVOnce) push(`, true`) + push(`),`) newline() push(`(`) } diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index b47b6b8d408..aeb96cc2b4a 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -116,7 +116,7 @@ export interface TransformContext addIdentifiers(exp: ExpressionNode | string): void removeIdentifiers(exp: ExpressionNode | string): void hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode - cache(exp: JSChildNode, isVNode?: boolean): CacheExpression + cache(exp: JSChildNode, isVNode?: boolean, inVOnce?: boolean): CacheExpression constantCache: WeakMap // 2.x Compat only @@ -297,11 +297,12 @@ export function createTransformContext( identifier.hoisted = exp return identifier }, - cache(exp, isVNode = false) { + cache(exp, isVNode = false, inVOnce = false) { const cacheExp = createCacheExpression( context.cached.length, exp, isVNode, + inVOnce, ) context.cached.push(cacheExp) return cacheExp diff --git a/packages/compiler-core/src/transforms/vOnce.ts b/packages/compiler-core/src/transforms/vOnce.ts index 483b98da961..685da59ccf0 100644 --- a/packages/compiler-core/src/transforms/vOnce.ts +++ b/packages/compiler-core/src/transforms/vOnce.ts @@ -17,7 +17,11 @@ export const transformOnce: NodeTransform = (node, context) => { context.inVOnce = false const cur = context.currentNode as ElementNode | IfNode | ForNode if (cur.codegenNode) { - cur.codegenNode = context.cache(cur.codegenNode, true /* isVNode */) + cur.codegenNode = context.cache( + cur.codegenNode, + true /* isVNode */, + true /* inVOnce */, + ) } } } diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts index 2658f40718f..958c1274806 100644 --- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts +++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts @@ -17,6 +17,7 @@ import { serializeInner as inner, nextTick, nodeOps, + onBeforeMount, onBeforeUnmount, onUnmounted, openBlock, @@ -1199,7 +1200,7 @@ describe('renderer: optimized mode', () => { createBlock('div', null, [ createVNode('div', null, [ cache[0] || - (setBlockTracking(-1), + (setBlockTracking(-1, true), ((cache[0] = createVNode('div', null, [ createVNode(Child), ])).cacheIndex = 0), @@ -1233,4 +1234,64 @@ describe('renderer: optimized mode', () => { expect(inner(root)).toBe('') expect(spyUnmounted).toHaveBeenCalledTimes(2) }) + + // #12371 + test('unmount children when the user calls a compiled slot', async () => { + const beforeMountSpy = vi.fn() + const beforeUnmountSpy = vi.fn() + + const Child = { + setup() { + onBeforeMount(beforeMountSpy) + onBeforeUnmount(beforeUnmountSpy) + return () => 'child' + }, + } + + const Wrapper = { + setup(_: any, { slots }: SetupContext) { + return () => ( + openBlock(), + createElementBlock('section', null, [ + (openBlock(), + createElementBlock('div', { key: 1 }, [ + createTextVNode(slots.header!() ? 'foo' : 'bar', 1 /* TEXT */), + renderSlot(slots, 'content'), + ])), + ]) + ) + }, + } + + const show = ref(false) + const app = createApp({ + render() { + return show.value + ? (openBlock(), + createBlock(Wrapper, null, { + header: withCtx(() => [createVNode({})]), + content: withCtx(() => [createVNode(Child)]), + _: 1, + })) + : createCommentVNode('v-if', true) + }, + }) + + app.mount(root) + expect(inner(root)).toMatchInlineSnapshot(`""`) + expect(beforeMountSpy).toHaveBeenCalledTimes(0) + expect(beforeUnmountSpy).toHaveBeenCalledTimes(0) + + show.value = true + await nextTick() + expect(inner(root)).toMatchInlineSnapshot( + `"
foochild
"`, + ) + expect(beforeMountSpy).toHaveBeenCalledTimes(1) + + show.value = false + await nextTick() + expect(inner(root)).toBe('') + expect(beforeUnmountSpy).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts index 2e0eee1f280..a7f6a2d5684 100644 --- a/packages/runtime-core/__tests__/vnode.spec.ts +++ b/packages/runtime-core/__tests__/vnode.spec.ts @@ -629,7 +629,7 @@ describe('vnode', () => { const vnode = (openBlock(), createBlock('div', null, [ - setBlockTracking(-1), + setBlockTracking(-1, true), (vnode1 = (openBlock(), createBlock('div'))), setBlockTracking(1), vnode1, diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 30200789be8..a8c5340cd1f 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -301,7 +301,7 @@ export let isBlockTreeEnabled = 1 * * ``` js * _cache[1] || ( - * setBlockTracking(-1), + * setBlockTracking(-1, true), * _cache[1] = createVNode(...), * setBlockTracking(1), * _cache[1] @@ -310,11 +310,11 @@ export let isBlockTreeEnabled = 1 * * @private */ -export function setBlockTracking(value: number): void { +export function setBlockTracking(value: number, inVOnce = false): void { isBlockTreeEnabled += value - if (value < 0 && currentBlock) { + if (value < 0 && currentBlock && inVOnce) { // mark current block so it doesn't take fast path and skip possible - // nested components duriung unmount + // nested components during unmount currentBlock.hasOnce = true } }