BaseTransition.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import {
  2. getCurrentInstance,
  3. SetupContext,
  4. ComponentOptions,
  5. ComponentInternalInstance
  6. } from '../component'
  7. import {
  8. cloneVNode,
  9. Comment,
  10. isSameVNodeType,
  11. VNode,
  12. VNodeArrayChildren
  13. } from '../vnode'
  14. import { warn } from '../warning'
  15. import { isKeepAlive } from './KeepAlive'
  16. import { toRaw } from '@vue/reactivity'
  17. import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
  18. import { ShapeFlags } from '../shapeFlags'
  19. import { onBeforeUnmount, onMounted } from '../apiLifecycle'
  20. export interface BaseTransitionProps {
  21. mode?: 'in-out' | 'out-in' | 'default'
  22. appear?: boolean
  23. // If true, indicates this is a transition that doesn't actually insert/remove
  24. // the element, but toggles the show / hidden status instead.
  25. // The transition hooks are injected, but will be skipped by the renderer.
  26. // Instead, a custom directive can control the transition by calling the
  27. // injected hooks (e.g. v-show).
  28. persisted?: boolean
  29. // Hooks. Using camel case for easier usage in render functions & JSX.
  30. // In templates these can be written as @before-enter="xxx" as prop names
  31. // are camelized.
  32. onBeforeEnter?: (el: any) => void
  33. onEnter?: (el: any, done: () => void) => void
  34. onAfterEnter?: (el: any) => void
  35. onEnterCancelled?: (el: any) => void
  36. // leave
  37. onBeforeLeave?: (el: any) => void
  38. onLeave?: (el: any, done: () => void) => void
  39. onAfterLeave?: (el: any) => void
  40. onLeaveCancelled?: (el: any) => void // only fired in persisted mode
  41. }
  42. export interface TransitionHooks {
  43. persisted: boolean
  44. beforeEnter(el: object): void
  45. enter(el: object): void
  46. leave(el: object, remove: () => void): void
  47. afterLeave?(): void
  48. delayLeave?(
  49. el: object,
  50. earlyRemove: () => void,
  51. delayedLeave: () => void
  52. ): void
  53. delayedLeave?(): void
  54. }
  55. type TransitionHookCaller = (
  56. hook: ((el: any) => void) | undefined,
  57. args?: any[]
  58. ) => void
  59. export type PendingCallback = (cancelled?: boolean) => void
  60. export interface TransitionState {
  61. isMounted: boolean
  62. isLeaving: boolean
  63. isUnmounting: boolean
  64. // Track pending leave callbacks for children of the same key.
  65. // This is used to force remove leaving a child when a new copy is entering.
  66. leavingVNodes: Map<any, Record<string, VNode>>
  67. }
  68. export interface TransitionElement {
  69. // in persisted mode (e.g. v-show), the same element is toggled, so the
  70. // pending enter/leave callbacks may need to cancalled if the state is toggled
  71. // before it finishes.
  72. _enterCb?: PendingCallback
  73. _leaveCb?: PendingCallback
  74. }
  75. export function useTransitionState(): TransitionState {
  76. const state: TransitionState = {
  77. isMounted: false,
  78. isLeaving: false,
  79. isUnmounting: false,
  80. leavingVNodes: new Map()
  81. }
  82. onMounted(() => {
  83. state.isMounted = true
  84. })
  85. onBeforeUnmount(() => {
  86. state.isUnmounting = true
  87. })
  88. return state
  89. }
  90. const BaseTransitionImpl = {
  91. name: `BaseTransition`,
  92. setup(props: BaseTransitionProps, { slots }: SetupContext) {
  93. const instance = getCurrentInstance()!
  94. const state = useTransitionState()
  95. return () => {
  96. const children = slots.default && slots.default()
  97. if (!children || !children.length) {
  98. return
  99. }
  100. // warn multiple elements
  101. if (__DEV__ && children.length > 1) {
  102. warn(
  103. '<transition> can only be used on a single element or component. Use ' +
  104. '<transition-group> for lists.'
  105. )
  106. }
  107. // there's no need to track reactivity for these props so use the raw
  108. // props for a bit better perf
  109. const rawProps = toRaw(props)
  110. const { mode } = rawProps
  111. // check mode
  112. if (__DEV__ && mode && !['in-out', 'out-in', 'default'].includes(mode)) {
  113. warn(`invalid <transition> mode: ${mode}`)
  114. }
  115. // at this point children has a guaranteed length of 1.
  116. const child = children[0]
  117. if (state.isLeaving) {
  118. return emptyPlaceholder(child)
  119. }
  120. // in the case of <transition><keep-alive/></transition>, we need to
  121. // compare the type of the kept-alive children.
  122. const innerChild = getKeepAliveChild(child)
  123. if (!innerChild) {
  124. return emptyPlaceholder(child)
  125. }
  126. const enterHooks = (innerChild.transition = resolveTransitionHooks(
  127. innerChild,
  128. rawProps,
  129. state,
  130. instance
  131. ))
  132. const oldChild = instance.subTree
  133. const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
  134. // handle mode
  135. if (
  136. oldInnerChild &&
  137. oldInnerChild.type !== Comment &&
  138. !isSameVNodeType(innerChild, oldInnerChild)
  139. ) {
  140. const prevHooks = oldInnerChild.transition!
  141. const leavingHooks = resolveTransitionHooks(
  142. oldInnerChild,
  143. rawProps,
  144. state,
  145. instance
  146. )
  147. // update old tree's hooks in case of dynamic transition
  148. setTransitionHooks(oldInnerChild, leavingHooks)
  149. // switching between different views
  150. if (mode === 'out-in') {
  151. state.isLeaving = true
  152. // return placeholder node and queue update when leave finishes
  153. leavingHooks.afterLeave = () => {
  154. state.isLeaving = false
  155. instance.update()
  156. }
  157. return emptyPlaceholder(child)
  158. } else if (mode === 'in-out') {
  159. delete prevHooks.delayedLeave
  160. leavingHooks.delayLeave = (
  161. el: TransitionElement,
  162. earlyRemove,
  163. delayedLeave
  164. ) => {
  165. const leavingVNodesCache = getLeavingNodesForType(
  166. state,
  167. oldInnerChild
  168. )
  169. leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
  170. // early removal callback
  171. el._leaveCb = () => {
  172. earlyRemove()
  173. el._leaveCb = undefined
  174. delete enterHooks.delayedLeave
  175. }
  176. enterHooks.delayedLeave = delayedLeave
  177. }
  178. }
  179. }
  180. return child
  181. }
  182. }
  183. }
  184. if (__DEV__) {
  185. ;(BaseTransitionImpl as ComponentOptions).props = {
  186. mode: String,
  187. appear: Boolean,
  188. persisted: Boolean,
  189. // enter
  190. onBeforeEnter: Function,
  191. onEnter: Function,
  192. onAfterEnter: Function,
  193. onEnterCancelled: Function,
  194. // leave
  195. onBeforeLeave: Function,
  196. onLeave: Function,
  197. onAfterLeave: Function,
  198. onLeaveCancelled: Function
  199. }
  200. }
  201. // export the public type for h/tsx inference
  202. // also to avoid inline import() in generated d.ts files
  203. export const BaseTransition = (BaseTransitionImpl as any) as {
  204. new (): {
  205. $props: BaseTransitionProps
  206. }
  207. }
  208. function getLeavingNodesForType(
  209. state: TransitionState,
  210. vnode: VNode
  211. ): Record<string, VNode> {
  212. const { leavingVNodes } = state
  213. let leavingVNodesCache = leavingVNodes.get(vnode.type)!
  214. if (!leavingVNodesCache) {
  215. leavingVNodesCache = Object.create(null)
  216. leavingVNodes.set(vnode.type, leavingVNodesCache)
  217. }
  218. return leavingVNodesCache
  219. }
  220. // The transition hooks are attached to the vnode as vnode.transition
  221. // and will be called at appropriate timing in the renderer.
  222. export function resolveTransitionHooks(
  223. vnode: VNode,
  224. {
  225. appear,
  226. persisted = false,
  227. onBeforeEnter,
  228. onEnter,
  229. onAfterEnter,
  230. onEnterCancelled,
  231. onBeforeLeave,
  232. onLeave,
  233. onAfterLeave,
  234. onLeaveCancelled
  235. }: BaseTransitionProps,
  236. state: TransitionState,
  237. instance: ComponentInternalInstance
  238. ): TransitionHooks {
  239. const key = String(vnode.key)
  240. const leavingVNodesCache = getLeavingNodesForType(state, vnode)
  241. const callHook: TransitionHookCaller = (hook, args) => {
  242. hook &&
  243. callWithAsyncErrorHandling(
  244. hook,
  245. instance,
  246. ErrorCodes.TRANSITION_HOOK,
  247. args
  248. )
  249. }
  250. const hooks: TransitionHooks = {
  251. persisted,
  252. beforeEnter(el: TransitionElement) {
  253. if (!appear && !state.isMounted) {
  254. return
  255. }
  256. // for same element (v-show)
  257. if (el._leaveCb) {
  258. el._leaveCb(true /* cancelled */)
  259. }
  260. // for toggled element with same key (v-if)
  261. const leavingVNode = leavingVNodesCache[key]
  262. if (
  263. leavingVNode &&
  264. isSameVNodeType(vnode, leavingVNode) &&
  265. leavingVNode.el._leaveCb
  266. ) {
  267. // force early removal (not cancelled)
  268. leavingVNode.el._leaveCb()
  269. }
  270. callHook(onBeforeEnter, [el])
  271. },
  272. enter(el: TransitionElement) {
  273. if (!appear && !state.isMounted) {
  274. return
  275. }
  276. let called = false
  277. const afterEnter = (el._enterCb = (cancelled?) => {
  278. if (called) return
  279. called = true
  280. if (cancelled) {
  281. callHook(onEnterCancelled, [el])
  282. } else {
  283. callHook(onAfterEnter, [el])
  284. }
  285. if (hooks.delayedLeave) {
  286. hooks.delayedLeave()
  287. }
  288. el._enterCb = undefined
  289. })
  290. if (onEnter) {
  291. onEnter(el, afterEnter)
  292. } else {
  293. afterEnter()
  294. }
  295. },
  296. leave(el: TransitionElement, remove) {
  297. const key = String(vnode.key)
  298. if (el._enterCb) {
  299. el._enterCb(true /* cancelled */)
  300. }
  301. if (state.isUnmounting) {
  302. return remove()
  303. }
  304. callHook(onBeforeLeave, [el])
  305. let called = false
  306. const afterLeave = (el._leaveCb = (cancelled?) => {
  307. if (called) return
  308. called = true
  309. remove()
  310. if (cancelled) {
  311. callHook(onLeaveCancelled, [el])
  312. } else {
  313. callHook(onAfterLeave, [el])
  314. }
  315. el._leaveCb = undefined
  316. if (leavingVNodesCache[key] === vnode) {
  317. delete leavingVNodesCache[key]
  318. }
  319. })
  320. leavingVNodesCache[key] = vnode
  321. if (onLeave) {
  322. onLeave(el, afterLeave)
  323. } else {
  324. afterLeave()
  325. }
  326. }
  327. }
  328. return hooks
  329. }
  330. // the placeholder really only handles one special case: KeepAlive
  331. // in the case of a KeepAlive in a leave phase we need to return a KeepAlive
  332. // placeholder with empty content to avoid the KeepAlive instance from being
  333. // unmounted.
  334. function emptyPlaceholder(vnode: VNode): VNode | undefined {
  335. if (isKeepAlive(vnode)) {
  336. vnode = cloneVNode(vnode)
  337. vnode.children = null
  338. return vnode
  339. }
  340. }
  341. function getKeepAliveChild(vnode: VNode): VNode | undefined {
  342. return isKeepAlive(vnode)
  343. ? vnode.children
  344. ? ((vnode.children as VNodeArrayChildren)[0] as VNode)
  345. : undefined
  346. : vnode
  347. }
  348. export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks) {
  349. if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
  350. setTransitionHooks(vnode.component.subTree, hooks)
  351. } else {
  352. vnode.transition = hooks
  353. }
  354. }