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 91a82db5bba..7d5e47719eb 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap @@ -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 */) - ]))) + ]))])) } }" `; @@ -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") @@ -30,7 +30,7 @@ return function render(_ctx, _cache) { _createElementVNode("span"), _createElementVNode("span") ], -1 /* CACHED */) - ]))) + ]))])) } }" `; @@ -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 */) - ]))) + ]))])) } }" `; @@ -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 */) - ]))) + ]))])) } }" `; @@ -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 */) - ]))) + ]))])) } }" `; @@ -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 */) - ]))) + ]))])) } }" `; @@ -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 */) - ]))) + ]))])) } }" `; @@ -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] ]) ])) @@ -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) ])) } @@ -422,7 +422,7 @@ 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" }, [ @@ -430,7 +430,7 @@ return function render(_ctx, _cache) { ]) ]) ], -1 /* CACHED */) - ])) + ]))]) ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) } }" @@ -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 */)) ])) } diff --git a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts index 74f6caca328..1b5b23fec29 100644 --- a/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts @@ -27,7 +27,7 @@ import { PatchFlags } from '@vue/shared' const cachedChildrenArrayMatcher = ( tags: string[], - needArraySpread = false, + needArraySpread = true, ) => ({ type: NodeTypes.JS_CACHE_EXPRESSION, needArraySpread, @@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => { { /* _ slot flag */ }, - { - type: NodeTypes.JS_PROPERTY, - key: { content: '__' }, - value: { content: '[0]' }, - }, ], }) }) @@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => { { /* _ slot flag */ }, - { - type: NodeTypes.JS_PROPERTY, - key: { content: '__' }, - value: { content: '[0]' }, - }, ], }) }) diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts index 0f112e19cad..c4d29f71248 100644 --- a/packages/compiler-core/src/transforms/cacheStatic.ts +++ b/packages/compiler-core/src/transforms/cacheStatic.ts @@ -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' @@ -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 && @@ -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[]), ) @@ -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[]), ) @@ -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 } diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap index 5bc40d3fab5..84c3024f6bf 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap @@ -4,11 +4,11 @@ exports[`stringify static html > eligible content (elements > 20) + non-eligible "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("", 20), _createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */), _createStaticVNode("", 20) - ]))) + ]))])) }" `; @@ -16,9 +16,9 @@ exports[`stringify static html > escape 1`] = ` "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("
1 + <&1 + <&1 + <&1 + <&1 + <&
", 1) - ]))) + ]))])) }" `; @@ -26,9 +26,9 @@ exports[`stringify static html > serializing constant bindings 1`] = ` "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("
1 + false1 + false1 + false1 + false1 + false
", 1) - ]))) + ]))])) }" `; @@ -36,9 +36,9 @@ exports[`stringify static html > serializing template string style 1`] = ` "const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue return function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ + return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [ _createStaticVNode("
1 + false1 + false1 + false1 + false1 + false
", 1) - ]))) + ]))])) }" `; @@ -46,7 +46,7 @@ exports[`stringify static html > should bail for