mirror of https://github.com/vuejs/core.git
fix(compiler-core): prevent cached array children from retaining detached dom nodes
This commit is contained in:
parent
c486536105
commit
18d2fac858
|
@ -170,11 +170,6 @@ describe('compiler: cacheStatic transform', () => {
|
||||||
{
|
{
|
||||||
/* _ slot flag */
|
/* _ slot flag */
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: NodeTypes.JS_PROPERTY,
|
|
||||||
key: { content: '__' },
|
|
||||||
value: { content: '[0]' },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -202,11 +197,6 @@ describe('compiler: cacheStatic transform', () => {
|
||||||
{
|
{
|
||||||
/* _ slot flag */
|
/* _ slot flag */
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: NodeTypes.JS_PROPERTY,
|
|
||||||
key: { content: '__' },
|
|
||||||
value: { content: '[0]' },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -420,6 +420,7 @@ export interface CacheExpression extends Node {
|
||||||
needPauseTracking: boolean
|
needPauseTracking: boolean
|
||||||
inVOnce: boolean
|
inVOnce: boolean
|
||||||
needArraySpread: boolean
|
needArraySpread: boolean
|
||||||
|
cachedAsArray: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemoExpression extends CallExpression {
|
export interface MemoExpression extends CallExpression {
|
||||||
|
@ -784,6 +785,7 @@ export function createCacheExpression(
|
||||||
needPauseTracking: needPauseTracking,
|
needPauseTracking: needPauseTracking,
|
||||||
inVOnce,
|
inVOnce,
|
||||||
needArraySpread: false,
|
needArraySpread: false,
|
||||||
|
cachedAsArray: false,
|
||||||
loc: locStub,
|
loc: locStub,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1012,7 +1012,7 @@ function genConditionalExpression(
|
||||||
|
|
||||||
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
||||||
const { push, helper, indent, deindent, newline } = context
|
const { push, helper, indent, deindent, newline } = context
|
||||||
const { needPauseTracking, needArraySpread } = node
|
const { needPauseTracking, needArraySpread, cachedAsArray } = node
|
||||||
if (needArraySpread) {
|
if (needArraySpread) {
|
||||||
push(`[...(`)
|
push(`[...(`)
|
||||||
}
|
}
|
||||||
|
@ -1027,6 +1027,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
|
||||||
}
|
}
|
||||||
push(`_cache[${node.index}] = `)
|
push(`_cache[${node.index}] = `)
|
||||||
genNode(node.value, context)
|
genNode(node.value, context)
|
||||||
|
if (cachedAsArray) {
|
||||||
|
push(`, _cache[${node.index}].patchFlag = -1, _cache[${node.index}]`)
|
||||||
|
}
|
||||||
if (needPauseTracking) {
|
if (needPauseTracking) {
|
||||||
push(`).cacheIndex = ${node.index},`)
|
push(`).cacheIndex = ${node.index},`)
|
||||||
newline()
|
newline()
|
||||||
|
|
|
@ -12,14 +12,11 @@ import {
|
||||||
type RootNode,
|
type RootNode,
|
||||||
type SimpleExpressionNode,
|
type SimpleExpressionNode,
|
||||||
type SlotFunctionExpression,
|
type SlotFunctionExpression,
|
||||||
type SlotsObjectProperty,
|
|
||||||
type TemplateChildNode,
|
type TemplateChildNode,
|
||||||
type TemplateNode,
|
type TemplateNode,
|
||||||
type TextCallNode,
|
type TextCallNode,
|
||||||
type VNodeCall,
|
type VNodeCall,
|
||||||
createArrayExpression,
|
createArrayExpression,
|
||||||
createObjectProperty,
|
|
||||||
createSimpleExpression,
|
|
||||||
getVNodeBlockHelper,
|
getVNodeBlockHelper,
|
||||||
getVNodeHelper,
|
getVNodeHelper,
|
||||||
} from '../ast'
|
} from '../ast'
|
||||||
|
@ -70,7 +67,10 @@ function walk(
|
||||||
inFor = false,
|
inFor = false,
|
||||||
) {
|
) {
|
||||||
const { children } = node
|
const { children } = node
|
||||||
const toCache: (PlainElementNode | TextCallNode)[] = []
|
const toCacheMap = new Map<
|
||||||
|
PlainElementNode | TextCallNode,
|
||||||
|
undefined | (() => void)
|
||||||
|
>()
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
const child = children[i]
|
const child = children[i]
|
||||||
// only plain elements & text calls are eligible for caching.
|
// only plain elements & text calls are eligible for caching.
|
||||||
|
@ -83,8 +83,11 @@ function walk(
|
||||||
: getConstantType(child, context)
|
: getConstantType(child, context)
|
||||||
if (constantType > ConstantTypes.NOT_CONSTANT) {
|
if (constantType > ConstantTypes.NOT_CONSTANT) {
|
||||||
if (constantType >= ConstantTypes.CAN_CACHE) {
|
if (constantType >= ConstantTypes.CAN_CACHE) {
|
||||||
;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.CACHED
|
toCacheMap.set(
|
||||||
toCache.push(child)
|
child,
|
||||||
|
() =>
|
||||||
|
((child.codegenNode as VNodeCall).patchFlag = PatchFlags.CACHED),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -115,6 +118,7 @@ function walk(
|
||||||
? ConstantTypes.NOT_CONSTANT
|
? ConstantTypes.NOT_CONSTANT
|
||||||
: getConstantType(child, context)
|
: getConstantType(child, context)
|
||||||
if (constantType >= ConstantTypes.CAN_CACHE) {
|
if (constantType >= ConstantTypes.CAN_CACHE) {
|
||||||
|
toCacheMap.set(child, () => {
|
||||||
if (
|
if (
|
||||||
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
|
child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
|
||||||
child.codegenNode.arguments.length > 0
|
child.codegenNode.arguments.length > 0
|
||||||
|
@ -124,7 +128,7 @@ function walk(
|
||||||
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
|
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
toCache.push(child)
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,8 +161,7 @@ function walk(
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedAsArray = false
|
let cachedAsArray = false
|
||||||
const slotCacheKeys = []
|
if (toCacheMap.size === children.length && node.type === NodeTypes.ELEMENT) {
|
||||||
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
|
|
||||||
if (
|
if (
|
||||||
node.tagType === ElementTypes.ELEMENT &&
|
node.tagType === ElementTypes.ELEMENT &&
|
||||||
node.codegenNode &&
|
node.codegenNode &&
|
||||||
|
@ -181,7 +184,6 @@ function walk(
|
||||||
// default slot
|
// default slot
|
||||||
const slot = getSlotNode(node.codegenNode, 'default')
|
const slot = getSlotNode(node.codegenNode, 'default')
|
||||||
if (slot) {
|
if (slot) {
|
||||||
slotCacheKeys.push(context.cached.length)
|
|
||||||
slot.returns = getCacheExpression(
|
slot.returns = getCacheExpression(
|
||||||
createArrayExpression(slot.returns as TemplateChildNode[]),
|
createArrayExpression(slot.returns as TemplateChildNode[]),
|
||||||
)
|
)
|
||||||
|
@ -205,7 +207,6 @@ function walk(
|
||||||
slotName.arg &&
|
slotName.arg &&
|
||||||
getSlotNode(parent.codegenNode, slotName.arg)
|
getSlotNode(parent.codegenNode, slotName.arg)
|
||||||
if (slot) {
|
if (slot) {
|
||||||
slotCacheKeys.push(context.cached.length)
|
|
||||||
slot.returns = getCacheExpression(
|
slot.returns = getCacheExpression(
|
||||||
createArrayExpression(slot.returns as TemplateChildNode[]),
|
createArrayExpression(slot.returns as TemplateChildNode[]),
|
||||||
)
|
)
|
||||||
|
@ -215,33 +216,16 @@ function walk(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cachedAsArray) {
|
if (!cachedAsArray) {
|
||||||
for (const child of toCache) {
|
for (const [child, setupCache] of toCacheMap) {
|
||||||
slotCacheKeys.push(context.cached.length)
|
if (setupCache) setupCache()
|
||||||
child.codegenNode = context.cache(child.codegenNode!)
|
child.codegenNode = context.cache(child.codegenNode!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// put the slot cached keys on the slot object, so that the cache
|
function getCacheExpression(
|
||||||
// can be removed when component unmounting to prevent memory leaks
|
value: JSChildNode,
|
||||||
if (
|
cachedAsArray: boolean = true,
|
||||||
slotCacheKeys.length &&
|
): CacheExpression {
|
||||||
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)
|
const exp = context.cache(value)
|
||||||
// #6978, #7138, #7114
|
// #6978, #7138, #7114
|
||||||
// a cached children array inside v-for can caused HMR errors since
|
// a cached children array inside v-for can caused HMR errors since
|
||||||
|
@ -249,6 +233,7 @@ function walk(
|
||||||
if (inFor && context.hmr) {
|
if (inFor && context.hmr) {
|
||||||
exp.needArraySpread = true
|
exp.needArraySpread = true
|
||||||
}
|
}
|
||||||
|
exp.cachedAsArray = cachedAsArray
|
||||||
return exp
|
return exp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,7 +253,7 @@ function walk(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toCache.length && context.transformHoist) {
|
if (toCacheMap.size && context.transformHoist) {
|
||||||
context.transformHoist(children, context, node)
|
context.transformHoist(children, context, node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,14 +56,10 @@ describe('component: slots', () => {
|
||||||
expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
|
expect(Object.getOwnPropertyDescriptor(slots, '_')!.enumerable).toBe(
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
expect(slots).toHaveProperty('__')
|
|
||||||
expect(Object.getOwnPropertyDescriptor(slots, '__')!.enumerable).toBe(
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
return h('div')
|
return h('div')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const slots = { foo: () => {}, _: 1, __: [1] }
|
const slots = { foo: () => {}, _: 1 }
|
||||||
render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
|
render(createBlock(Comp, null, slots), nodeOps.createElement('div'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -79,15 +79,10 @@ export type RawSlots = {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_?: SlotFlags
|
_?: SlotFlags
|
||||||
/**
|
|
||||||
* cache indexes for slot content
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
__?: number[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInternalKey = (key: string) =>
|
const isInternalKey = (key: string) =>
|
||||||
key === '_' || key === '__' || key === '_ctx' || key === '$stable'
|
key === '_' || key === '_ctx' || key === '$stable'
|
||||||
|
|
||||||
const normalizeSlotValue = (value: unknown): VNode[] =>
|
const normalizeSlotValue = (value: unknown): VNode[] =>
|
||||||
isArray(value)
|
isArray(value)
|
||||||
|
@ -194,10 +189,6 @@ export const initSlots = (
|
||||||
): void => {
|
): void => {
|
||||||
const slots = (instance.slots = createInternalObject())
|
const slots = (instance.slots = createInternalObject())
|
||||||
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
|
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)._
|
const type = (children as RawSlots)._
|
||||||
if (type) {
|
if (type) {
|
||||||
assignSlots(slots, children as Slots, optimized)
|
assignSlots(slots, children as Slots, optimized)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Fragment,
|
Fragment,
|
||||||
type VNode,
|
type VNode,
|
||||||
type VNodeArrayChildren,
|
type VNodeArrayChildren,
|
||||||
|
cloneVNode,
|
||||||
createBlock,
|
createBlock,
|
||||||
createVNode,
|
createVNode,
|
||||||
isVNode,
|
isVNode,
|
||||||
|
@ -71,12 +72,24 @@ export function renderSlot(
|
||||||
;(slot as ContextualRenderFn)._d = false
|
;(slot as ContextualRenderFn)._d = false
|
||||||
}
|
}
|
||||||
openBlock()
|
openBlock()
|
||||||
const validSlotContent = slot && ensureValidVNode(slot(props))
|
let validSlotContent = slot && ensureValidVNode(slot(props))
|
||||||
const slotKey =
|
const slotKey =
|
||||||
props.key ||
|
props.key ||
|
||||||
// slot content array of a dynamic conditional slot may have a branch
|
// slot content array of a dynamic conditional slot may have a branch
|
||||||
// key attached in the `createSlots` helper, respect that
|
// key attached in the `createSlots` helper, respect that
|
||||||
(validSlotContent && (validSlotContent as any).key)
|
(validSlotContent && (validSlotContent as any).key)
|
||||||
|
|
||||||
|
// if slot content is an cached array, deep clone it to prevent
|
||||||
|
// cached vnodes from retaining detached DOM nodes
|
||||||
|
if (
|
||||||
|
validSlotContent &&
|
||||||
|
(validSlotContent as any).patchFlag === PatchFlags.CACHED
|
||||||
|
) {
|
||||||
|
validSlotContent = (validSlotContent as VNode[]).map(child =>
|
||||||
|
cloneVNode(child),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const rendered = createBlock(
|
const rendered = createBlock(
|
||||||
Fragment,
|
Fragment,
|
||||||
{
|
{
|
||||||
|
|
|
@ -2277,17 +2277,7 @@ function baseCreateRenderer(
|
||||||
unregisterHMR(instance)
|
unregisterHMR(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { bum, scope, job, subTree, um, m, a } = instance
|
||||||
bum,
|
|
||||||
scope,
|
|
||||||
job,
|
|
||||||
subTree,
|
|
||||||
um,
|
|
||||||
m,
|
|
||||||
a,
|
|
||||||
parent,
|
|
||||||
slots: { __: slotCacheKeys },
|
|
||||||
} = instance
|
|
||||||
invalidateMount(m)
|
invalidateMount(m)
|
||||||
invalidateMount(a)
|
invalidateMount(a)
|
||||||
|
|
||||||
|
@ -2296,13 +2286,6 @@ function baseCreateRenderer(
|
||||||
invokeArrayFns(bum)
|
invokeArrayFns(bum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove slots content from parent renderCache
|
|
||||||
if (parent && isArray(slotCacheKeys)) {
|
|
||||||
slotCacheKeys.forEach(v => {
|
|
||||||
parent.renderCache[v] = undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
__COMPAT__ &&
|
__COMPAT__ &&
|
||||||
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
||||||
|
|
|
@ -471,7 +471,11 @@ function createBaseVNode(
|
||||||
ref: props && normalizeRef(props),
|
ref: props && normalizeRef(props),
|
||||||
scopeId: currentScopeId,
|
scopeId: currentScopeId,
|
||||||
slotScopeIds: null,
|
slotScopeIds: null,
|
||||||
children,
|
children:
|
||||||
|
// children is a cached array
|
||||||
|
isArray(children) && (children as any).patchFlag === PatchFlags.CACHED
|
||||||
|
? (children as VNode[]).map(child => cloneVNode(child))
|
||||||
|
: children,
|
||||||
component: null,
|
component: null,
|
||||||
suspense: null,
|
suspense: null,
|
||||||
ssContent: null,
|
ssContent: null,
|
||||||
|
@ -680,7 +684,9 @@ export function cloneVNode<T, U>(
|
||||||
scopeId: vnode.scopeId,
|
scopeId: vnode.scopeId,
|
||||||
slotScopeIds: vnode.slotScopeIds,
|
slotScopeIds: vnode.slotScopeIds,
|
||||||
children:
|
children:
|
||||||
__DEV__ && patchFlag === PatchFlags.CACHED && isArray(children)
|
// if vnode is cached, deep clone it's children to prevent cached children
|
||||||
|
// from retaining detached DOM nodes
|
||||||
|
patchFlag === PatchFlags.CACHED && isArray(children)
|
||||||
? (children as VNode[]).map(deepCloneVNode)
|
? (children as VNode[]).map(deepCloneVNode)
|
||||||
: children,
|
: children,
|
||||||
target: vnode.target,
|
target: vnode.target,
|
||||||
|
@ -738,7 +744,7 @@ export function cloneVNode<T, U>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dev only, for HMR of hoisted vnodes reused in v-for
|
* for HMR of hoisted vnodes reused in v-for
|
||||||
* https://github.com/vitejs/vite/issues/2022
|
* https://github.com/vitejs/vite/issues/2022
|
||||||
*/
|
*/
|
||||||
function deepCloneVNode(vnode: VNode): VNode {
|
function deepCloneVNode(vnode: VNode): VNode {
|
||||||
|
|
Loading…
Reference in New Issue