fix(runtime-core): properly handle inherit transition during clone VNode (#10809)

close #3716
close #10497
close #4091
This commit is contained in:
edison 2024-04-29 14:39:14 +08:00 committed by GitHub
parent e8fd6446d1
commit 638a79f64a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 71 additions and 8 deletions

View File

@ -166,7 +166,7 @@ export function renderComponentRoot(
propsOptions, propsOptions,
) )
} }
root = cloneVNode(root, fallthroughAttrs) root = cloneVNode(root, fallthroughAttrs, false, true)
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) { } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
const allAttrs = Object.keys(attrs) const allAttrs = Object.keys(attrs)
const eventAttrs: string[] = [] const eventAttrs: string[] = []
@ -221,10 +221,15 @@ export function renderComponentRoot(
getComponentName(instance.type), getComponentName(instance.type),
) )
} }
root = cloneVNode(root, { root = cloneVNode(
root,
{
class: cls, class: cls,
style: style, style: style,
}) },
false,
true,
)
} }
} }
@ -237,7 +242,7 @@ export function renderComponentRoot(
) )
} }
// clone before mutating since the root may be a hoisted vnode // clone before mutating since the root may be a hoisted vnode
root = cloneVNode(root) root = cloneVNode(root, null, false, true)
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
} }
// inherit transition data // inherit transition data

View File

@ -624,10 +624,11 @@ export function cloneVNode<T, U>(
vnode: VNode<T, U>, vnode: VNode<T, U>,
extraProps?: (Data & VNodeProps) | null, extraProps?: (Data & VNodeProps) | null,
mergeRef = false, mergeRef = false,
cloneTransition = false,
): VNode<T, U> { ): VNode<T, U> {
// This is intentionally NOT using spread or extend to avoid the runtime // This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost. // key enumeration cost.
const { props, ref, patchFlag, children } = vnode const { props, ref, patchFlag, children, transition } = vnode
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
const cloned: VNode<T, U> = { const cloned: VNode<T, U> = {
__v_isVNode: true, __v_isVNode: true,
@ -670,7 +671,7 @@ export function cloneVNode<T, U>(
dynamicChildren: vnode.dynamicChildren, dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext, appContext: vnode.appContext,
dirs: vnode.dirs, dirs: vnode.dirs,
transition: vnode.transition, transition,
// These should technically only be non-null on mounted VNodes. However, // These should technically only be non-null on mounted VNodes. However,
// they *should* be copied for kept-alive vnodes. So we just always copy // they *should* be copied for kept-alive vnodes. So we just always copy
@ -685,9 +686,18 @@ export function cloneVNode<T, U>(
ctx: vnode.ctx, ctx: vnode.ctx,
ce: vnode.ce, ce: vnode.ce,
} }
// if the vnode will be replaced by the cloned one, it is necessary
// to clone the transition to ensure that the vnode referenced within
// the transition hooks is fresh.
if (transition && cloneTransition) {
cloned.transition = transition.clone(cloned as VNode)
}
if (__COMPAT__) { if (__COMPAT__) {
defineLegacyVNodeProperties(cloned as VNode) defineLegacyVNodeProperties(cloned as VNode)
} }
return cloned return cloned
} }

View File

@ -1215,6 +1215,54 @@ describe('e2e: Transition', () => {
E2E_TIMEOUT, E2E_TIMEOUT,
) )
// #3716
test(
'wrapping transition + fallthrough attrs',
async () => {
await page().goto(baseUrl)
await page().waitForSelector('#app')
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
components: {
'my-transition': {
template: `
<transition foo="1" name="test">
<slot></slot>
</transition>
`,
},
},
template: `
<div id="container">
<my-transition>
<div v-if="toggle">content</div>
</my-transition>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
setup: () => {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})
expect(await html('#container')).toBe('<div foo="1">content</div>')
await click('#toggleBtn')
// toggle again before leave finishes
await nextTick()
await click('#toggleBtn')
await transitionFinish()
expect(await html('#container')).toBe(
'<div foo="1" class="">content</div>',
)
},
E2E_TIMEOUT,
)
test( test(
'w/ KeepAlive + unmount innerChild', 'w/ KeepAlive + unmount innerChild',
async () => { async () => {