Skip to content

Commit 18d2fac

Browse files
committed
fix(compiler-core): prevent cached array children from retaining detached dom nodes
1 parent c486536 commit 18d2fac

File tree

9 files changed

+61
-92
lines changed

9 files changed

+61
-92
lines changed

packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => {
170170
{
171171
/* _ slot flag */
172172
},
173-
{
174-
type: NodeTypes.JS_PROPERTY,
175-
key: { content: '__' },
176-
value: { content: '[0]' },
177-
},
178173
],
179174
})
180175
})
@@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => {
202197
{
203198
/* _ slot flag */
204199
},
205-
{
206-
type: NodeTypes.JS_PROPERTY,
207-
key: { content: '__' },
208-
value: { content: '[0]' },
209-
},
210200
],
211201
})
212202
})

packages/compiler-core/src/ast.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ export interface CacheExpression extends Node {
420420
needPauseTracking: boolean
421421
inVOnce: boolean
422422
needArraySpread: boolean
423+
cachedAsArray: boolean
423424
}
424425

425426
export interface MemoExpression extends CallExpression {
@@ -784,6 +785,7 @@ export function createCacheExpression(
784785
needPauseTracking: needPauseTracking,
785786
inVOnce,
786787
needArraySpread: false,
788+
cachedAsArray: false,
787789
loc: locStub,
788790
}
789791
}

