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([ expect(node.arguments).toMatchObject([
KEEP_ALIVE, KEEP_ALIVE,
`null`, `null`,
createObjectMatcher({ // keep-alive should not compile content to slots
default: { [{ type: NodeTypes.ELEMENT, tag: 'span' }]
type: NodeTypes.JS_FUNCTION_EXPRESSION
},
_compiled: `[true]`
})
]) ])
} }

View File

@ -138,8 +138,11 @@ export const transformElement: NodeTransform = (node, context) => {
if (!hasProps) { if (!hasProps) {
args.push(`null`) args.push(`null`)
} }
// Portal should have normal children instead of slots // Portal & KeepAlive should have normal children instead of slots
if (isComponent && !isPortal) { // 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) const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots) args.push(slots)
if (hasDynamicSlots) { if (hasDynamicSlots) {

View File

@ -91,7 +91,8 @@ export function renderComponentRoot(
if ( if (
__DEV__ && __DEV__ &&
!(result.shapeFlag & ShapeFlags.COMPONENT) && !(result.shapeFlag & ShapeFlags.COMPONENT) &&
!(result.shapeFlag & ShapeFlags.ELEMENT) !(result.shapeFlag & ShapeFlags.ELEMENT) &&
result.type !== Comment
) { ) {
warn( warn(
`Component inside <Transition> renders non-element root node ` + `Component inside <Transition> renders non-element root node ` +

View File

@ -1,8 +1,9 @@
import { ComponentInternalInstance, currentInstance } from './component' import { ComponentInternalInstance, currentInstance } from './component'
import { VNode, NormalizedChildren, normalizeVNode, VNodeChild } from './vnode' 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 { ShapeFlags } from './shapeFlags'
import { warn } from './warning' import { warn } from './warning'
import { isKeepAlive } from './components/KeepAlive'
export type Slot = (...args: any[]) => VNode[] export type Slot = (...args: any[]) => VNode[]
@ -65,7 +66,7 @@ export function resolveSlots(
} }
} else if (children !== null) { } else if (children !== null) {
// non slot object children (direct value) passed to a component // non slot object children (direct value) passed to a component
if (__DEV__) { if (__DEV__ && !isKeepAlive(instance.vnode)) {
warn( warn(
`Non-function value encountered for default slot. ` + `Non-function value encountered for default slot. ` +
`Prefer function slots for better performance.` `Prefer function slots for better performance.`
@ -74,7 +75,5 @@ export function resolveSlots(
const normalized = normalizeSlotValue(children) const normalized = normalizeSlotValue(children)
slots = { default: () => normalized } slots = { default: () => normalized }
} }
if (slots !== void 0) { instance.slots = slots || EMPTY_OBJ
instance.slots = slots
}
} }

View File

@ -3,7 +3,13 @@ import {
SetupContext, SetupContext,
ComponentOptions ComponentOptions
} from '../component' } from '../component'
import { cloneVNode, Comment, isSameVNodeType, VNode } from '../vnode' import {
cloneVNode,
Comment,
isSameVNodeType,
VNode,
VNodeChildren
} 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'
@ -36,17 +42,38 @@ export interface BaseTransitionProps {
onLeaveCancelled?: (el: any) => void 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 = ( type TransitionHookCaller = (
hook: ((el: any) => void) | undefined, hook: ((el: any) => void) | undefined,
args?: any[] args?: any[]
) => void ) => void
type PendingCallback = (cancelled?: boolean) => void
interface TransitionState { interface TransitionState {
isMounted: boolean isMounted: boolean
isLeaving: boolean isLeaving: boolean
isUnmounting: boolean isUnmounting: boolean
pendingEnter?: (cancelled?: boolean) => void // Track pending leave callbacks for children of the same key.
pendingLeave?: (cancelled?: boolean) => void // 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 = { const BaseTransitionImpl = {
@ -56,7 +83,8 @@ const BaseTransitionImpl = {
const state: TransitionState = { const state: TransitionState = {
isMounted: false, isMounted: false,
isLeaving: false, isLeaving: false,
isUnmounting: false isUnmounting: false,
leavingVNodes: Object.create(null)
} }
onMounted(() => { onMounted(() => {
state.isMounted = true state.isMounted = true
@ -84,7 +112,7 @@ const BaseTransitionImpl = {
// warn multiple elements // warn multiple elements
if (__DEV__ && children.length > 1) { if (__DEV__ && children.length > 1) {
warn( 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.' '<transition-group> for lists.'
) )
} }
@ -101,45 +129,53 @@ const BaseTransitionImpl = {
// at this point children has a guaranteed length of 1. // at this point children has a guaranteed length of 1.
const child = children[0] const child = children[0]
if (state.isLeaving) { if (state.isLeaving) {
return placeholder(child) return emptyPlaceholder(child)
} }
let delayedLeave: (() => void) | undefined // in the case of <transition><keep-alive/></transition>, we need to
const performDelayedLeave = () => delayedLeave && delayedLeave() // 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, rawProps,
state, state,
callTransitionHook, callTransitionHook
performDelayedLeave
)) ))
// clone old subTree because we need to modify it
const oldChild = instance.subTree const oldChild = instance.subTree
? (instance.subTree = cloneVNode(instance.subTree)) const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
: null
// handle mode // handle mode
if ( if (
oldChild && oldInnerChild &&
!isSameVNodeType(child, oldChild) && oldInnerChild.type !== Comment &&
oldChild.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 // update old tree's hooks in case of dynamic transition
// need to do this recursively in case of HOCs setTransitionHooks(oldInnerChild, leavingHooks)
updateHOCTransitionData(oldChild, transitionHooks)
// switching between different views // switching between different views
if (mode === 'out-in') { if (mode === 'out-in') {
state.isLeaving = true state.isLeaving = true
// return placeholder node and queue update when leave finishes // return placeholder node and queue update when leave finishes
transitionHooks.afterLeave = () => { leavingHooks.afterLeave = () => {
state.isLeaving = false state.isLeaving = false
instance.update() instance.update()
} }
return placeholder(child) return emptyPlaceholder(child)
} else if (mode === 'in-out') { } else if (mode === 'in-out') {
transitionHooks.delayLeave = performLeave => { delete prevHooks.delayedLeave
delayedLeave = performLeave 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 // The transition hooks are attached to the vnode as vnode.transition
// and will be called at appropriate timing in the renderer. // and will be called at appropriate timing in the renderer.
function resolveTransitionHooks( function resolveTransitionHooks(
vnode: VNode,
{ {
appear, appear,
persisted = false, persisted = false,
@ -200,36 +228,51 @@ function resolveTransitionHooks(
onLeaveCancelled onLeaveCancelled
}: BaseTransitionProps, }: BaseTransitionProps,
state: TransitionState, state: TransitionState,
callHook: TransitionHookCaller, callHook: TransitionHookCaller
performDelayedLeave: () => void
): TransitionHooks { ): TransitionHooks {
return { const { leavingVNodes } = state
const key = String(vnode.key)
const hooks: TransitionHooks = {
persisted, persisted,
beforeEnter(el) { beforeEnter(el: TransitionElement) {
if (state.pendingLeave) {
state.pendingLeave(true /* cancelled */)
}
if (!appear && !state.isMounted) { if (!appear && !state.isMounted) {
return 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]) callHook(onBeforeEnter, [el])
}, },
enter(el) { enter(el: TransitionElement) {
if (!appear && !state.isMounted) { if (!appear && !state.isMounted) {
return return
} }
let called = false let called = false
const afterEnter = (state.pendingEnter = (cancelled?) => { const afterEnter = (el._enterCb = (cancelled?) => {
if (called) return if (called) return
called = true called = true
if (cancelled) { if (cancelled) {
callHook(onEnterCancelled, [el]) callHook(onEnterCancelled, [el])
} else { } else {
callHook(onAfterEnter, [el]) callHook(onAfterEnter, [el])
performDelayedLeave()
} }
state.pendingEnter = undefined if (hooks.delayedLeave) {
hooks.delayedLeave()
}
el._enterCb = undefined
}) })
if (onEnter) { if (onEnter) {
onEnter(el, afterEnter) onEnter(el, afterEnter)
@ -238,16 +281,17 @@ function resolveTransitionHooks(
} }
}, },
leave(el, remove) { leave(el: TransitionElement, remove) {
if (state.pendingEnter) { const key = String(vnode.key)
state.pendingEnter(true /* cancelled */) if (el._enterCb) {
el._enterCb(true /* cancelled */)
} }
if (state.isUnmounting) { if (state.isUnmounting) {
return remove() return remove()
} }
callHook(onBeforeLeave, [el]) callHook(onBeforeLeave, [el])
let called = false let called = false
const afterLeave = (state.pendingLeave = (cancelled?) => { const afterLeave = (el._leaveCb = (cancelled?) => {
if (called) return if (called) return
called = true called = true
remove() remove()
@ -256,8 +300,10 @@ function resolveTransitionHooks(
} else { } else {
callHook(onAfterLeave, [el]) callHook(onAfterLeave, [el])
} }
state.pendingLeave = undefined el._leaveCb = undefined
delete leavingVNodes[key]
}) })
leavingVNodes[key] = vnode
if (onLeave) { if (onLeave) {
onLeave(el, afterLeave) onLeave(el, afterLeave)
} else { } else {
@ -265,13 +311,15 @@ function resolveTransitionHooks(
} }
} }
} }
return hooks
} }
// the placeholder really only handles one special case: KeepAlive // 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 // 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 // placeholder with empty content to avoid the KeepAlive instance from being
// unmounted. // unmounted.
function placeholder(vnode: VNode): VNode | undefined { function emptyPlaceholder(vnode: VNode): VNode | undefined {
if (isKeepAlive(vnode)) { if (isKeepAlive(vnode)) {
vnode = cloneVNode(vnode) vnode = cloneVNode(vnode)
vnode.children = null vnode.children = null
@ -279,10 +327,18 @@ function placeholder(vnode: VNode): VNode | undefined {
} }
} }
function updateHOCTransitionData(vnode: VNode, data: TransitionHooks) { function getKeepAliveChild(vnode: VNode): VNode | undefined {
if (vnode.shapeFlag & ShapeFlags.COMPONENT) { return isKeepAlive(vnode)
updateHOCTransitionData(vnode.component!.subTree, data) ? 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 { } else {
vnode.transition = data vnode.transition = hooks
} }
} }

View File

@ -17,8 +17,10 @@ import { SuspenseBoundary } from './Suspense'
import { import {
RendererInternals, RendererInternals,
queuePostRenderEffect, queuePostRenderEffect,
invokeHooks invokeHooks,
MoveType
} from '../renderer' } from '../renderer'
import { setTransitionHooks } from './BaseTransition'
type MatchPattern = string | RegExp | string[] | RegExp[] type MatchPattern = string | RegExp | string[] | RegExp[]
@ -80,7 +82,7 @@ const KeepAliveImpl = {
const storageContainer = createElement('div') const storageContainer = createElement('div')
sink.activate = (vnode, container, anchor) => { sink.activate = (vnode, container, anchor) => {
move(vnode, container, anchor) move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
queuePostRenderEffect(() => { queuePostRenderEffect(() => {
const component = vnode.component! const component = vnode.component!
component.isDeactivated = false component.isDeactivated = false
@ -91,7 +93,7 @@ const KeepAliveImpl = {
} }
sink.deactivate = (vnode: VNode) => { sink.deactivate = (vnode: VNode) => {
move(vnode, storageContainer, null) move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => { queuePostRenderEffect(() => {
const component = vnode.component! const component = vnode.component!
if (component.da !== null) { if (component.da !== null) {
@ -188,6 +190,10 @@ const KeepAliveImpl = {
vnode.el = cached.el vnode.el = cached.el
vnode.anchor = cached.anchor vnode.anchor = cached.anchor
vnode.component = cached.component vnode.component = cached.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// avoid vnode being mounted as fresh // avoid vnode being mounted as fresh
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest // make this key the freshest

View File

@ -3,7 +3,7 @@ import { ShapeFlags } from '../shapeFlags'
import { isFunction, isArray } from '@vue/shared' import { isFunction, isArray } from '@vue/shared'
import { ComponentInternalInstance, handleSetupResult } from '../component' import { ComponentInternalInstance, handleSetupResult } from '../component'
import { Slots } from '../componentSlots' import { Slots } from '../componentSlots'
import { RendererInternals } from '../renderer' import { RendererInternals, MoveType } from '../renderer'
import { queuePostFlushCb, queueJob } from '../scheduler' import { queuePostFlushCb, queueJob } from '../scheduler'
import { updateHOCHostEl } from '../componentRenderUtils' import { updateHOCHostEl } from '../componentRenderUtils'
import { handleError, ErrorCodes } from '../errorHandling' import { handleError, ErrorCodes } from '../errorHandling'
@ -213,7 +213,7 @@ export interface SuspenseBoundary<
effects: Function[] effects: Function[]
resolve(): void resolve(): void
recede(): void recede(): void
move(container: HostElement, anchor: HostNode | null): void move(container: HostElement, anchor: HostNode | null, type: MoveType): void
next(): HostNode | null next(): HostNode | null
registerDep( registerDep(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
@ -299,7 +299,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
unmount(fallbackTree as VNode, parentComponent, suspense, true) unmount(fallbackTree as VNode, parentComponent, suspense, true)
} }
// move content from off-dom container to actual container // 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!) const el = (vnode.el = (subTree as VNode).el!)
// suspense as the root node of a component... // suspense as the root node of a component...
if (parentComponent && parentComponent.subTree === vnode) { if (parentComponent && parentComponent.subTree === vnode) {
@ -346,7 +346,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
// move content tree back to the off-dom container // move content tree back to the off-dom container
const anchor = next(subTree) const anchor = next(subTree)
move(subTree as VNode, hiddenContainer, null) move(subTree as VNode, hiddenContainer, null, MoveType.LEAVE)
// remount the fallback tree // remount the fallback tree
patch( patch(
null, null,
@ -372,11 +372,12 @@ function createSuspenseBoundary<HostNode, HostElement>(
} }
}, },
move(container, anchor) { move(container, anchor, type) {
move( move(
suspense.isResolved ? suspense.subTree : suspense.fallbackTree, suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
container, container,
anchor anchor,
type
) )
suspense.container = container suspense.container = container
}, },

View File

@ -109,12 +109,20 @@ export interface RendererInternals<HostNode = any, HostElement = any> {
move: ( move: (
vnode: VNode<HostNode, HostElement>, vnode: VNode<HostNode, HostElement>,
container: HostElement, container: HostElement,
anchor: HostNode | null anchor: HostNode | null,
type: MoveType,
parentSuspense?: SuspenseBoundary<HostNode, HostElement> | null
) => void ) => void
next: (vnode: VNode<HostNode, HostElement>) => HostNode | null next: (vnode: VNode<HostNode, HostElement>) => HostNode | null
options: RendererOptions<HostNode, HostElement> options: RendererOptions<HostNode, HostElement>
} }
export const enum MoveType {
ENTER,
LEAVE,
REORDER
}
const prodEffectOptions = { const prodEffectOptions = {
scheduler: queueJob scheduler: queueJob
} }
@ -367,9 +375,6 @@ export function createRenderer<
invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode) invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
} }
} }
if (transition != null && !transition.persisted) {
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) {
@ -383,6 +388,9 @@ export function createRenderer<
optimized || vnode.dynamicChildren !== null optimized || vnode.dynamicChildren !== null
) )
} }
if (transition != null && !transition.persisted) {
transition.beforeEnter(el)
}
hostInsert(el, container, anchor) hostInsert(el, container, anchor)
const vnodeMountedHook = props && props.onVnodeMounted const vnodeMountedHook = props && props.onVnodeMounted
if ( if (
@ -747,7 +755,12 @@ export function createRenderer<
hostSetElementText(nextTarget, children as string) hostSetElementText(nextTarget, children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
for (let i = 0; i < (children as HostVNode[]).length; i++) { 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__) { } else if (__DEV__) {
@ -1372,7 +1385,7 @@ export function createRenderer<
// There is no stable subsequence (e.g. a reverse) // There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence // OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) { if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor) move(nextChild, container, anchor, MoveType.REORDER)
} else { } else {
j-- j--
} }
@ -1384,25 +1397,54 @@ export function createRenderer<
function move( function move(
vnode: HostVNode, vnode: HostVNode,
container: HostElement, container: HostElement,
anchor: HostNode | null anchor: HostNode | null,
type: MoveType,
parentSuspense: HostSuspenseBoundary | null = null
) { ) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT) { if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
move(vnode.component!.subTree, container, anchor) move(vnode.component!.subTree, container, anchor, type)
return return
} }
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) { if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
vnode.suspense!.move(container, anchor) vnode.suspense!.move(container, anchor, type)
return return
} }
if (vnode.type === Fragment) { if (vnode.type === Fragment) {
hostInsert(vnode.el!, container, anchor) hostInsert(vnode.el!, container, anchor)
const children = vnode.children as HostVNode[] const children = vnode.children as HostVNode[]
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
move(children[i], container, anchor) move(children[i], container, anchor, type)
} }
hostInsert(vnode.anchor!, container, anchor) hostInsert(vnode.anchor!, container, anchor)
} else { } 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)
}
} }
} }