deactivate: (vnode: VNode) => void
}
+export const isKeepAlive = (vnode: VNode): boolean =>
+ (vnode.type as any).__isKeepAlive
+
const KeepAliveImpl = {
name: `KeepAlive`,
// would prevent it from being tree-shaken.
__isKeepAlive: true,
+ props: {
+ include: [String, RegExp, Array],
+ exclude: [String, RegExp, Array],
+ max: [String, Number]
+ },
+
setup(props: KeepAliveProps, { slots }: SetupContext) {
const cache: Cache = new Map()
const keys: Keys = new Set()
}
}
-if (__DEV__) {
- ;(KeepAliveImpl as any).props = {
- include: [String, RegExp, Array],
- exclude: [String, RegExp, Array],
- max: [String, Number]
- }
-}
-
// export the public type for h/tsx inference
export const KeepAlive = (KeepAliveImpl as any) as {
new (): {
--- /dev/null
+import { createComponent } from '../apiCreateComponent'
+import { getCurrentInstance } from '../component'
+import {
+ cloneVNode,
+ Comment,
+ isSameVNodeType,
+ VNodeProps,
+ VNode,
+ mergeProps
+} from '../vnode'
+import { warn } from '../warning'
+import { isKeepAlive } from './KeepAlive'
+import { toRaw } from '@vue/reactivity'
+import { onMounted } from '../apiLifecycle'
+
+// Using camel case here makes it easier to use in render functions & JSX.
+// In templates these will be written as @before-enter="xxx"
+// The compiler has special handling to convert them into the proper cases.
+export interface TransitionProps {
+ mode?: 'in-out' | 'out-in' | 'default'
+ appear?: boolean
+ // enter
+ onBeforeEnter?: (el: any) => void
+ onEnter?: (el: any, done: () => void) => void
+ onAfterEnter?: (el: any) => void
+ onEnterCancelled?: (el: any) => void
+ // leave
+ onBeforeLeave?: (el: any) => void
+ onLeave?: (el: any, done: () => void) => void
+ onAfterLeave?: (el: any) => void
+ onLeaveCancelled?: (el: any) => void
+}
+
+export const Transition = createComponent({
+ name: `Transition`,
+ setup(props: TransitionProps, { slots }) {
+ const instance = getCurrentInstance()!
+ let isLeaving = false
+ let isMounted = false
+
+ onMounted(() => {
+ isMounted = true
+ })
+
+ return () => {
+ const children = slots.default && slots.default()
+ if (!children || !children.length) {
+ return
+ }
+
+ // warn multiple elements
+ if (__DEV__ && children.length > 1) {
+ warn(
+ '<transition> can only be used on a single element. Use ' +
+ '<transition-group> for lists.'
+ )
+ }
+
+ // there's no need to track reactivity for these props so use the raw
+ // props for a bit better perf
+ const rawProps = toRaw(props)
+ const { mode } = rawProps
+ // check mode
+ if (__DEV__ && mode && !['in-out', 'out-in', 'default'].includes(mode)) {
+ warn(`invalid <transition> mode: ${mode}`)
+ }
+
+ // at this point children has a guaranteed length of 1.
+ const rawChild = children[0]
+ if (isLeaving) {
+ return placeholder(rawChild)
+ }
+
+ rawChild.transition = rawProps
+ // clone old subTree because we need to modify it
+ const oldChild = instance.subTree
+ ? (instance.subTree = cloneVNode(instance.subTree))
+ : null
+
+ // handle mode
+ let performDelayedLeave: (() => void) | undefined
+ if (
+ oldChild &&
+ !isSameVNodeType(rawChild, oldChild) &&
+ oldChild.type !== Comment
+ ) {
+ // update old tree's hooks in case of dynamic transition
+ oldChild.transition = rawProps
+ // switching between different views
+ if (mode === 'out-in') {
+ isLeaving = true
+ // return placeholder node and queue update when leave finishes
+ oldChild.props = mergeProps(oldChild.props!, {
+ onVnodeRemoved() {
+ isLeaving = false
+ instance.update()
+ }
+ })
+ return placeholder(rawChild)
+ } else if (mode === 'in-out') {
+ let delayedLeave: () => void
+ performDelayedLeave = () => delayedLeave()
+ oldChild.props = mergeProps(oldChild.props!, {
+ onVnodeDelayLeave(performLeave) {
+ delayedLeave = performLeave
+ }
+ })
+ }
+ }
+
+ return cloneVNode(
+ rawChild,
+ resolveTransitionInjections(rawProps, isMounted, performDelayedLeave)
+ )
+ }
+ }
+})
+
+if (__DEV__) {
+ ;(Transition as any).props = {
+ mode: String,
+ appear: Boolean,
+ // enter
+ onBeforeEnter: Function,
+ onEnter: Function,
+ onAfterEnter: Function,
+ onEnterCancelled: Function,
+ // leave
+ onBeforeLeave: Function,
+ onLeave: Function,
+ onAfterLeave: Function,
+ onLeaveCancelled: Function
+ }
+}
+
+function resolveTransitionInjections(
+ {
+ appear,
+ onBeforeEnter,
+ onEnter,
+ onAfterEnter,
+ onEnterCancelled,
+ onBeforeLeave,
+ onLeave,
+ onAfterLeave,
+ onLeaveCancelled
+ }: TransitionProps,
+ isMounted: boolean,
+ performDelayedLeave?: () => void
+): VNodeProps {
+ // TODO handle appear
+ // TODO handle cancel hooks
+ return {
+ onVnodeBeforeMount(vnode) {
+ if (!isMounted && !appear) {
+ return
+ }
+ onBeforeEnter && onBeforeEnter(vnode.el)
+ },
+ onVnodeMounted({ el }) {
+ if (!isMounted && !appear) {
+ return
+ }
+ const done = () => {
+ onAfterEnter && onAfterEnter(el)
+ performDelayedLeave && performDelayedLeave()
+ }
+ if (onEnter) {
+ onEnter(el, done)
+ } else {
+ done()
+ }
+ },
+ onVnodeBeforeRemove({ el }, remove) {
+ onBeforeLeave && onBeforeLeave(el)
+ if (onLeave) {
+ onLeave(el, () => {
+ remove()
+ onAfterLeave && onAfterLeave(el)
+ })
+ } else {
+ remove()
+ onAfterLeave && onAfterLeave(el)
+ }
+ }
+ }
+}
+
+// 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 {
+ if (isKeepAlive(vnode)) {
+ vnode = cloneVNode(vnode)
+ vnode.children = null
+ return vnode
+ }
+}
}
export function invokeDirectiveHook(
- hook: Function | Function[],
+ hook: ((...args: any[]) => any) | ((...args: any[]) => any)[],
instance: ComponentInternalInstance | null,
vnode: VNode,
prevVNode: VNode | null = null
// Internal Components
export { Suspense, SuspenseProps } from './components/Suspense'
export { KeepAlive, KeepAliveProps } from './components/KeepAlive'
+export { Transition, TransitionProps } from './components/Transition'
// VNode flags
export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
import { PublicPatchFlags } from '@vue/shared'
normalizeVNode,
VNode,
VNodeChildren,
- createVNode
+ createVNode,
+ isSameVNodeType
} from './vnode'
import {
ComponentInternalInstance,
EMPTY_ARR,
isReservedProp,
isFunction,
- PatchFlags
+ PatchFlags,
+ isArray
} from '@vue/shared'
import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler'
import {
queueEffectWithSuspense,
SuspenseImpl
} from './components/Suspense'
-import { ErrorCodes, callWithErrorHandling } from './errorHandling'
-import { KeepAliveSink } from './components/KeepAlive'
+import {
+ ErrorCodes,
+ callWithErrorHandling,
+ callWithAsyncErrorHandling
+} from './errorHandling'
+import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
export interface RendererOptions<HostNode = any, HostElement = any> {
patchProp(
}
}
-function isSameType(n1: VNode, n2: VNode): boolean {
- return n1.type === n2.type && n1.key === n2.key
-}
-
export function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
for (let i = 0; i < hooks.length; i++) {
hooks[i](arg)
optimized: boolean = false
) {
// patching & not same type, unmount old tree
- if (n1 != null && !isSameType(n1, n2)) {
+ if (n1 != null && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
hostInsert(el, container, anchor)
if (props != null && props.onVnodeMounted != null) {
queuePostRenderEffect(() => {
- invokeDirectiveHook(props.onVnodeMounted, parentComponent, vnode)
+ invokeDirectiveHook(props.onVnodeMounted!, parentComponent, vnode)
}, parentSuspense)
}
}
const Comp = initialVNode.type as Component
// inject renderer internals for keepAlive
- if ((Comp as any).__isKeepAlive) {
+ if (isKeepAlive(initialVNode)) {
const sink = instance.sink as KeepAliveSink
sink.renderer = internals
sink.parentSuspense = parentSuspense
if (next !== null) {
updateComponentPreRender(instance, next)
}
+ const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
- const nextTree = (instance.subTree = renderComponentRoot(instance))
+ instance.subTree = nextTree
// beforeUpdate hook
if (instance.bu !== null) {
invokeHooks(instance.bu)
const n2 = optimized
? (c2[i] as HostVNode)
: (c2[i] = normalizeVNode(c2[i]))
- if (isSameType(n1, n2)) {
+ if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
const n2 = optimized
? (c2[i] as HostVNode)
: (c2[e2] = normalizeVNode(c2[e2]))
- if (isSameType(n1, n2)) {
+ if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
- isSameType(prevChild, c2[j] as HostVNode)
+ isSameVNodeType(prevChild, c2[j] as HostVNode)
) {
newIndex = j
break
}
if (doRemove) {
- hostRemove(vnode.el!)
- if (anchor != null) hostRemove(anchor)
+ const beforeRemoveHooks = props && props.onVnodeBeforeRemove
+ const remove = () => {
+ hostRemove(vnode.el!)
+ if (anchor != null) hostRemove(anchor)
+ const removedHook = props && props.onVnodeRemoved
+ removedHook && removedHook()
+ }
+ if (vnode.shapeFlag & ShapeFlags.ELEMENT && beforeRemoveHooks != null) {
+ const delayLeave = props && props.onVnodeDelayLeave
+ const performLeave = () => {
+ invokeBeforeRemoveHooks(
+ beforeRemoveHooks,
+ parentComponent,
+ vnode,
+ remove
+ )
+ }
+ if (delayLeave) {
+ delayLeave(performLeave)
+ } else {
+ performLeave()
+ }
+ } else {
+ remove()
+ }
}
if (props != null && props.onVnodeUnmounted != null) {
queuePostRenderEffect(() => {
- invokeDirectiveHook(props.onVnodeUnmounted, parentComponent, vnode)
+ invokeDirectiveHook(props.onVnodeUnmounted!, parentComponent, vnode)
}, parentSuspense)
}
}
+ function invokeBeforeRemoveHooks(
+ hooks: ((...args: any[]) => any) | ((...args: any[]) => any)[],
+ instance: ComponentInternalInstance | null,
+ vnode: HostVNode,
+ done: () => void
+ ) {
+ if (!isArray(hooks)) {
+ hooks = [hooks]
+ }
+ let delayedRemoveCount = hooks.length
+ const doneRemove = () => {
+ delayedRemoveCount--
+ if (allHooksCalled && !delayedRemoveCount) {
+ done()
+ }
+ }
+ let allHooksCalled = false
+ for (let i = 0; i < hooks.length; i++) {
+ callWithAsyncErrorHandling(
+ hooks[i],
+ instance,
+ ErrorCodes.DIRECTIVE_HOOK,
+ [vnode, doneRemove]
+ )
+ }
+ allHooksCalled = true
+ if (!delayedRemoveCount) {
+ done()
+ }
+ }
+
function unmountComponent(
instance: ComponentInternalInstance,
parentSuspense: HostSuspenseBoundary | null,
import { SuspenseBoundary } from './components/Suspense'
import { DirectiveBinding } from './directives'
import { SuspenseImpl } from './components/Suspense'
+import { TransitionProps } from './components/Transition'
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
__isFragment: true
[key: string]: any
key?: string | number
ref?: string | Ref | ((ref: object | null) => void)
+
+ // vnode hooks
+ onVnodeBeforeMount?: (vnode: VNode) => void
+ onVnodeMounted?: (vnode: VNode) => void
+ onVnodeBeforeUpdate?: (vnode: VNode, oldVNode: VNode) => void
+ onVnodeUpdated?: (vnode: VNode, oldVNode: VNode) => void
+ onVnodeBeforeUnmount?: (vnode: VNode) => void
+ onVnodeUnmounted?: (vnode: VNode) => void
+
+ // transition hooks, internal.
+ onVnodeDelayLeave?: (performLeave: () => void) => void
+ onVnodeBeforeRemove?: (vnode: VNode, remove: () => void) => void
+ onVnodeRemoved?: () => void
}
type VNodeChildAtom<HostNode, HostElement> =
type: VNodeTypes
props: VNodeProps | null
key: string | number | null
- ref: string | Function | null
+ ref: string | Ref | ((ref: object | null) => void) | null
children: NormalizedChildren<HostNode, HostElement>
component: ComponentInternalInstance | null
suspense: SuspenseBoundary<HostNode, HostElement> | null
dirs: DirectiveBinding[] | null
+ transition: TransitionProps | null
// DOM
el: HostNode | null
return value ? value._isVNode === true : false
}
+export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
+ return n1.type === n2.type && n1.key === n2.key
+}
+
export function createVNode(
type: VNodeTypes,
- props: { [key: string]: any } | null = null,
+ props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null
component: null,
suspense: null,
dirs: null,
+ transition: null,
el: null,
anchor: null,
target: null,
export function cloneVNode<T, U>(
vnode: VNode<T, U>,
- extraProps?: Data
+ extraProps?: Data & VNodeProps
): VNode<T, U> {
// This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost.
dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext,
dirs: vnode.dirs,
+ transition: vnode.transition,
// These should technically only be non-null on mounted VNodes. However,
// they *should* be copied for kept-alive vnodes. So we just always copy
const handlersRE = /^on|^vnode/
-export function mergeProps(...args: Data[]) {
+export function mergeProps(...args: (Data & VNodeProps)[]) {
const ret: Data = {}
extend(ret, args[0])
for (let i = 1; i < args.length; i++) {