feat(transition): support component child

This commit is contained in:
Evan You 2019-11-20 21:56:17 -05:00
parent 93561b080e
commit 79f23a2f77
6 changed files with 114 additions and 111 deletions

View File

@ -8,7 +8,6 @@ import {
isFunction, isFunction,
isArray, isArray,
isObject, isObject,
isReservedProp,
hasOwn, hasOwn,
toRawType, toRawType,
PatchFlags, PatchFlags,
@ -122,8 +121,8 @@ export function resolveProps(
if (rawProps != null) { if (rawProps != null) {
for (const key in rawProps) { for (const key in rawProps) {
// key, ref are reserved // key, ref are reserved and never passed down
if (isReservedProp(key)) continue if (key === 'key' || key === 'ref') continue
// prop option names are camelized during normalization, so to support // prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key. // kebab -> camel conversion here we need to camelize the key.
const camelKey = camelize(key) const camelKey = camelize(key)

View File

@ -85,6 +85,12 @@ export function renderComponentRoot(
) )
} }
} }
// inherit transition data
if (vnode.transition != null) {
// TODO warn if component has transition data but root is a fragment
result.transition = vnode.transition
}
} catch (err) { } catch (err) {
handleError(err, instance, ErrorCodes.RENDER_FUNCTION) handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
result = createVNode(Comment) result = createVNode(Comment)

View File

@ -1,17 +1,12 @@
import { createComponent } from '../apiCreateComponent' import { createComponent } from '../apiCreateComponent'
import { getCurrentInstance } from '../component' import { getCurrentInstance, ComponentInternalInstance } from '../component'
import { import { cloneVNode, Comment, isSameVNodeType, VNode } from '../vnode'
cloneVNode,
Comment,
isSameVNodeType,
VNodeProps,
VNode,
mergeProps
} from '../vnode'
import { warn } from '../warning' import { warn } from '../warning'
import { isKeepAlive } from './KeepAlive' import { isKeepAlive } from './KeepAlive'
import { toRaw } from '@vue/reactivity' import { toRaw } from '@vue/reactivity'
import { onMounted } from '../apiLifecycle' import { onMounted } from '../apiLifecycle'
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
import { ShapeFlags } from '../shapeFlags'
// Using camel case here makes it easier to use in render functions & JSX. // Using camel case here makes it easier to use in render functions & JSX.
// In templates these will be written as @before-enter="xxx" // In templates these will be written as @before-enter="xxx"
@ -66,52 +61,51 @@ export const Transition = createComponent({
} }
// at this point children has a guaranteed length of 1. // at this point children has a guaranteed length of 1.
const rawChild = children[0] const child = children[0]
if (isLeaving) { if (isLeaving) {
return placeholder(rawChild) return placeholder(child)
} }
rawChild.transition = rawProps let delayedLeave: (() => void) | undefined
const performDelayedLeave = () => delayedLeave && delayedLeave()
const transitionData = (child.transition = resolveTransitionData(
instance,
rawProps,
isMounted,
performDelayedLeave
))
// clone old subTree because we need to modify it // clone old subTree because we need to modify it
const oldChild = instance.subTree const oldChild = instance.subTree
? (instance.subTree = cloneVNode(instance.subTree)) ? (instance.subTree = cloneVNode(instance.subTree))
: null : null
// handle mode // handle mode
let performDelayedLeave: (() => void) | undefined
if ( if (
oldChild && oldChild &&
!isSameVNodeType(rawChild, oldChild) && !isSameVNodeType(child, oldChild) &&
oldChild.type !== Comment oldChild.type !== Comment
) { ) {
// update old tree's hooks in case of dynamic transition // update old tree's hooks in case of dynamic transition
oldChild.transition = rawProps // need to do this recursively in case of HOCs
updateHOCTransitionData(oldChild, transitionData)
// switching between different views // switching between different views
if (mode === 'out-in') { if (mode === 'out-in') {
isLeaving = true isLeaving = true
// return placeholder node and queue update when leave finishes // return placeholder node and queue update when leave finishes
oldChild.props = mergeProps(oldChild.props!, { transitionData.afterLeave = () => {
onVnodeRemoved() { isLeaving = false
isLeaving = false instance.update()
instance.update() }
} return placeholder(child)
})
return placeholder(rawChild)
} else if (mode === 'in-out') { } else if (mode === 'in-out') {
let delayedLeave: () => void transitionData.delayLeave = performLeave => {
performDelayedLeave = () => delayedLeave() delayedLeave = performLeave
oldChild.props = mergeProps(oldChild.props!, { }
onVnodeDelayLeave(performLeave) {
delayedLeave = performLeave
}
})
} }
} }
return cloneVNode( return child
rawChild,
resolveTransitionInjections(rawProps, isMounted, performDelayedLeave)
)
} }
} }
}) })
@ -133,7 +127,16 @@ if (__DEV__) {
} }
} }
function resolveTransitionInjections( export interface TransitionData {
beforeEnter(el: object): void
enter(el: object): void
leave(el: object, remove: () => void): void
afterLeave?(): void
delayLeave?(performLeave: () => void): void
}
function resolveTransitionData(
instance: ComponentInternalInstance,
{ {
appear, appear,
onBeforeEnter, onBeforeEnter,
@ -146,24 +149,35 @@ function resolveTransitionInjections(
onLeaveCancelled onLeaveCancelled
}: TransitionProps, }: TransitionProps,
isMounted: boolean, isMounted: boolean,
performDelayedLeave?: () => void performDelayedLeave: () => void
): VNodeProps { ): TransitionData {
// TODO handle appear
// TODO handle cancel hooks // TODO handle cancel hooks
return { return {
onVnodeBeforeMount(vnode) { beforeEnter(el) {
if (!isMounted && !appear) { if (!isMounted && !appear) {
return return
} }
onBeforeEnter && onBeforeEnter(vnode.el) onBeforeEnter &&
callWithAsyncErrorHandling(
onBeforeEnter,
instance,
ErrorCodes.TRANSITION_HOOK,
[el]
)
}, },
onVnodeMounted({ el }) { enter(el) {
if (!isMounted && !appear) { if (!isMounted && !appear) {
return return
} }
const done = () => { const done = () => {
onAfterEnter && onAfterEnter(el) onAfterEnter &&
performDelayedLeave && performDelayedLeave() callWithAsyncErrorHandling(
onAfterEnter,
instance,
ErrorCodes.TRANSITION_HOOK,
[el]
)
performDelayedLeave()
} }
if (onEnter) { if (onEnter) {
onEnter(el, done) onEnter(el, done)
@ -171,16 +185,30 @@ function resolveTransitionInjections(
done() done()
} }
}, },
onVnodeBeforeRemove({ el }, remove) { leave(el, remove) {
onBeforeLeave && onBeforeLeave(el) onBeforeLeave &&
callWithAsyncErrorHandling(
onBeforeLeave,
instance,
ErrorCodes.TRANSITION_HOOK,
[el]
)
const afterLeave = () =>
onAfterLeave &&
callWithAsyncErrorHandling(
onAfterLeave,
instance,
ErrorCodes.TRANSITION_HOOK,
[el]
)
if (onLeave) { if (onLeave) {
onLeave(el, () => { onLeave(el, () => {
remove() remove()
onAfterLeave && onAfterLeave(el) afterLeave()
}) })
} else { } else {
remove() remove()
onAfterLeave && onAfterLeave(el) afterLeave()
} }
} }
} }
@ -197,3 +225,11 @@ function placeholder(vnode: VNode): VNode | undefined {
return vnode return vnode
} }
} }
function updateHOCTransitionData(vnode: VNode, data: TransitionData) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
updateHOCTransitionData(vnode.component!.subTree, data)
} else {
vnode.transition = data
}
}

