expect(node.arguments).toMatchObject([
KEEP_ALIVE,
`null`,
- createObjectMatcher({
- default: {
- type: NodeTypes.JS_FUNCTION_EXPRESSION
- },
- _compiled: `[true]`
- })
+ // keep-alive should not compile content to slots
+ [{ type: NodeTypes.ELEMENT, tag: 'span' }]
])
}
if (!hasProps) {
args.push(`null`)
}
- // Portal should have normal children instead of slots
- if (isComponent && !isPortal) {
+ // Portal & KeepAlive should have normal children instead of slots
+ // Portal is not a real component has dedicated handling in the renderer
+ // KeepAlive should not track its own deps so that it can be used inside
+ // Transition
+ if (isComponent && !isPortal && !isKeepAlive) {
const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots)
if (hasDynamicSlots) {
if (
__DEV__ &&
!(result.shapeFlag & ShapeFlags.COMPONENT) &&
- !(result.shapeFlag & ShapeFlags.ELEMENT)
+ !(result.shapeFlag & ShapeFlags.ELEMENT) &&
+ result.type !== Comment
) {
warn(
`Component inside <Transition> renders non-element root node ` +
import { ComponentInternalInstance, currentInstance } from './component'
import { VNode, NormalizedChildren, normalizeVNode, VNodeChild } from './vnode'
-import { isArray, isFunction } from '@vue/shared'
+import { isArray, isFunction, EMPTY_OBJ } from '@vue/shared'
import { ShapeFlags } from './shapeFlags'
import { warn } from './warning'
+import { isKeepAlive } from './components/KeepAlive'
export type Slot = (...args: any[]) => VNode[]
}
} else if (children !== null) {
// non slot object children (direct value) passed to a component
- if (__DEV__) {
+ if (__DEV__ && !isKeepAlive(instance.vnode)) {
warn(
`Non-function value encountered for default slot. ` +
`Prefer function slots for better performance.`
const normalized = normalizeSlotValue(children)
slots = { default: () => normalized }
}
- if (slots !== void 0) {
- instance.slots = slots
- }
+ instance.slots = slots || EMPTY_OBJ
}
SetupContext,
ComponentOptions
} from '../component'
-import { cloneVNode, Comment, isSameVNodeType, VNode } from '../vnode'
+import {
+ cloneVNode,
+ Comment,
+ isSameVNodeType,
+ VNode,
+ VNodeChildren
+} from '../vnode'
import { warn } from '../warning'
import { isKeepAlive } from './KeepAlive'
import { toRaw } from '@vue/reactivity'
onLeaveCancelled?: (el: any) => void
}
+export interface TransitionHooks {
+ persisted: boolean
+ beforeEnter(el: object): void
+ enter(el: object): void
+ leave(el: object, remove: () => void): void
+ afterLeave?(): void
+ delayLeave?(delayedLeave: () => void): void
+ delayedLeave?(): void
+}
+
type TransitionHookCaller = (
hook: ((el: any) => void) | undefined,
args?: any[]
) => void
+type PendingCallback = (cancelled?: boolean) => void
+
interface TransitionState {
isMounted: boolean
isLeaving: boolean
isUnmounting: boolean
- pendingEnter?: (cancelled?: boolean) => void
- pendingLeave?: (cancelled?: boolean) => void
+ // Track pending leave callbacks for children of the same key.
+ // This is used to force remove leaving a child when a new copy is entering.
+ leavingVNodes: Record<string, VNode>
+}
+
+interface TransitionElement {
+ // in persisted mode (e.g. v-show), the same element is toggled, so the
+ // pending enter/leave callbacks may need to cancalled if the state is toggled
+ // before it finishes.
+ _enterCb?: PendingCallback
+ _leaveCb?: PendingCallback
}
const BaseTransitionImpl = {
const state: TransitionState = {
isMounted: false,
isLeaving: false,
- isUnmounting: false
+ isUnmounting: false,
+ leavingVNodes: Object.create(null)
}
onMounted(() => {
state.isMounted = true
// warn multiple elements
if (__DEV__ && children.length > 1) {
warn(
- '<transition> can only be used on a single element. Use ' +
+ '<transition> can only be used on a single element or component. Use ' +
'<transition-group> for lists.'
)
}
// at this point children has a guaranteed length of 1.
const child = children[0]
if (state.isLeaving) {
- return placeholder(child)
+ return emptyPlaceholder(child)
}
- let delayedLeave: (() => void) | undefined
- const performDelayedLeave = () => delayedLeave && delayedLeave()
+ // in the case of <transition><keep-alive/></transition>, we need to
+ // compare the type of the kept-alive children.
+ const innerChild = getKeepAliveChild(child)
+ if (!innerChild) {
+ return emptyPlaceholder(child)
+ }
- const transitionHooks = (child.transition = resolveTransitionHooks(
+ const enterHooks = (innerChild.transition = resolveTransitionHooks(
+ innerChild,
rawProps,
state,
- callTransitionHook,
- performDelayedLeave
+ callTransitionHook
))
- // clone old subTree because we need to modify it
const oldChild = instance.subTree
- ? (instance.subTree = cloneVNode(instance.subTree))
- : null
-
+ const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
// handle mode
if (
- oldChild &&
- !isSameVNodeType(child, oldChild) &&
- oldChild.type !== Comment
+ oldInnerChild &&
+ oldInnerChild.type !== Comment &&
+ !isSameVNodeType(innerChild, oldInnerChild)
) {
+ const prevHooks = oldInnerChild.transition!
+ const leavingHooks = resolveTransitionHooks(
+ oldInnerChild,
+ rawProps,
+ state,
+ callTransitionHook
+ )
// update old tree's hooks in case of dynamic transition
- // need to do this recursively in case of HOCs
- updateHOCTransitionData(oldChild, transitionHooks)
+ setTransitionHooks(oldInnerChild, leavingHooks)
// switching between different views
if (mode === 'out-in') {
state.isLeaving = true
// return placeholder node and queue update when leave finishes
- transitionHooks.afterLeave = () => {
+ leavingHooks.afterLeave = () => {
state.isLeaving = false
instance.update()
}
- return placeholder(child)
+ return emptyPlaceholder(child)
} else if (mode === 'in-out') {
- transitionHooks.delayLeave = performLeave => {
- delayedLeave = performLeave
+ delete prevHooks.delayedLeave
+ leavingHooks.delayLeave = delayedLeave => {
+ enterHooks.delayedLeave = delayedLeave
}
}
}
}
}
-export interface TransitionHooks {
- persisted: boolean
- beforeEnter(el: object): void
- enter(el: object): void
- leave(el: object, remove: () => void): void
- afterLeave?(): void
- delayLeave?(performLeave: () => void): void
-}
-
// The transition hooks are attached to the vnode as vnode.transition
// and will be called at appropriate timing in the renderer.
function resolveTransitionHooks(
+ vnode: VNode,
{
appear,
persisted = false,
onLeaveCancelled
}: BaseTransitionProps,
state: TransitionState,
- callHook: TransitionHookCaller,
- performDelayedLeave: () => void
+ callHook: TransitionHookCaller
): TransitionHooks {
- return {
+ const { leavingVNodes } = state
+ const key = String(vnode.key)
+
+ const hooks: TransitionHooks = {
persisted,
- beforeEnter(el) {
- if (state.pendingLeave) {
- state.pendingLeave(true /* cancelled */)
- }
+ beforeEnter(el: TransitionElement) {
if (!appear && !state.isMounted) {
return
}
+ // for same element (v-show)
+ if (el._leaveCb) {
+ el._leaveCb(true /* cancelled */)
+ }
+ // for toggled element with same key (v-if)
+ const leavingVNode = leavingVNodes[key]
+ if (
+ leavingVNode &&
+ isSameVNodeType(vnode, leavingVNode) &&
+ leavingVNode.el._leaveCb
+ ) {
+ // force early removal (not cancelled)
+ leavingVNode.el._leaveCb()
+ }
callHook(onBeforeEnter, [el])
},
- enter(el) {
+ enter(el: TransitionElement) {
if (!appear && !state.isMounted) {
return
}
let called = false
- const afterEnter = (state.pendingEnter = (cancelled?) => {
+ const afterEnter = (el._enterCb = (cancelled?) => {
if (called) return
called = true
if (cancelled) {
callHook(onEnterCancelled, [el])
} else {
callHook(onAfterEnter, [el])
- performDelayedLeave()
}
- state.pendingEnter = undefined
+ if (hooks.delayedLeave) {
+ hooks.delayedLeave()
+ }
+ el._enterCb = undefined
})
if (onEnter) {
onEnter(el, afterEnter)
}
},
- leave(el, remove) {
- if (state.pendingEnter) {
- state.pendingEnter(true /* cancelled */)
+ leave(el: TransitionElement, remove) {
+ const key = String(vnode.key)
+ if (el._enterCb) {
+ el._enterCb(true /* cancelled */)
}
if (state.isUnmounting) {
return remove()
}
callHook(onBeforeLeave, [el])
let called = false
- const afterLeave = (state.pendingLeave = (cancelled?) => {
+ const afterLeave = (el._leaveCb = (cancelled?) => {
if (called) return
called = true
remove()
} else {
callHook(onAfterLeave, [el])
}
- state.pendingLeave = undefined
+ el._leaveCb = undefined
+ delete leavingVNodes[key]
})
+ leavingVNodes[key] = vnode
if (onLeave) {
onLeave(el, afterLeave)
} else {
}
}
}
+
+ return hooks
}
// the placeholder really only handles one special case: KeepAlive
// in the case of a KeepAlive in a leave phase we need to return a KeepAlive
// placeholder with empty content to avoid the KeepAlive instance from being
// unmounted.
-function placeholder(vnode: VNode): VNode | undefined {
+function emptyPlaceholder(vnode: VNode): VNode | undefined {
if (isKeepAlive(vnode)) {
vnode = cloneVNode(vnode)
vnode.children = null
}
}
-function updateHOCTransitionData(vnode: VNode, data: TransitionHooks) {
- if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
- updateHOCTransitionData(vnode.component!.subTree, data)
+function getKeepAliveChild(vnode: VNode): VNode | undefined {
+ return isKeepAlive(vnode)
+ ? vnode.children
+ ? ((vnode.children as VNodeChildren)[0] as VNode)
+ : undefined
+ : vnode
+}
+
+export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks) {
+ if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
+ setTransitionHooks(vnode.component.subTree, hooks)
} else {
- vnode.transition = data
+ vnode.transition = hooks
}
}
import {
RendererInternals,
queuePostRenderEffect,
- invokeHooks
+ invokeHooks,
+ MoveType
} from '../renderer'
+import { setTransitionHooks } from './BaseTransition'
type MatchPattern = string | RegExp | string[] | RegExp[]
const storageContainer = createElement('div')
sink.activate = (vnode, container, anchor) => {
- move(vnode, container, anchor)
+ move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
queuePostRenderEffect(() => {
const component = vnode.component!
component.isDeactivated = false
}
sink.deactivate = (vnode: VNode) => {
- move(vnode, storageContainer, null)
+ move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
const component = vnode.component!
if (component.da !== null) {
vnode.el = cached.el
vnode.anchor = cached.anchor
vnode.component = cached.component
+ if (vnode.transition) {
+ // recursively update transition hooks on subTree
+ setTransitionHooks(vnode, vnode.transition!)
+ }
// avoid vnode being mounted as fresh
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
import { isFunction, isArray } from '@vue/shared'
import { ComponentInternalInstance, handleSetupResult } from '../component'
import { Slots } from '../componentSlots'
-import { RendererInternals } from '../renderer'
+import { RendererInternals, MoveType } from '../renderer'
import { queuePostFlushCb, queueJob } from '../scheduler'
import { updateHOCHostEl } from '../componentRenderUtils'
import { handleError, ErrorCodes } from '../errorHandling'
effects: Function[]
resolve(): void
recede(): void
- move(container: HostElement, anchor: HostNode | null): void
+ move(container: HostElement, anchor: HostNode | null, type: MoveType): void
next(): HostNode | null
registerDep(
instance: ComponentInternalInstance,
unmount(fallbackTree as VNode, parentComponent, suspense, true)
}
// move content from off-dom container to actual container
- move(subTree as VNode, container, anchor)
+ move(subTree as VNode, container, anchor, MoveType.ENTER)
const el = (vnode.el = (subTree as VNode).el!)
// suspense as the root node of a component...
if (parentComponent && parentComponent.subTree === vnode) {
// move content tree back to the off-dom container
const anchor = next(subTree)
- move(subTree as VNode, hiddenContainer, null)
+ move(subTree as VNode, hiddenContainer, null, MoveType.LEAVE)
// remount the fallback tree
patch(
null,
}
},
- move(container, anchor) {
+ move(container, anchor, type) {
move(
suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
container,
- anchor
+ anchor,
+ type
)
suspense.container = container
},
move: (
vnode: VNode<HostNode, HostElement>,
container: HostElement,
- anchor: HostNode | null
+ anchor: HostNode | null,
+ type: MoveType,
+ parentSuspense?: SuspenseBoundary<HostNode, HostElement> | null
) => void
next: (vnode: VNode<HostNode, HostElement>) => HostNode | null
options: RendererOptions<HostNode, HostElement>
}
+export const enum MoveType {
+ ENTER,
+ LEAVE,
+ REORDER
+}
+
const prodEffectOptions = {
scheduler: queueJob
}
invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
}
}
- if (transition != null && !transition.persisted) {
- transition.beforeEnter(el)
- }
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
optimized || vnode.dynamicChildren !== null
)
}
+ if (transition != null && !transition.persisted) {
+ transition.beforeEnter(el)
+ }
hostInsert(el, container, anchor)
const vnodeMountedHook = props && props.onVnodeMounted
if (
hostSetElementText(nextTarget, children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
for (let i = 0; i < (children as HostVNode[]).length; i++) {
- move((children as HostVNode[])[i], nextTarget, null)
+ move(
+ (children as HostVNode[])[i],
+ nextTarget,
+ null,
+ MoveType.REORDER
+ )
}
}
} else if (__DEV__) {
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
- move(nextChild, container, anchor)
+ move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
function move(
vnode: HostVNode,
container: HostElement,
- anchor: HostNode | null
+ anchor: HostNode | null,
+ type: MoveType,
+ parentSuspense: HostSuspenseBoundary | null = null
) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
- move(vnode.component!.subTree, container, anchor)
+ move(vnode.component!.subTree, container, anchor, type)
return
}
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
- vnode.suspense!.move(container, anchor)
+ vnode.suspense!.move(container, anchor, type)
return
}
if (vnode.type === Fragment) {
hostInsert(vnode.el!, container, anchor)
const children = vnode.children as HostVNode[]
for (let i = 0; i < children.length; i++) {
- move(children[i], container, anchor)
+ move(children[i], container, anchor, type)
}
hostInsert(vnode.anchor!, container, anchor)
} else {
- hostInsert(vnode.el!, container, anchor)
+ // Plain element
+ const { el, transition, shapeFlag } = vnode
+ const needTransition =
+ type !== MoveType.REORDER &&
+ shapeFlag & ShapeFlags.ELEMENT &&
+ transition != null
+ if (needTransition) {
+ if (type === MoveType.ENTER) {
+ transition!.beforeEnter(el!)
+ hostInsert(el!, container, anchor)
+ queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
+ } else {
+ const { leave, delayLeave, afterLeave } = transition!
+ const performLeave = () => {
+ leave(el!, () => {
+ hostInsert(el!, container, anchor)
+ afterLeave && afterLeave()
+ })
+ }
+ if (delayLeave) {
+ delayLeave(performLeave)
+ } else {
+ performLeave()
+ }
+ }
+ } else {
+ hostInsert(el!, container, anchor)
+ }
}
}