Skip to content

fix(compiler-core): prevent cached array children from retaining detached dom nodes #13691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */)
])))
]))]))
}
}"
`;
Expand All @@ -21,7 +21,7 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
Expand All @@ -30,7 +30,7 @@ return function render(_ctx, _cache) {
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */)
])))
]))]))
}
}"
`;
Expand All @@ -42,11 +42,11 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createCommentVNode("comment")
], -1 /* CACHED */)
])))
]))]))
}
}"
`;
Expand All @@ -58,11 +58,11 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo", -1 /* CACHED */),
_createElementVNode("div", null, null, -1 /* CACHED */)
])))
]))]))
}
}"
`;
Expand All @@ -74,9 +74,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */)
])))
]))]))
}
}"
`;
Expand Down Expand Up @@ -147,9 +147,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */)
])))
]))]))
}
}"
`;
Expand All @@ -161,9 +161,9 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */)
])))
]))]))
}
}"
`;
Expand Down Expand Up @@ -215,9 +215,9 @@ return function render(_ctx, _cache) {
const _directive_foo = _resolveDirective("foo")

return (_openBlock(), _createElementBlock("div", null, [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [
_withDirectives((_openBlock(), _createElementBlock("svg", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */)
]))), [
]))])), [
[_directive_foo]
])
]))
Expand Down Expand Up @@ -401,9 +401,9 @@ return function render(_ctx, _cache) {

return (_openBlock(), _createElementBlock("div", null, [
ok
? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
? (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
]))]))
: _createCommentVNode("v-if", true)
]))
}
Expand All @@ -422,15 +422,15 @@ return function render(_ctx, _cache) {

return (_openBlock(), _createElementBlock(_Fragment, null, [
_createCommentVNode("comment"),
_createElementVNode("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("div", { id: "b" }, [
_createElementVNode("div", { id: "c" }, [
_createElementVNode("div", { id: "d" }, [
_createElementVNode("div", { id: "e" }, "hello")
])
])
], -1 /* CACHED */)
]))
]))])
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
}
}"
Expand All @@ -448,9 +448,9 @@ return function render(_ctx, _cache) {

return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
return (_openBlock(), _createElementBlock("div", _hoisted_1, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
]))]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}
Expand Down
12 changes: 1 addition & 11 deletions packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { PatchFlags } from '@vue/shared'

const cachedChildrenArrayMatcher = (
tags: string[],
needArraySpread = false,
needArraySpread = true,
) => ({
type: NodeTypes.JS_CACHE_EXPRESSION,
needArraySpread,
Expand Down Expand Up @@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})
Expand Down Expand Up @@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => {
{
/* _ slot flag */
},
{
type: NodeTypes.JS_PROPERTY,
key: { content: '__' },
value: { content: '[0]' },
},
],
})
})
Expand Down
37 changes: 7 additions & 30 deletions packages/compiler-core/src/transforms/cacheStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ import {
type RootNode,
type SimpleExpressionNode,
type SlotFunctionExpression,
type SlotsObjectProperty,
type TemplateChildNode,
type TemplateNode,
type TextCallNode,
type VNodeCall,
createArrayExpression,
createObjectProperty,
createSimpleExpression,
getVNodeBlockHelper,
getVNodeHelper,
} from '../ast'
Expand Down Expand Up @@ -157,7 +154,6 @@ function walk(
}

let cachedAsArray = false
const slotCacheKeys = []
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
if (
node.tagType === ElementTypes.ELEMENT &&
Expand All @@ -181,7 +177,6 @@ function walk(
// default slot
const slot = getSlotNode(node.codegenNode, 'default')
if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
Expand All @@ -205,7 +200,6 @@ function walk(
slotName.arg &&
getSlotNode(parent.codegenNode, slotName.arg)
if (slot) {
slotCacheKeys.push(context.cached.length)
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
Expand All @@ -216,39 +210,22 @@ function walk(

if (!cachedAsArray) {
for (const child of toCache) {
slotCacheKeys.push(context.cached.length)
child.codegenNode = context.cache(child.codegenNode!)
}
}

// put the slot cached keys on the slot object, so that the cache
// can be removed when component unmounting to prevent memory leaks
if (
slotCacheKeys.length &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.COMPONENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
node.codegenNode.children &&
!isArray(node.codegenNode.children) &&
node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
node.codegenNode.children.properties.push(
createObjectProperty(
`__`,
createSimpleExpression(JSON.stringify(slotCacheKeys), false),
) as SlotsObjectProperty,
)
}

function getCacheExpression(value: JSChildNode): CacheExpression {
const exp = context.cache(value)
// #6978, #7138, #7114
// a cached children array inside v-for can caused HMR errors since
// it might be mutated when mounting the first item
if (inFor && context.hmr) {
exp.needArraySpread = true
}
// #13221
// fix memory leak in cached array:
// cached vnodes get replaced by cloned ones during mountChildren,
// which bind DOM elements. These DOM references persist after unmount,
// preventing garbage collection. Array spread avoids mutating cached
// array, preventing memory leaks.
exp.needArraySpread = true
return exp
}

Expand Down
Loading