Skip to content

Commit 560def4

Browse files
authored
fix(runtime-core): invalidate detached v-for memo vnodes after unmount (#14624)
close #12708 close #12710
1 parent 5725222 commit 560def4

4 files changed

Lines changed: 80 additions & 5 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function render(_ctx, _cache) {
77
return (_openBlock(), _createElementBlock("div", null, [
88
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.tableData, (data, __, ___, _cached) => {
99
const _memo = (_ctx.getLetter(data))
10-
if (_cached && _cached.key === _ctx.getId(data) && _isMemoSame(_cached, _memo)) return _cached
10+
if (_cached && _cached.el && _cached.key === _ctx.getId(data) && _isMemoSame(_cached, _memo)) return _cached
1111
const _item = (_openBlock(), _createElementBlock("span", {
1212
key: _ctx.getId(data)
1313
}))
@@ -55,7 +55,7 @@ export function render(_ctx, _cache) {
5555
return (_openBlock(), _createElementBlock("div", null, [
5656
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cached) => {
5757
const _memo = ([x, y === _ctx.z])
58-
if (_cached && _cached.key === x && _isMemoSame(_cached, _memo)) return _cached
58+
if (_cached && _cached.el && _cached.key === x && _isMemoSame(_cached, _memo)) return _cached
5959
const _item = (_openBlock(), _createElementBlock("span", { key: x }, "foobar"))
6060
_item.memo = _memo
6161
return _item
@@ -71,7 +71,7 @@ export function render(_ctx, _cache) {
7171
return (_openBlock(), _createElementBlock("div", null, [
7272
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cached) => {
7373
const _memo = ([x, y === _ctx.z])
74-
if (_cached && _cached.key === x && _isMemoSame(_cached, _memo)) return _cached
74+
if (_cached && _cached.el && _cached.key === x && _isMemoSame(_cached, _memo)) return _cached
7575
const _item = (_openBlock(), _createElementBlock("div", { key: x }, [
7676
_createElementVNode("span", null, "foobar")
7777
]))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export const transformFor: NodeTransform = createStructuralDirectiveTransform(
221221
loop.body = createBlockStatement([
222222
createCompoundExpression([`const _memo = (`, memo.exp!, `)`]),
223223
createCompoundExpression([
224-
`if (_cached`,
224+
`if (_cached && _cached.el`,
225225
...(keyExp ? [` && _cached.key === `, keyExp] : []),
226226
` && ${context.helperString(
227227
IS_MEMO_SAME,

packages/runtime-core/__tests__/helpers/withMemo.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,71 @@ describe('v-memo', () => {
204204
)
205205
})
206206

207+
test('on v-if + v-for', async () => {
208+
const [el, vm] = mount({
209+
template: `<span v-if="show">
210+
<span v-for="elem in [1]" :key="elem" v-memo="[count]">{{ count }}</span>
211+
</span>`,
212+
data: () => ({
213+
show: true,
214+
count: 0,
215+
}),
216+
})
217+
218+
expect(el.innerHTML).toBe(`<span><span>0</span></span>`)
219+
220+
vm.show = false
221+
await nextTick()
222+
expect(el.innerHTML).toBe(`<!--v-if-->`)
223+
224+
vm.show = true
225+
await nextTick()
226+
expect(el.innerHTML).toBe(`<span><span>0</span></span>`)
227+
228+
vm.count++
229+
await nextTick()
230+
expect(el.innerHTML).toBe(`<span><span>1</span></span>`)
231+
232+
vm.count++
233+
await nextTick()
234+
expect(el.innerHTML).toBe(`<span><span>2</span></span>`)
235+
})
236+
237+
test('on v-if + v-for in production mode', async () => {
238+
__DEV__ = false
239+
try {
240+
const [el, vm] = mount({
241+
template: `<span v-if="show">
242+
<span v-for="elem in [1]" :key="elem" v-memo="[count]">{{ count }}</span>
243+
</span>`,
244+
data: () => ({
245+
show: true,
246+
count: 0,
247+
}),
248+
})
249+
250+
expect(el.innerHTML).toBe(`<span><span>0</span></span>`)
251+
252+
vm.show = false
253+
await nextTick()
254+
expect(el.innerHTML).toBe(`<!---->`)
255+
256+
vm.show = true
257+
await nextTick()
258+
expect(el.innerHTML).toBe(`<span><span>0</span></span>`)
259+
260+
vm.count++
261+
await nextTick()
262+
expect(el.innerHTML).toBe(`<span><span>1</span></span>`)
263+
264+
vm.count++
265+
await nextTick()
266+
expect(el.innerHTML).toBe(`<span><span>2</span></span>`)
267+
} finally {
268+
__DEV__ = true
269+
}
270+
})
271+
207272
test('on v-for /w constant expression ', async () => {
208273
const [el, vm] = mount({
209274
template: `<div v-for="item in 3" v-memo="[count < 2 ? true : count]">

packages/runtime-core/src/renderer.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2134,6 +2134,7 @@ function baseCreateRenderer(
21342134
patchFlag,
21352135
dirs,
21362136
cacheIndex,
2137+
memo,
21372138
} = vnode
21382139

21392140
if (patchFlag === PatchFlags.BAIL) {
@@ -2222,15 +2223,24 @@ function baseCreateRenderer(
22222223
}
22232224
}
22242225

2226+
// v-for + v-memo stores cached vnodes inside renderList's array cache rather
2227+
// than component renderCache. Invalidate detached cached vnodes after
2228+
// unmount so a later v-if remount won't reuse a vnode whose DOM is gone.
2229+
const shouldInvalidateMemo = memo != null && cacheIndex == null
2230+
22252231
if (
22262232
(shouldInvokeVnodeHook &&
22272233
(vnodeHook = props && props.onVnodeUnmounted)) ||
2228-
shouldInvokeDirs
2234+
shouldInvokeDirs ||
2235+
shouldInvalidateMemo
22292236
) {
22302237
queuePostRenderEffect(() => {
22312238
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
22322239
shouldInvokeDirs &&
22332240
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
2241+
if (shouldInvalidateMemo) {
2242+
vnode.el = null
2243+
}
22342244
}, parentSuspense)
22352245
}
22362246
}

0 commit comments

Comments
 (0)