]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(transition): base transition component
authorEvan You <yyx990803@gmail.com>
Wed, 20 Nov 2019 23:04:44 +0000 (18:04 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 21 Nov 2019 03:46:32 +0000 (22:46 -0500)
packages/runtime-core/src/components/KeepAlive.ts
packages/runtime-core/src/components/Transition.ts [new file with mode: 0644]
packages/runtime-core/src/directives.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts

index 0f37c99961695c27e6d7ded6a13330701bd15407..50bb946cc0a6aa4302040f5d034b8087756e4898 100644 (file)
@@ -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 (): {
diff --git a/packages/runtime-core/src/components/Transition.ts b/packages/runtime-core/src/components/Transition.ts
new file mode 100644 (file)
index 0000000..8b96af7
--- /dev/null
@@ -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
+  }
+}
index 9f77358706b83d1951848b6ddb3c72a9825cff8a..4f80bb1f23171261199e624e394fc6f2a544073b 100644 (file)
@@ -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
index 7729342f2bf5079a3784a6d266f54e365d475e05..4c241a57a787777b1f2342dd90f6358ee4170c86 100644 (file)
@@ -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'
index 2cae06e92ee8937276dd0afe36981d8bed340a63..5ca2ca06313011350da633570a2d135ca89968ef 100644 (file)
@@ -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) {
-      hostRemove(vnode.el!)
-      if (anchor != null) hostRemove(anchor)
+      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,
index 71227f3d9e94e40e82d54c2663ccbb6950f9f4ab..3134057feebede682a9f9ff2308c8938b1fb0326 100644 (file)
@@ -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++) {