]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(transition): compat with keep-alive
authorEvan You <yyx990803@gmail.com>
Mon, 25 Nov 2019 22:34:28 +0000 (17:34 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 25 Nov 2019 22:35:15 +0000 (17:35 -0500)
packages/compiler-core/__tests__/transforms/transformElement.spec.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/componentSlots.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/components/KeepAlive.ts
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/renderer.ts

index 078ea9bc255b70c33cd6be12cd7857b1dd1b5d9f..69cbce0060974fcc0a5f867cbcef5aafd326b302 100644 (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' }]
       ])
     }
 
index e440bb66bfde9723b4b2da447a091ae9c768c3de..8e71a714d10ee4ca5f9e6172fbd8f2f2120422b7 100644 (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) {
index 7663677c67109b9917c400f86e832234646ea349..ed62cce233a1b005984e0c6bf554de6b15e64396 100644 (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 ` +
index 52dd6f4f8a1885035374db6c8b20b40d013f7f17..5821f38e85b7828fb0b4c0ae69156c8e7fc2fd38 100644 (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
 }
index 528e9e9069b1ed0edd62fae49874cc4daa5e1b87..34403cfc145851770bb8750f3d82564899216c5e 100644 (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
   }
 }
index 3040b453b4d511c9d2d548aeb9e707f88278438f..10c6389120cecbb6c7528cc2d4993bad86652e9e 100644 (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
index 101d86f7e1ae65f996a83ccf8affdb5685e5507e..d075ce12d325432013bede017ba46abc782f7e2d 100644 (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
     },
index ec79c1e3c409efcfc39169dcd5e5fccea98a5b2f..6a91d7e60fce14b7cc41f7573f9b561b4af756bd 100644 (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)
+      }
     }
   }