feat(transition): base transition component
This commit is contained in:
parent
f7009d58a3
commit
93561b080e
@ -39,6 +39,9 @@ export interface KeepAliveSink {
|
||||
deactivate: (vnode: VNode) => void
|
||||
}
|
||||
|
||||
export const isKeepAlive = (vnode: VNode): boolean =>
|
||||
(vnode.type as any).__isKeepAlive
|
||||
|
||||
const KeepAliveImpl = {
|
||||
name: `KeepAlive`,
|
||||
|
||||
@ -47,6 +50,12 @@ const KeepAliveImpl = {
|
||||
// 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()
|
||||
@ -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 const KeepAlive = (KeepAliveImpl as any) as {
|
||||
new (): {
|
||||
|
199
packages/runtime-core/src/components/Transition.ts
Normal file
199
packages/runtime-core/src/components/Transition.ts
Normal 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
|
||||
}
|
||||
}
|
@ -147,7 +147,7 @@ export function withDirectives<T extends VNode>(
|
||||
}
|
||||
|
||||
export function invokeDirectiveHook(
|
||||
hook: Function | Function[],
|
||||
hook: ((...args: any[]) => any) | ((...args: any[]) => any)[],
|
||||
instance: ComponentInternalInstance | null,
|
||||
vnode: VNode,
|
||||
prevVNode: VNode | null = null
|
||||
|
@ -28,6 +28,7 @@ export { Text, Comment, Fragment, Portal } from './vnode'
|
||||
// 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'
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
normalizeVNode,
|
||||
VNode,
|
||||
VNodeChildren,
|
||||
createVNode
|
||||
createVNode,
|
||||
isSameVNodeType
|
||||
} from './vnode'
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
@ -26,7 +27,8 @@ import {
|
||||
EMPTY_ARR,
|
||||
isReservedProp,
|
||||
isFunction,
|
||||
PatchFlags
|
||||
PatchFlags,
|
||||
isArray
|
||||
} from '@vue/shared'
|
||||
import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler'
|
||||
import {
|
||||
@ -50,8 +52,12 @@ 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(
|
||||
@ -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) {
|
||||
for (let i = 0; i < hooks.length; i++) {
|
||||
hooks[i](arg)
|
||||
@ -203,7 +205,7 @@ export function createRenderer<
|
||||
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
|
||||
@ -386,7 +388,7 @@ export function createRenderer<
|
||||
hostInsert(el, container, anchor)
|
||||
if (props != null && props.onVnodeMounted != null) {
|
||||
queuePostRenderEffect(() => {
|
||||
invokeDirectiveHook(props.onVnodeMounted, parentComponent, vnode)
|
||||
invokeDirectiveHook(props.onVnodeMounted!, parentComponent, vnode)
|
||||
}, parentSuspense)
|
||||
}
|
||||
}
|
||||
@ -844,7 +846,7 @@ export function createRenderer<
|
||||
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
|
||||
@ -937,8 +939,9 @@ export function createRenderer<
|
||||
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)
|
||||
@ -1167,7 +1170,7 @@ export function createRenderer<
|
||||
const n2 = optimized
|
||||
? (c2[i] as HostVNode)
|
||||
: (c2[i] = normalizeVNode(c2[i]))
|
||||
if (isSameType(n1, n2)) {
|
||||
if (isSameVNodeType(n1, n2)) {
|
||||
patch(
|
||||
n1,
|
||||
n2,
|
||||
@ -1192,7 +1195,7 @@ export function createRenderer<
|
||||
const n2 = optimized
|
||||
? (c2[i] as HostVNode)
|
||||
: (c2[e2] = normalizeVNode(c2[e2]))
|
||||
if (isSameType(n1, n2)) {
|
||||
if (isSameVNodeType(n1, n2)) {
|
||||
patch(
|
||||
n1,
|
||||
n2,
|
||||
@ -1308,7 +1311,7 @@ export function createRenderer<
|
||||
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
|
||||
@ -1459,17 +1462,71 @@ export function createRenderer<
|
||||
}
|
||||
|
||||
if (doRemove) {
|
||||
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,
|
||||
|
@ -19,6 +19,7 @@ import { AppContext } from './apiApp'
|
||||
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
|
||||
@ -48,6 +49,19 @@ export interface VNodeProps {
|
||||
[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> =
|
||||
@ -79,11 +93,12 @@ export interface VNode<HostNode = any, HostElement = any> {
|
||||
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
|
||||
@ -173,9 +188,13 @@ export function isVNode(value: any): value is VNode {
|
||||
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
|
||||
@ -221,6 +240,7 @@ export function createVNode(
|
||||
component: null,
|
||||
suspense: null,
|
||||
dirs: null,
|
||||
transition: null,
|
||||
el: null,
|
||||
anchor: null,
|
||||
target: null,
|
||||
@ -252,7 +272,7 @@ export function createVNode(
|
||||
|
||||
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.
|
||||
@ -274,6 +294,7 @@ export function cloneVNode<T, U>(
|
||||
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
|
||||
@ -376,7 +397,7 @@ export function normalizeClass(value: unknown): string {
|
||||
|
||||
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++) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user