Skip to content

Commit 00695a5

Browse files
authored
fix(compiler-core): avoid cached text vnodes retaining detached DOM nodes (#13662)
close #13661
1 parent da1f8d7 commit 00695a5

File tree

3 files changed

+102
-2
lines changed

3 files changed

+102
-2
lines changed

packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ return function render(_ctx, _cache) {
6060
6161
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
6262
_createElementVNode("span", null, null, -1 /* CACHED */),
63-
_createTextVNode("foo"),
63+
_createTextVNode("foo", -1 /* CACHED */),
6464
_createElementVNode("div", null, null, -1 /* CACHED */)
6565
])))
6666
}

packages/compiler-core/src/transforms/cacheStatic.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ import {
2424
getVNodeHelper,
2525
} from '../ast'
2626
import type { TransformContext } from '../transform'
27-
import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared'
27+
import {
28+
PatchFlagNames,
29+
PatchFlags,
30+
isArray,
31+
isString,
32+
isSymbol,
33+
} from '@vue/shared'
2834
import { findDir, isSlotOutlet } from '../utils'
2935
import {
3036
GUARD_REACTIVE_PROPS,
@@ -109,6 +115,15 @@ function walk(
109115
? ConstantTypes.NOT_CONSTANT
110116
: getConstantType(child, context)
111117
if (constantType >= ConstantTypes.CAN_CACHE) {
118+
if (
119+
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
120+
child.codegenNode.arguments.length > 0
121+
) {
122+
child.codegenNode.arguments.push(
123+
PatchFlags.CACHED +
124+
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
125+
)
126+
}
112127
toCache.push(child)
113128
continue
114129
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
2+
import path from 'node:path'
3+
4+
const { page, html, click } = setupPuppeteer()
5+
6+
beforeEach(async () => {
7+
await page().setContent(`<div id="app"></div>`)
8+
await page().addScriptTag({
9+
path: path.resolve(__dirname, '../../dist/vue.global.js'),
10+
})
11+
})
12+
13+
describe('not leaking', async () => {
14+
// #13661
15+
test(
16+
'cached text vnodes should not retaining detached DOM nodes',
17+
async () => {
18+
const client = await page().createCDPSession()
19+
await page().evaluate(async () => {
20+
const { createApp, ref } = (window as any).Vue
21+
createApp({
22+
components: {
23+
Comp1: {
24+
template: `
25+
<h1><slot></slot></h1>
26+
<div>{{ test.length }}</div>
27+
`,
28+
setup() {
29+
const test = ref([...Array(3000)].map((_, i) => ({ i })))
30+
// @ts-expect-error
31+
window.__REF__ = new WeakRef(test)
32+
33+
return { test }
34+
},
35+
},
36+
Comp2: {
37+
template: `<h2>comp2</h2>`,
38+
},
39+
},
40+
template: `
41+
<button id="toggleBtn" @click="click">button</button>
42+
<Comp1 v-if="toggle">
43+
<div>
44+
<Comp2/>
45+
text node
46+
</div>
47+
</Comp1>
48+
`,
49+
setup() {
50+
const toggle = ref(true)
51+
const click = () => (toggle.value = !toggle.value)
52+
return { toggle, click }
53+
},
54+
}).mount('#app')
55+
})
56+
57+
expect(await html('#app')).toBe(
58+
`<button id="toggleBtn">button</button>` +
59+
`<h1>` +
60+
`<div>` +
61+
`<h2>comp2</h2>` +
62+
` text node ` +
63+
`</div>` +
64+
`</h1>` +
65+
`<div>3000</div>`,
66+
)
67+
68+
await click('#toggleBtn')
69+
expect(await html('#app')).toBe(
70+
`<button id="toggleBtn">button</button><!--v-if-->`,
71+
)
72+
73+
const isCollected = async () =>
74+
// @ts-expect-error
75+
await page().evaluate(() => window.__REF__.deref() === undefined)
76+
77+
while ((await isCollected()) === false) {
78+
await client.send('HeapProfiler.collectGarbage')
79+
}
80+
81+
expect(await isCollected()).toBe(true)
82+
},
83+
E2E_TIMEOUT,
84+
)
85+
})

0 commit comments

Comments
 (0)