From: edison Date: Wed, 23 Jul 2025 00:36:15 +0000 (+0800) Subject: fix(compiler-core): avoid cached text vnodes retaining detached DOM nodes (#13662) X-Git-Tag: v3.5.18~9 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=00695a5b41b2d032deaeada83831ff83aa6bfd4e;p=thirdparty%2Fvuejs%2Fcore.git fix(compiler-core): avoid cached text vnodes retaining detached DOM nodes (#13662) close #13661 --- diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap index b8bef22c47..91a82db5bb 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap @@ -60,7 +60,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ _createElementVNode("span", null, null, -1 /* CACHED */), - _createTextVNode("foo"), + _createTextVNode("foo", -1 /* CACHED */), _createElementVNode("div", null, null, -1 /* CACHED */) ]))) } diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts index 239ee689a9..0f112e19ca 100644 --- a/packages/compiler-core/src/transforms/cacheStatic.ts +++ b/packages/compiler-core/src/transforms/cacheStatic.ts @@ -24,7 +24,13 @@ import { getVNodeHelper, } from '../ast' import type { TransformContext } from '../transform' -import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared' +import { + PatchFlagNames, + PatchFlags, + isArray, + isString, + isSymbol, +} from '@vue/shared' import { findDir, isSlotOutlet } from '../utils' import { GUARD_REACTIVE_PROPS, @@ -109,6 +115,15 @@ function walk( ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context) if (constantType >= ConstantTypes.CAN_CACHE) { + if ( + child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION && + child.codegenNode.arguments.length > 0 + ) { + child.codegenNode.arguments.push( + PatchFlags.CACHED + + (__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``), + ) + } toCache.push(child) continue } diff --git a/packages/vue/__tests__/e2e/memory-leak.spec.ts b/packages/vue/__tests__/e2e/memory-leak.spec.ts new file mode 100644 index 0000000000..2412cea2b0 --- /dev/null +++ b/packages/vue/__tests__/e2e/memory-leak.spec.ts @@ -0,0 +1,85 @@ +import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' +import path from 'node:path' + +const { page, html, click } = setupPuppeteer() + +beforeEach(async () => { + await page().setContent(`
`) + await page().addScriptTag({ + path: path.resolve(__dirname, '../../dist/vue.global.js'), + }) +}) + +describe('not leaking', async () => { + // #13661 + test( + 'cached text vnodes should not retaining detached DOM nodes', + async () => { + const client = await page().createCDPSession() + await page().evaluate(async () => { + const { createApp, ref } = (window as any).Vue + createApp({ + components: { + Comp1: { + template: ` +

+
{{ test.length }}
+ `, + setup() { + const test = ref([...Array(3000)].map((_, i) => ({ i }))) + // @ts-expect-error + window.__REF__ = new WeakRef(test) + + return { test } + }, + }, + Comp2: { + template: `

comp2

`, + }, + }, + template: ` + + +
+ + text node +
+
+ `, + setup() { + const toggle = ref(true) + const click = () => (toggle.value = !toggle.value) + return { toggle, click } + }, + }).mount('#app') + }) + + expect(await html('#app')).toBe( + `` + + `

` + + `
` + + `

comp2

` + + ` text node ` + + `
` + + `

` + + `
3000
`, + ) + + await click('#toggleBtn') + expect(await html('#app')).toBe( + ``, + ) + + const isCollected = async () => + // @ts-expect-error + await page().evaluate(() => window.__REF__.deref() === undefined) + + while ((await isCollected()) === false) { + await client.send('HeapProfiler.collectGarbage') + } + + expect(await isCollected()).toBe(true) + }, + E2E_TIMEOUT, + ) +})