]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-core): prevent cached array children from retaining detached dom nodes...
authoredison <daiwei521@126.com>
Wed, 20 Aug 2025 12:51:04 +0000 (20:51 +0800)
committerGitHub <noreply@github.com>
Wed, 20 Aug 2025 12:51:04 +0000 (20:51 +0800)
fix element-plus/element-plus#21408
Re-fix #13211

packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
packages/compiler-core/src/transforms/cacheStatic.ts
packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
packages/compiler-sfc/__tests__/__snapshots__/templateTransformAssetUrl.spec.ts.snap
packages/compiler-sfc/__tests__/__snapshots__/templateTransformSrcset.spec.ts.snap
packages/runtime-core/__tests__/componentSlots.spec.ts
packages/runtime-core/src/componentSlots.ts
packages/runtime-core/src/renderer.ts
packages/vue/__tests__/e2e/memory-leak.spec.ts

index 91a82db5bba30b55e312652f79010b8267069801..7d5e47719eb654ebe88fe93a14b02389846ab8a7 100644 (file)
@@ -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 */))
     ]))
   }
index 74f6caca328831ddf9d5534d02dd602b31f7a723..1b5b23fec2989b8a693fd04e2a293575abbd1b6b 100644 (file)
@@ -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]' },
-        },
       ],
     })
   })
index 0f112e19cadc0ee526ff5893b57113d0fa392033..c4d29f71248b28f7e3eb6d196ca1bb80a5fae9da 100644 (file)
@@ -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
   }
 
index 5bc40d3fab5181d51847a9eb2d5dd2a94d320a22..84c3024f6bfc5cdb680ecbffadcd8dfbcb64c1c7 100644 (file)
@@ -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("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 20),
     _createElementVNode("div", { key: "1" }, "1", -1 /* CACHED */),
     _createStaticVNode("<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>", 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("<div><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span></div>", 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("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 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("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -46,7 +46,7 @@ exports[`stringify static html > should bail for <option> elements with null val
 "const { createElementVNode: _createElementVNode, 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] = [
     _createElementVNode("select", null, [
       _createElementVNode("option", { value: null }),
       _createElementVNode("option", { value: "1" }),
@@ -55,7 +55,7 @@ return function render(_ctx, _cache) {
       _createElementVNode("option", { value: "1" }),
       _createElementVNode("option", { value: "1" })
     ], -1 /* CACHED */)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -63,7 +63,7 @@ exports[`stringify static html > should bail for <option> elements with number v
 "const { createElementVNode: _createElementVNode, 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] = [
     _createElementVNode("select", null, [
       _createElementVNode("option", { value: 1 }),
       _createElementVNode("option", { value: 1 }),
@@ -71,7 +71,7 @@ return function render(_ctx, _cache) {
       _createElementVNode("option", { value: 1 }),
       _createElementVNode("option", { value: 1 })
     ], -1 /* CACHED */)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -95,7 +95,7 @@ exports[`stringify static html > should bail on bindings that are cached but not
 "const { createElementVNode: _createElementVNode, 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] = [
     _createElementVNode("div", null, [
       _createElementVNode("span", { class: "foo" }, "foo"),
       _createElementVNode("span", { class: "foo" }, "foo"),
@@ -104,7 +104,7 @@ return function render(_ctx, _cache) {
       _createElementVNode("span", { class: "foo" }, "foo"),
       _createElementVNode("img", { src: _imports_0_ })
     ], -1 /* CACHED */)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -112,9 +112,9 @@ exports[`stringify static html > should work for <option> elements with string v
 "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("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -122,9 +122,9 @@ exports[`stringify static html > should work for multiple adjacent nodes 1`] = `
 "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("<span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span>", 5)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -132,9 +132,9 @@ exports[`stringify static html > should work on eligible content (elements > 20)
 "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("<div><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>", 1)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -142,9 +142,9 @@ exports[`stringify static html > should work on eligible content (elements with
 "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("<div><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span></div>", 1)
-  ])))
+  ]))]))
 }"
 `;
 
@@ -152,9 +152,9 @@ exports[`stringify static html > should work with bindings that are non-static b
 "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("<div><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><img src=\\"" + _imports_0_ + "\\"></div>", 1)
-  ])))
+  ]))]))
 }"
 `;
 
index 18ec05da2b574ad66d9c47a1cfdd02331f67dc69..903b5be8693496ea5ebddbded18b687f57073c74 100644 (file)
@@ -81,9 +81,9 @@ import _imports_1 from '/bar.png'
 
 
 export function render(_ctx, _cache) {
-  return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
+  return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
     _createStaticVNode("<img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\">", 5)
-  ])))
+  ]))]))
 }"
 `;
 