packages/compiler-core/src/codegen.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1012,7 +1012,7 @@ function genConditionalExpression(
10121012

10131013
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
10141014
const { push, helper, indent, deindent, newline } = context
1015-
const { needPauseTracking, needArraySpread } = node
1015+
const { needPauseTracking, needArraySpread, cachedAsArray } = node
10161016
if (needArraySpread) {
10171017
push(`[...(`)
10181018
}
@@ -1027,6 +1027,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
10271027
}
10281028
push(`_cache[${node.index}] = `)
10291029
genNode(node.value, context)
1030+
if (cachedAsArray) {
1031+
push(`, _cache[${node.index}].patchFlag = -1, _cache[${node.index}]`)
1032+
}
10301033
if (needPauseTracking) {
10311034
push(`).cacheIndex = ${node.index},`)
10321035
newline()

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

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ import {
1212
type RootNode,
1313
type SimpleExpressionNode,
1414
type SlotFunctionExpression,
15-
type SlotsObjectProperty,
1615
type TemplateChildNode,
1716
type TemplateNode,
1817
type TextCallNode,
1918
type VNodeCall,
2019
createArrayExpression,
21-
createObjectProperty,
22-
createSimpleExpression,
2320
getVNodeBlockHelper,
2421
getVNodeHelper,
2522
} from '../ast'
@@ -70,7 +67,10 @@ function walk(
7067
inFor = false,
7168
) {
7269
const { children } = node
73-
const toCache: (PlainElementNode | TextCallNode)[] = []
70+
const toCacheMap = new Map<
71+
PlainElementNode | TextCallNode,
72+
undefined | (() => void)
73+
>()
7474
for (let i = 0; i < children.length; i++) {
7575
const child = children[i]
7676
// only plain elements & text calls are eligible for caching.
@@ -83,8 +83,11 @@ function walk(
8383
: getConstantType(child, context)
8484
if (constantType > ConstantTypes.NOT_CONSTANT) {
8585
if (constantType >= ConstantTypes.CAN_CACHE) {
86-
;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.CACHED
87-
toCache.push(child)
86+
toCacheMap.set(
87+
child,
88+
() =>
89+
((child.codegenNode as VNodeCall).patchFlag = PatchFlags.CACHED),
90+
)
8891
continue
8992
}
9093
} else {
@@ -115,16 +118,17 @@ function walk(
115118
? ConstantTypes.NOT_CONSTANT
116119
: getConstantType(child, context)
117120
if (constantType >= ConstantTypes.CAN_CACHE) {
118-
if (
119-
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
120-
child.codegenNode.arguments.length > 0
121-
) {
122-
child.codegenNode.arguments.push(
123-
PatchFlags.CACHED +
124-
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
125-
)
126-
}
127-
toCache.push(child)
121+
toCacheMap.set(child, () => {
122+
if (
123+
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
124+
child.codegenNode.arguments.length > 0
125+
) {
126+
child.codegenNode.arguments.push(
127+
PatchFlags.CACHED +
128+
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
129+
)
130+
}
131+
})
128132
continue
129133
}
130134
}
@@ -157,8 +161,7 @@ function walk(
157161
}
158162

159163
let cachedAsArray = false
160-
const slotCacheKeys = []
161-
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
164+
if (toCacheMap.size === children.length && node.type === NodeTypes.ELEMENT) {
162165
if (
163166
node.tagType === ElementTypes.ELEMENT &&
164167
node.codegenNode &&
@@ -181,7 +184,6 @@ function walk(
181184
// default slot
182185
const slot = getSlotNode(node.codegenNode, 'default')
183186
if (slot) {
184-
slotCacheKeys.push(context.cached.length)
185187
slot.returns = getCacheExpression(
186188
createArrayExpression(slot.returns as TemplateChildNode[]),
187189
)
@@ -205,7 +207,6 @@ function walk(
205207
slotName.arg &&
206208
getSlotNode(parent.codegenNode, slotName.arg)
207209
if (slot) {
208-
slotCacheKeys.push(context.cached.length)
209210
slot.returns = getCacheExpression(
210211
createArrayExpression(slot.returns as TemplateChildNode[]),
211212
)
@@ -215,40 +216,24 @@ function walk(
215216
}
216217

217218
if (!cachedAsArray) {
218-
for (const child of toCache) {
219-
slotCacheKeys.push(context.cached.length)
219+
for (const [child, setupCache] of toCacheMap) {
220+
if (setupCache) setupCache()
220221
child.codegenNode = context.cache(child.codegenNode!)
221222
}
222223
}
223224

224-
// put the slot cached keys on the slot object, so that the cache
225-
// can be removed when component unmounting to prevent memory leaks
226-
if (
227-
slotCacheKeys.length &&
228-
node.type === NodeTypes.ELEMENT &&
229-
node.tagType === ElementTypes.COMPONENT &&
230-
node.codegenNode &&
231-
node.codegenNode.type === NodeTypes.VNODE_CALL &&
232-
node.codegenNode.children &&
233-
!isArray(node.codegenNode.children) &&
234-
node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
235-
) {
236-
node.codegenNode.children.properties.push(
237-
createObjectProperty(
238-
`__`,
239-
createSimpleExpression(JSON.stringify(slotCacheKeys), false),
240-
) as SlotsObjectProperty,
241-
)
242-
}
243-
244-
function getCacheExpression(value: JSChildNode): CacheExpression {
225+
function getCacheExpression(
226+
value: JSChildNode,
227+
cachedAsArray: boolean = true,
228+
): CacheExpression {
245229
const exp = context.cache(value)
246230
// #6978, #7138, #7114
247231
// a cached children array inside v-for can caused HMR errors since
248232
// it might be mutated when mounting the first item
249233
if (inFor && context.hmr) {
250234
exp.needArraySpread = true
251235
}
236+
exp.cachedAsArray = cachedAsArray
252237
return exp
253238
}
254239

@@ -268,7 +253,7 @@ function walk(
268253
}
269254
}
270255

271-
if (toCache.length && context.transformHoist) {
256+
if (toCacheMap.size && context.transformHoist) {
272257
context.transformHoist(children, context, node)
273258
}
274259
}

packages/runtime-core/__tests__/componentSlots.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,10 @@ describe('component: slots', () => {
5656
expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
5757
false,
5858
)
59-
expect(slots).toHaveProperty('__')
60-
expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe(
61-
false,
62-
)
6359
return h('div')
6460
},
6561
}
66-
const slots = { foo: () => {}, _: 1, __: [1] }
62+
const slots = { foo: () => {}, _: 1 }
6763
render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
6864
})
6965

packages/runtime-core/src/componentSlots.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,10 @@ export type RawSlots = {
7979
* @internal
8080
*/
8181
_?: SlotFlags
82-
/**
83-
* cache indexes for slot content
84-
* @internal
85-
*/
86-
__?: number[]
8782
}
8883

8984
const isInternalKey = (key: string) =>
90-
key === '_' || key === '__' || key === '_ctx' || key === '$stable'
85+
key === '_' || key === '_ctx' || key === '$stable'
9186

9287
const normalizeSlotValue = (value: unknown): VNode[] =>
9388
isArray(value)
@@ -194,10 +189,6 @@ export const initSlots = (
194189
): void => {
195190
const slots = (instance.slots = createInternalObject())
196191
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
197-
const cacheIndexes = (children as RawSlots).__
198-
// make cache indexes marker non-enumerable
199-
if (cacheIndexes) def(slots, '__', cacheIndexes, true)
200-
201192
const type = (children as RawSlots)._
202193
if (type) {
203194
assignSlots(slots, children as Slots, optimized)

packages/runtime-core/src/helpers/renderSlot.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Fragment,
1010
type VNode,
1111
type VNodeArrayChildren,
12+
cloneVNode,
1213
createBlock,
1314
createVNode,
1415
isVNode,
@@ -71,12 +72,24 @@ export function renderSlot(
7172
;(slot as ContextualRenderFn)._d = false
7273
}
7374
openBlock()
74-
const validSlotContent = slot && ensureValidVNode(slot(props))
75+
let validSlotContent = slot && ensureValidVNode(slot(props))
7576
const slotKey =
7677
props.key ||
7778
// slot content array of a dynamic conditional slot may have a branch
7879
// key attached in the `createSlots` helper, respect that
7980
(validSlotContent && (validSlotContent as any).key)
81+
82+
// if slot content is an cached array, deep clone it to prevent
83+
// cached vnodes from retaining detached DOM nodes
84+
if (
85+
validSlotContent &&
86+
(validSlotContent as any).patchFlag === PatchFlags.CACHED
87+
) {
88+
validSlotContent = (validSlotContent as VNode[]).map(child =>
89+
cloneVNode(child),
90+
)
91+
}
92+
8093
const rendered = createBlock(
8194
Fragment,
8295
{

packages/runtime-core/src/renderer.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2277,17 +2277,7 @@ function baseCreateRenderer(
22772277
unregisterHMR(instance)
22782278
}
22792279

2280-
const {
2281-
bum,
2282-
scope,
2283-
job,
2284-
subTree,
2285-
um,
2286-
m,
2287-
a,
2288-
parent,
2289-
slots: { __: slotCacheKeys },
2290-
} = instance
2280+
const { bum, scope, job, subTree, um, m, a } = instance
22912281
invalidateMount(m)
22922282
invalidateMount(a)
22932283

@@ -2296,13 +2286,6 @@ function baseCreateRenderer(
22962286
invokeArrayFns(bum)
22972287
}
22982288

2299-
// remove slots content from parent renderCache
2300-
if (parent && isArray(slotCacheKeys)) {
2301-
slotCacheKeys.forEach(v => {
2302-
parent.renderCache[v] = undefined
2303-
})
2304-
}
2305-
23062289
if (
23072290
__COMPAT__ &&
23082291
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)

packages/runtime-core/src/vnode.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,11 @@ function createBaseVNode(
471471
ref: props && normalizeRef(props),
472472
scopeId: currentScopeId,
473473
slotScopeIds: null,
474-
children,
474+
children:
475+
// children is a cached array
476+
isArray(children) && (children as any).patchFlag === PatchFlags.CACHED
477+
? (children as VNode[]).map(child => cloneVNode(child))
478+
: children,
475479
component: null,
476480
suspense: null,
477481
ssContent: null,
@@ -680,7 +684,9 @@ export function cloneVNode<T, U>(
680684
scopeId: vnode.scopeId,
681685
slotScopeIds: vnode.slotScopeIds,
682686
children:
683-
__DEV__ && patchFlag === PatchFlags.CACHED && isArray(children)
687+
// if vnode is cached, deep clone it's children to prevent cached children
688+
// from retaining detached DOM nodes
689+
patchFlag === PatchFlags.CACHED && isArray(children)
684690
? (children as VNode[]).map(deepCloneVNode)
685691
: children,
686692
target: vnode.target,
@@ -738,7 +744,7 @@ export function cloneVNode<T, U>(
738744
}
739745

740746
/**
741-
* Dev only, for HMR of hoisted vnodes reused in v-for
747+
* for HMR of hoisted vnodes reused in v-for
742748
* https://github.com/vitejs/vite/issues/2022
743749
*/
744750
function deepCloneVNode(vnode: VNode): VNode {

0 commit comments

Comments
 (0)