BaseTransition.ts 13 KB

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