index 28e0af71f7ad7f43522b85ca27a84ef966abf83a..8381f5a85cdd45d406c1c6c0306c09c8fd6c51ff 100644 (file)
@@ -238,8 +238,8 @@ const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
 const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
 
 export function render(_ctx, _cache) {
-  return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
+  return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
     _createStaticVNode("<img src=\\"./logo.png\\" srcset=\\"\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_1 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_2 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_3 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_4 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_5 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_6 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_7 + "\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_8 + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_9 + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", 12)
-  ])))
+  ]))]))
 }"
 `;
index 765fce33e42765419509ddb675eb18df7fd7a1bc..458731dd150c9b3ac407b3868822b809d444cd51 100644 (file)
@@ -56,14 +56,10 @@ describe('component: slots', () => {
         expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
           false,
         )
-        expect(slots).toHaveProperty('__')
-        expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe(
-          false,
-        )
         return h('div')
       },
     }
-    const slots = { foo: () => {}, _: 1, __: [1] }
+    const slots = { foo: () => {}, _: 1 }
     render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
   })
 
index 380728750f84768600851eda8917ecdf835fecb8..c19df7ec80fd11c6470b6fa3c7d5b96c46eb88eb 100644 (file)
@@ -79,15 +79,10 @@ export type RawSlots = {
    * @internal
    */
   _?: SlotFlags
-  /**
-   * cache indexes for slot content
-   * @internal
-   */
-  __?: number[]
 }
 
 const isInternalKey = (key: string) =>
-  key === '_' || key === '__' || key === '_ctx' || key === '$stable'
+  key === '_' || key === '_ctx' || key === '$stable'
 
 const normalizeSlotValue = (value: unknown): VNode[] =>
   isArray(value)
@@ -194,10 +189,6 @@ export const initSlots = (
 ): void => {
   const slots = (instance.slots = createInternalObject())
   if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
-    const cacheIndexes = (children as RawSlots).__
-    // make cache indexes marker non-enumerable
-    if (cacheIndexes) def(slots, '__', cacheIndexes, true)
-
     const type = (children as RawSlots)._
     if (type) {
       assignSlots(slots, children as Slots, optimized)
index f046e93ad85af04d0c4191e84e6fb7751e65bd69..129206fdf6b63a7eac0ce6507ba34d25877a8bfa 100644 (file)
@@ -2277,17 +2277,7 @@ function baseCreateRenderer(
       unregisterHMR(instance)
     }
 
-    const {
-      bum,
-      scope,
-      job,
-      subTree,
-      um,
-      m,
-      a,
-      parent,
-      slots: { __: slotCacheKeys },
-    } = instance
+    const { bum, scope, job, subTree, um, m, a } = instance
     invalidateMount(m)
     invalidateMount(a)
 
@@ -2296,13 +2286,6 @@ function baseCreateRenderer(
       invokeArrayFns(bum)
     }
 
-    // remove slots content from parent renderCache
-    if (parent && isArray(slotCacheKeys)) {
-      slotCacheKeys.forEach(v => {
-        parent.renderCache[v] = undefined
-      })
-    }
-
     if (
       __COMPAT__ &&
       isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
@@ -2513,7 +2496,11 @@ export function traverseStaticChildren(
           traverseStaticChildren(c1, c2)
       }
       // #6852 also inherit for text nodes
-      if (c2.type === Text) {
+      if (
+        c2.type === Text &&
+        // avoid cached text nodes retaining detached dom nodes
+        c2.patchFlag !== PatchFlags.CACHED
+      ) {
         c2.el = c1.el
       }
       // #2324 also inherit for comment nodes, but not placeholders (e.g. v-if which
index 2412cea2b0a900c95c782e927eb08b070e0edf14..bd53387dc1b90cb171f06b823592b92e3bbc90bc 100644 (file)
@@ -82,4 +82,146 @@ describe('not leaking', async () => {
     },
     E2E_TIMEOUT,
   )
+
+  // #13211
+  test(
+    'cached array 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 }
+              },
+            },
+          },
+          template: `
+          <button id="toggleBtn" @click="click">button</button>
+          <Comp1 v-if="toggle">slot content</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>` +
+          `slot content` +
+          `</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,
+  )
+
+  // https://github.com/element-plus/element-plus/issues/21408
+  test(
+    'cached text nodes in Fragment 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: {
+            Comp: {
+              template: `<div>{{ test.length }}</div>`,
+              setup() {
+                const test = ref([...Array(3000)].map((_, i) => ({ i })))
+                // @ts-expect-error
+                window.__REF__ = new WeakRef(test)
+
+                return { test }
+              },
+            },
+          },
+          template: `
+          <button id="addBtn" @click="add">add</button>
+          <button id="toggleBtn" @click="click">button</button>
+          <div v-if="toggle">
+            <template v-for="item in items" :key="item">
+              text
+              <div>{{ item }}</div>
+            </template>
+            <Comp/>
+          </div>
+        `,
+          setup() {
+            const toggle = ref(true)
+            const items = ref([1])
+            const click = () => (toggle.value = !toggle.value)
+            const add = () => items.value.push(2)
+            return { toggle, click, items, add }
+          },
+        }).mount('#app')
+      })
+
+      expect(await html('#app')).toBe(
+        `<button id="addBtn">add</button>` +
+          `<button id="toggleBtn">button</button>` +
+          `<div>` +
+          ` text ` +
+          `<div>1</div>` +
+          `<div>3000</div></div>`,
+      )
+
+      await click('#addBtn')
+      expect(await html('#app')).toBe(
+        `<button id="addBtn">add</button>` +
+          `<button id="toggleBtn">button</button>` +
+          `<div>` +
+          ` text ` +
+          `<div>1</div>` +
+          ` text ` +
+          `<div>2</div>` +
+          `<div>3000</div></div>`,
+      )
+
+      await click('#toggleBtn')
+      expect(await html('#app')).toBe(
+        `<button id="addBtn">add</button>` +
+          `<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,
+  )
 })