feat(transition): compat with keep-alive

This commit is contained in:
Evan You 2019-11-25 17:34:28 -05:00
parent 16ea2993d6
commit c6fb506fc0
8 changed files with 192 additions and 88 deletions

View File

@ -347,12 +347,8 @@ describe('compiler: element transform', () => {
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' }]
])
}

View File

@ -138,8 +138,11 @@ export const transformElement: NodeTransform = (node, context) => {
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) {

View File

@ -91,7 +91,8 @@ export function renderComponentRoot(
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 ` +

View File

@ -1,8 +1,9 @@
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[]
@ -65,7 +66,7 @@ export function resolveSlots(
}
} 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.`
@ -74,7 +75,5 @@ export function resolveSlots(
const normalized = normalizeSlotValue(children)
slots = { default: () => normalized }
}
if (slots !== void 0) {
instance.slots = slots
}
instance.slots = slots || EMPTY_OBJ
}

View File

@ -3,7 +3,13 @@ import {
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'
@ -36,17 +42,38 @@ export interface BaseTransitionProps {
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 = {
@ -56,7 +83,8 @@ const BaseTransitionImpl = {
const state: TransitionState = {
isMounted: false,
isLeaving: false,
isUnmounting: false
isUnmounting: false,
leavingVNodes: Object.create(null)
}
onMounted(() => {
state.isMounted = true
@ -84,7 +112,7 @@ const BaseTransitionImpl = {
// 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.'
)
}
@ -101,45 +129,53 @@ const BaseTransitionImpl = {
// 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
}
}
}
@ -175,18 +211,10 @@ export const BaseTransition = (BaseTransitionImpl as any) as {
}
}
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,
@ -200,36 +228,51 @@ function resolveTransitionHooks(
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)
@ -238,16 +281,17 @@ function resolveTransitionHooks(
}
},
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()
@ -256,8 +300,10 @@ function resolveTransitionHooks(
} else {
callHook(onAfterLeave, [el])
}
state.pendingLeave = undefined
el._leaveCb = undefined
delete leavingVNodes[key]
})
leavingVNodes[key] = vnode
if (onLeave) {
onLeave(el, afterLeave)
} else {
@ -265,13 +311,15 @@ function resolveTransitionHooks(
}
}
}
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
@ -279,10 +327,18 @@ function placeholder(vnode: VNode): VNode | undefined {
}
}
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
}
}

View File

@ -17,8 +17,10 @@ import { SuspenseBoundary } from './Suspense'
import {
RendererInternals,
queuePostRenderEffect,
invokeHooks
invokeHooks,
MoveType
} from '../renderer'
import { setTransitionHooks } from './BaseTransition'
type MatchPattern = string | RegExp | string[] | RegExp[]
@ -80,7 +82,7 @@ const KeepAliveImpl = {
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
@ -91,7 +93,7 @@ const KeepAliveImpl = {
}
sink.deactivate = (vnode: VNode) => {
move(vnode, storageContainer, null)
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
const component = vnode.component!
if (component.da !== null) {
@ -188,6 +190,10 @@ const KeepAliveImpl = {
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

View File

@ -3,7 +3,7 @@ import { ShapeFlags } from '../shapeFlags'
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'
@ -213,7 +213,7 @@ export interface SuspenseBoundary<
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,
@ -299,7 +299,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
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) {
@ -346,7 +346,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
// 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,
@ -372,11 +372,12 @@ function createSuspenseBoundary<HostNode, HostElement>(
}
},
move(container, anchor) {
move(container, anchor, type) {
move(
suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
container,
anchor
anchor,
type
)
suspense.container = container
},

View File

@ -109,12 +109,20 @@ export interface RendererInternals<HostNode = any, HostElement = any> {
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
}
@ -367,9 +375,6 @@ export function createRenderer<
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) {
@ -383,6 +388,9 @@ export function createRenderer<
optimized || vnode.dynamicChildren !== null
)
}
if (transition != null && !transition.persisted) {
transition.beforeEnter(el)
}
hostInsert(el, container, anchor)
const vnodeMountedHook = props && props.onVnodeMounted
if (
@ -747,7 +755,12 @@ export function createRenderer<
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__) {
@ -1372,7 +1385,7 @@ export function createRenderer<
// 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--
}
@ -1384,25 +1397,54 @@ export function createRenderer<
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)
}
}
}