View File

@ -14,6 +14,7 @@ export const enum ErrorCodes {
NATIVE_EVENT_HANDLER, NATIVE_EVENT_HANDLER,
COMPONENT_EVENT_HANDLER, COMPONENT_EVENT_HANDLER,
DIRECTIVE_HOOK, DIRECTIVE_HOOK,
TRANSITION_HOOK,
APP_ERROR_HANDLER, APP_ERROR_HANDLER,
APP_WARN_HANDLER, APP_WARN_HANDLER,
FUNCTION_REF, FUNCTION_REF,
@ -42,6 +43,7 @@ export const ErrorTypeStrings: Record<number | string, string> = {
[ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler', [ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
[ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler', [ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
[ErrorCodes.DIRECTIVE_HOOK]: 'directive hook', [ErrorCodes.DIRECTIVE_HOOK]: 'directive hook',
[ErrorCodes.TRANSITION_HOOK]: 'transition hook',
[ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler', [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler', [ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
[ErrorCodes.FUNCTION_REF]: 'ref function', [ErrorCodes.FUNCTION_REF]: 'ref function',

View File

@ -27,8 +27,7 @@ import {
EMPTY_ARR, EMPTY_ARR,
isReservedProp, isReservedProp,
isFunction, isFunction,
PatchFlags, PatchFlags
isArray
} from '@vue/shared' } from '@vue/shared'
import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler' import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler'
import { import {
@ -52,11 +51,7 @@ import {
queueEffectWithSuspense, queueEffectWithSuspense,
SuspenseImpl SuspenseImpl
} from './components/Suspense' } from './components/Suspense'
import { import { ErrorCodes, callWithErrorHandling } from './errorHandling'
ErrorCodes,
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
import { KeepAliveSink, isKeepAlive } from './components/KeepAlive' import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
export interface RendererOptions<HostNode = any, HostElement = any> { export interface RendererOptions<HostNode = any, HostElement = any> {
@ -362,7 +357,7 @@ export function createRenderer<
const tag = vnode.type as string const tag = vnode.type as string
isSVG = isSVG || tag === 'svg' isSVG = isSVG || tag === 'svg'
const el = (vnode.el = hostCreateElement(tag, isSVG)) const el = (vnode.el = hostCreateElement(tag, isSVG))
const { props, shapeFlag } = vnode const { props, shapeFlag, transition } = vnode
if (props != null) { if (props != null) {
for (const key in props) { for (const key in props) {
if (isReservedProp(key)) continue if (isReservedProp(key)) continue
@ -372,6 +367,9 @@ export function createRenderer<
invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode) invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
} }
} }
if (transition != null) {
transition.beforeEnter(el)
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string) hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
@ -386,9 +384,12 @@ export function createRenderer<
) )
} }
hostInsert(el, container, anchor) hostInsert(el, container, anchor)
if (props != null && props.onVnodeMounted != null) { const vnodeMountedHook = props && props.onVnodeMounted
if (vnodeMountedHook != null || transition != null) {
queuePostRenderEffect(() => { queuePostRenderEffect(() => {
invokeDirectiveHook(props.onVnodeMounted!, parentComponent, vnode) vnodeMountedHook &&
invokeDirectiveHook(vnodeMountedHook, parentComponent, vnode)
transition && transition.enter(el)
}, parentSuspense) }, parentSuspense)
} }
} }
@ -1412,13 +1413,15 @@ export function createRenderer<
doRemove?: boolean doRemove?: boolean
) { ) {
const { const {
el,
props, props,
ref, ref,
type, type,
children, children,
dynamicChildren, dynamicChildren,
shapeFlag, shapeFlag,
anchor anchor,
transition
} = vnode } = vnode
// unset ref // unset ref
@ -1462,23 +1465,16 @@ export function createRenderer<
} }
if (doRemove) { if (doRemove) {
const beforeRemoveHooks = props && props.onVnodeBeforeRemove
const remove = () => { const remove = () => {
hostRemove(vnode.el!) hostRemove(vnode.el!)
if (anchor != null) hostRemove(anchor) if (anchor != null) hostRemove(anchor)
const removedHook = props && props.onVnodeRemoved if (transition != null && transition.afterLeave) {
removedHook && removedHook() transition.afterLeave()
}
if (vnode.shapeFlag & ShapeFlags.ELEMENT && beforeRemoveHooks != null) {
const delayLeave = props && props.onVnodeDelayLeave
const performLeave = () => {
invokeBeforeRemoveHooks(
beforeRemoveHooks,
parentComponent,
vnode,
remove
)
} }
}
if (vnode.shapeFlag & ShapeFlags.ELEMENT && transition != null) {
const { leave, delayLeave } = transition
const performLeave = () => leave(el!, remove)
if (delayLeave) { if (delayLeave) {
delayLeave(performLeave) delayLeave(performLeave)
} else { } else {
@ -1496,37 +1492,6 @@ export function createRenderer<
} }
} }
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( function unmountComponent(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
parentSuspense: HostSuspenseBoundary | null, parentSuspense: HostSuspenseBoundary | null,

View File

@ -19,7 +19,7 @@ import { AppContext } from './apiApp'
import { SuspenseBoundary } from './components/Suspense' import { SuspenseBoundary } from './components/Suspense'
import { DirectiveBinding } from './directives' import { DirectiveBinding } from './directives'
import { SuspenseImpl } from './components/Suspense' import { SuspenseImpl } from './components/Suspense'
import { TransitionProps } from './components/Transition' import { TransitionData } from './components/Transition'
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
__isFragment: true __isFragment: true
@ -57,11 +57,6 @@ export interface VNodeProps {
onVnodeUpdated?: (vnode: VNode, oldVNode: VNode) => void onVnodeUpdated?: (vnode: VNode, oldVNode: VNode) => void
onVnodeBeforeUnmount?: (vnode: VNode) => void onVnodeBeforeUnmount?: (vnode: VNode) => void
onVnodeUnmounted?: (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 VNodeChildAtom<HostNode, HostElement> =
@ -98,7 +93,7 @@ export interface VNode<HostNode = any, HostElement = any> {
component: ComponentInternalInstance | null component: ComponentInternalInstance | null
suspense: SuspenseBoundary<HostNode, HostElement> | null suspense: SuspenseBoundary<HostNode, HostElement> | null
dirs: DirectiveBinding[] | null dirs: DirectiveBinding[] | null
transition: TransitionProps | null transition: TransitionData | null
// DOM // DOM
el: HostNode | null el: HostNode | null