mirror of https://github.com/vuejs/core.git
fix(compiler-core): avoid cached text vnodes retaining detached DOM nodes (#13662)
close #13661
This commit is contained in:
parent
da1f8d7987
commit
00695a5b41
|
@ -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 */)
|
||||
])))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(`<div id="app"></div>`)
|
||||
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: `
|
||||
<h1><slot></slot></h1>
|
||||
<div>{{ test.length }}</div>
|
||||
`,
|
||||
setup() {
|
||||
const test = ref([...Array(3000)].map((_, i) => ({ i })))
|
||||
// @ts-expect-error
|
||||
window.__REF__ = new WeakRef(test)
|
||||
|
||||
return { test }
|
||||
},
|
||||
},
|
||||
Comp2: {
|
||||
template: `<h2>comp2</h2>`,
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<button id="toggleBtn" @click="click">button</button>
|
||||
<Comp1 v-if="toggle">
|
||||
<div>
|
||||
<Comp2/>
|
||||
text node
|
||||
</div>
|
||||
</Comp1>
|
||||
`,
|
||||
setup() {
|
||||
const toggle = ref(true)
|
||||
const click = () => (toggle.value = !toggle.value)
|
||||
return { toggle, click }
|
||||
},
|
||||
}).mount('#app')
|
||||
})
|
||||
|
||||
expect(await html('#app')).toBe(
|
||||
`<button id="toggleBtn">button</button>` +
|
||||
`<h1>` +
|
||||
`<div>` +
|
||||
`<h2>comp2</h2>` +
|
||||
` text node ` +
|
||||
`</div>` +
|
||||
`</h1>` +
|
||||
`<div>3000</div>`,
|
||||
)
|
||||
|
||||
await click('#toggleBtn')
|
||||
expect(await html('#app')).toBe(
|
||||
`<button id="toggleBtn">button</button><!--v-if-->`,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
})
|
Loading…
Reference in New Issue