feat(transition): base transition component

This commit is contained in:
Evan You 2019-11-20 18:04:44 -05:00
parent f7009d58a3
commit 93561b080e
6 changed files with 310 additions and 31 deletions

View File

@ -39,6 +39,9 @@ export interface KeepAliveSink {
deactivate: (vnode: VNode) => void deactivate: (vnode: VNode) => void
} }
export const isKeepAlive = (vnode: VNode): boolean =>
(vnode.type as any).__isKeepAlive
const KeepAliveImpl = { const KeepAliveImpl = {
name: `KeepAlive`, name: `KeepAlive`,
@ -47,6 +50,12 @@ const KeepAliveImpl = {
// would prevent it from being tree-shaken. // would prevent it from being tree-shaken.
__isKeepAlive: true, __isKeepAlive: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props: KeepAliveProps, { slots }: SetupContext) { setup(props: KeepAliveProps, { slots }: SetupContext) {
const cache: Cache = new Map() const cache: Cache = new Map()
const keys: Keys = new Set() const keys: Keys = new Set()
@ -200,14 +209,6 @@ const KeepAliveImpl = {
} }
} }
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 the public type for h/tsx inference
export const KeepAlive = (KeepAliveImpl as any) as { export const KeepAlive = (KeepAliveImpl as any) as {
new (): { new (): {

View File

@ -0,0 +1,199 @@
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
}
}

View File

@ -147,7 +147,7 @@ export function withDirectives<T extends VNode>(
} }
export function invokeDirectiveHook( export function invokeDirectiveHook(
hook: Function | Function[], hook: ((...args: any[]) => any) | ((...args: any[]) => any)[],
instance: ComponentInternalInstance | null, instance: ComponentInternalInstance | null,
vnode: VNode, vnode: VNode,
prevVNode: VNode | null = null prevVNode: VNode | null = null

View File

@ -28,6 +28,7 @@ export { Text, Comment, Fragment, Portal } from './vnode'
// Internal Components // Internal Components
export { Suspense, SuspenseProps } from './components/Suspense' export { Suspense, SuspenseProps } from './components/Suspense'
export { KeepAlive, KeepAliveProps } from './components/KeepAlive' export { KeepAlive, KeepAliveProps } from './components/KeepAlive'
export { Transition, TransitionProps } from './components/Transition'
// VNode flags // VNode flags
export { PublicShapeFlags as ShapeFlags } from './shapeFlags' export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
import { PublicPatchFlags } from '@vue/shared' import { PublicPatchFlags } from '@vue/shared'

View File

@ -6,7 +6,8 @@ import {
normalizeVNode, normalizeVNode,
VNode, VNode,
VNodeChildren, VNodeChildren,
createVNode createVNode,
isSameVNodeType
} from './vnode' } from './vnode'
import { import {
ComponentInternalInstance, ComponentInternalInstance,
@ -26,7 +27,8 @@ 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 {
@ -50,8 +52,12 @@ import {
queueEffectWithSuspense, queueEffectWithSuspense,
SuspenseImpl SuspenseImpl
} from './components/Suspense' } from './components/Suspense'
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import {
import { KeepAliveSink } from './components/KeepAlive' ErrorCodes,
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
export interface RendererOptions<HostNode = any, HostElement = any> { export interface RendererOptions<HostNode = any, HostElement = any> {
patchProp( patchProp(
@ -128,10 +134,6 @@ function createDevEffectOptions(
} }
} }
function isSameType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
export function invokeHooks(hooks: Function[], arg?: DebuggerEvent) { export function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
for (let i = 0; i < hooks.length; i++) { for (let i = 0; i < hooks.length; i++) {
hooks[i](arg) hooks[i](arg)
@ -203,7 +205,7 @@ export function createRenderer<
optimized: boolean = false optimized: boolean = false
) { ) {
// patching & not same type, unmount old tree // patching & not same type, unmount old tree
if (n1 != null && !isSameType(n1, n2)) { if (n1 != null && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1) anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true) unmount(n1, parentComponent, parentSuspense, true)
n1 = null n1 = null
@ -386,7 +388,7 @@ export function createRenderer<
hostInsert(el, container, anchor) hostInsert(el, container, anchor)
if (props != null && props.onVnodeMounted != null) { if (props != null && props.onVnodeMounted != null) {
queuePostRenderEffect(() => { queuePostRenderEffect(() => {
invokeDirectiveHook(props.onVnodeMounted, parentComponent, vnode) invokeDirectiveHook(props.onVnodeMounted!, parentComponent, vnode)
}, parentSuspense) }, parentSuspense)
} }
} }
@ -844,7 +846,7 @@ export function createRenderer<
const Comp = initialVNode.type as Component const Comp = initialVNode.type as Component
// inject renderer internals for keepAlive // inject renderer internals for keepAlive
if ((Comp as any).__isKeepAlive) { if (isKeepAlive(initialVNode)) {
const sink = instance.sink as KeepAliveSink const sink = instance.sink as KeepAliveSink
sink.renderer = internals sink.renderer = internals
sink.parentSuspense = parentSuspense sink.parentSuspense = parentSuspense
@ -937,8 +939,9 @@ export function createRenderer<
if (next !== null) { if (next !== null) {
updateComponentPreRender(instance, next) updateComponentPreRender(instance, next)
} }
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree const prevTree = instance.subTree
const nextTree = (instance.subTree = renderComponentRoot(instance)) instance.subTree = nextTree
// beforeUpdate hook // beforeUpdate hook
if (instance.bu !== null) { if (instance.bu !== null) {
invokeHooks(instance.bu) invokeHooks(instance.bu)
@ -1167,7 +1170,7 @@ export function createRenderer<
const n2 = optimized const n2 = optimized
? (c2[i] as HostVNode) ? (c2[i] as HostVNode)
: (c2[i] = normalizeVNode(c2[i])) : (c2[i] = normalizeVNode(c2[i]))
if (isSameType(n1, n2)) { if (isSameVNodeType(n1, n2)) {
patch( patch(
n1, n1,
n2, n2,
@ -1192,7 +1195,7 @@ export function createRenderer<
const n2 = optimized const n2 = optimized
? (c2[i] as HostVNode) ? (c2[i] as HostVNode)
: (c2[e2] = normalizeVNode(c2[e2])) : (c2[e2] = normalizeVNode(c2[e2]))
if (isSameType(n1, n2)) { if (isSameVNodeType(n1, n2)) {
patch( patch(
n1, n1,
n2, n2,
@ -1308,7 +1311,7 @@ export function createRenderer<
for (j = s2; j <= e2; j++) { for (j = s2; j <= e2; j++) {
if ( if (
newIndexToOldIndexMap[j - s2] === 0 && newIndexToOldIndexMap[j - s2] === 0 &&
isSameType(prevChild, c2[j] as HostVNode) isSameVNodeType(prevChild, c2[j] as HostVNode)
) { ) {
newIndex = j newIndex = j
break break
@ -1459,17 +1462,71 @@ export function createRenderer<
} }
if (doRemove) { if (doRemove) {
hostRemove(vnode.el!) const beforeRemoveHooks = props && props.onVnodeBeforeRemove
if (anchor != null) hostRemove(anchor) 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) { if (props != null && props.onVnodeUnmounted != null) {
queuePostRenderEffect(() => { queuePostRenderEffect(() => {
invokeDirectiveHook(props.onVnodeUnmounted, parentComponent, vnode) invokeDirectiveHook(props.onVnodeUnmounted!, parentComponent, vnode)
}, parentSuspense) }, 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( function unmountComponent(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
parentSuspense: HostSuspenseBoundary | null, parentSuspense: HostSuspenseBoundary | null,

View File

@ -19,6 +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'
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
__isFragment: true __isFragment: true
@ -48,6 +49,19 @@ export interface VNodeProps {
[key: string]: any [key: string]: any
key?: string | number key?: string | number
ref?: string | Ref | ((ref: object | null) => void) 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 VNodeChildAtom<HostNode, HostElement> =
@ -79,11 +93,12 @@ export interface VNode<HostNode = any, HostElement = any> {
type: VNodeTypes type: VNodeTypes
props: VNodeProps | null props: VNodeProps | null
key: string | number | null key: string | number | null
ref: string | Function | null ref: string | Ref | ((ref: object | null) => void) | null
children: NormalizedChildren<HostNode, HostElement> children: NormalizedChildren<HostNode, HostElement>
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
// DOM // DOM
el: HostNode | null el: HostNode | null
@ -173,9 +188,13 @@ export function isVNode(value: any): value is VNode {
return value ? value._isVNode === true : false 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( export function createVNode(
type: VNodeTypes, type: VNodeTypes,
props: { [key: string]: any } | null = null, props: (Data & VNodeProps) | null = null,
children: unknown = null, children: unknown = null,
patchFlag: number = 0, patchFlag: number = 0,
dynamicProps: string[] | null = null dynamicProps: string[] | null = null
@ -221,6 +240,7 @@ export function createVNode(
component: null, component: null,
suspense: null, suspense: null,
dirs: null, dirs: null,
transition: null,
el: null, el: null,
anchor: null, anchor: null,
target: null, target: null,
@ -252,7 +272,7 @@ export function createVNode(
export function cloneVNode<T, U>( export function cloneVNode<T, U>(
vnode: VNode<T, U>, vnode: VNode<T, U>,
extraProps?: Data extraProps?: Data & VNodeProps
): 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.
@ -274,6 +294,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,
// 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
@ -376,7 +397,7 @@ export function normalizeClass(value: unknown): string {
const handlersRE = /^on|^vnode/ const handlersRE = /^on|^vnode/
export function mergeProps(...args: Data[]) { export function mergeProps(...args: (Data & VNodeProps)[]) {
const ret: Data = {} const ret: Data = {}
extend(ret, args[0]) extend(ret, args[0])
for (let i = 1; i < args.length; i++) { for (let i = 1; i < args.length; i++) {