/** * `transition-group` 使用了取巧地实现方式。对 组件的每一个 Slot 包装进 TransitionGroupItem, * `TransitionGroupItem` 在 `onUnmount` 也就是实际销毁前,先将自身状态主动变更为已销毁,从而欺骗框架层跳过对该组件的销毁, * 也就保留了 dom 不被移除,直到 leave 动画结束后,才又将状态变为正常后调用 `destroyComponent` 函数执行实际的销毁。 */ import { ComponentHost, DEFAULT_SLOT_NAME } from '../../core'; import { COMPONENT_STATE_DESTROIED, COMPONENT_STATE_RENDERED, CONTEXT, ROOT_NODES, STATE, addUnmountFn, destroyComponent, renderFunctionComponent, renderSlotFunction, } from '../../core'; import type { FC, JNode, WithChildren } from '../../jsx'; import { type AnyFn, addEvent, throwErr } from '../../util'; import { For, type ForSlot } from '../for'; import type { EachVm, KEY_DATA, KEY_INDEX } from '../for/common'; import { TRANSITION_END, classnames2tokens } from './helper'; import type { TransitionClassnames } from './transition'; const CLASSNAMES = Symbol('classnames'); const APPEAR = Symbol('appear'); const ONDESTROY = Symbol('onDestroy'); export function TransitionGroupItem( props: { [CLASSNAMES]: string[][]; [APPEAR]: boolean; [ONDESTROY]: (fn: AnyFn) => AnyFn; } & WithChildren, host: ComponentHost, ) { const classTokens = props[CLASSNAMES]; const toggleClass = (el: Element, enter: boolean, init = false) => { const clist = el.classList; const ir = enter ? 2 : 0; const ia = enter ? 0 : 2; !init && clist.remove(...classTokens[ir], ...classTokens[ir + 1]); clist.add(...classTokens[ia], ...classTokens[ia + 1]); }; let el: ComponentHost | undefined = new ComponentHost(host[CONTEXT]); const nodes = renderSlotFunction(el, props[DEFAULT_SLOT_NAME] as FC); if (nodes.length > 1 || !(nodes[0] instanceof Element)) { throwErr('transition-require-element'); } const rootEl = nodes[0] as Element; let tm = 0; if (props[APPEAR]) { toggleClass(rootEl, false, true); tm = window.setTimeout(() => { toggleClass(rootEl, true); }, 10); } else { toggleClass(rootEl, true, true); } const finalDestroy = () => { if (!el) return; el[STATE] = COMPONENT_STATE_RENDERED; destroyComponent(el); el = undefined; }; // 在实际销毁前,先将自身状态主动变更为已销毁,从而欺骗框架层跳过对该组件的销毁,也就保留了 dom 不被移除。 // 但保留的 dom 在某种形式上成了垂悬对象,也就是即使父组件 TransitionGroup 整体被销毁时也还是存在,为了避免这个问题, // 通过 props[ONDESTROY] 向父组件注册监听器,当父组件整体都被销毁时,直接立即销毁当前保留的还在动画中的 dom const dereg = props[ONDESTROY](finalDestroy); addUnmountFn(host, () => { if (tm) clearTimeout(tm); if (!el) return; // 在实际销毁前,先将自身状态主动变更为已销毁,从而欺骗框架层跳过对该组件的销毁,也就保留了 dom 不被移除。 el[STATE] = COMPONENT_STATE_DESTROIED; toggleClass(rootEl, false); addEvent( rootEl, TRANSITION_END, () => { dereg(); // 将 finalDestroy 从监听列表删除,防止内存泄露。 // leave 动画结束后,才又将状态变为正常后调用 `destroyComponent` 函数执行实际的销毁。 finalDestroy(); }, { once: true }, ); }); host[ROOT_NODES].push(el); return nodes; } export interface TransitionGroupProps { loop: T[] | null | undefined; /** TransitionGroup 必须指定 key */ key: keyof T | typeof KEY_DATA | typeof KEY_INDEX; appear?: boolean; } export function TransitionGroup( props: TransitionGroupProps & TransitionClassnames & WithChildren>, hostRoot: ComponentHost, ) { const onDestroyNotifies = new Set(); addUnmountFn(hostRoot, () => { onDestroyNotifies.forEach((notifyFn) => notifyFn()); onDestroyNotifies.clear(); }); let innerAppear = !!props.appear; const origChildren = props[DEFAULT_SLOT_NAME] as FC; const renderEachFn = (forEachVm: EachVm, host: ComponentHost) => { const itemProps = { [CLASSNAMES]: classnames2tokens(props), [APPEAR]: innerAppear, [ONDESTROY]: (fn: AnyFn) => { onDestroyNotifies.add(fn); return () => onDestroyNotifies.delete(fn); }, [DEFAULT_SLOT_NAME]: (_: unknown, tranHost: ComponentHost) => { return origChildren?.(forEachVm, tranHost) ?? []; }, }; const el = new ComponentHost(host[CONTEXT]); host[ROOT_NODES].push(el); return renderFunctionComponent(el, TransitionGroupItem as FC, itemProps); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any props[DEFAULT_SLOT_NAME] = renderEachFn as any; const el = new ComponentHost(hostRoot[CONTEXT]); hostRoot[ROOT_NODES].push(el); const nodes = renderFunctionComponent(el, For, props); innerAppear = true; return nodes; }