vue实现过渡动画的过程
在进入/离开的过渡中,会有 6 个 class 切换。
v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。(开始显示) (元素开始状态)v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。 (transition 和 animation)v-enter-to:2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时v-enter被移除),在过渡/动画完成之后移除。 (元素结束状态)v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。(开始隐藏)v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to:2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时v-leave被删除),在过渡/动画完成之后移除。
1.1 enter和leave
// 元素 从无到有
const enter = (vnode: any, toggleDisplay?: () => void) => {
const el = vnode.elm;
const data = resolveTransition(vnode.data.transition) as any;
if (isUndef(data)) {
return;
}
const {
css,
state,
name,
type,
children,
enterClass,
enterActiveClass,
enterToClass,
appearClass,
appearActiveClass,
appearToClass,
beforeEnter,
enter,
afterEnter,
enterCancelled,
beforeAppear,
appear,
afterAppear,
appearCancelled,
duration,
} = data;
// 执行 leave 回调
if (isDef(el._leaveCb)) {
el._leaveCb.cancelled = true
el._leaveCb();
}
const isAppear = true; // 初始渲染时是否显示过渡
//
const startClass = isAppear && appearClass ? appearClass : enterClass;
const activeClass = isAppear && appearActiveClass ? appearActiveClass : enterActiveClass;
const toClass = isAppear && appearToClass ? appearToClass : enterToClass;
//
const beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter;
const enterHook = isAppear ? (typeof appear === 'function' || enter) : enter;
const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter;
const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled;
//
const explicitEnterDuration: any = toNumber(isObject(duration) ? duration.enter : duration);
// const expectsCSS = css !== false && !isIE9;
const expectsCSS = true; //
const userWantsControl = getHookArgumentsLength(enterHook); // enterHook 函数参数大于1为true
// 动画结束后执行的回调函数
el._enterCb = once(() => {
if (expectsCSS) {
removeClass(el, toClass);
removeClass(el, activeClass);
}
// 取消执行
if (el._enterCb?.cancelled) {
if (expectsCSS) {
removeClass(el, startClass);
}
enterCancelledHook && enterCancelledHook(el);
}
else {
afterEnterHook && afterEnterHook(el);
}
// 执行完置为空
el._enterCb = null;
})
// 开始过渡
beforeEnterHook && beforeEnterHook(el);
if (expectsCSS) {
addClass(el, startClass);
addClass(el, activeClass);
nextFrame(() => {
removeClass(el, startClass);
if (!el._enterCb?.cancelled) {
if (expectsCSS) {
addClass(el, toClass);
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(el._enterCb, explicitEnterDuration);
} else {
whenTransitionEnds(el, el._enterCb, type);
}
}
}
}
})
}
if (vnode.data.show) {
toggleDisplay && toggleDisplay();
enterHook && enterHook(el, el._enterCb);
}
if (!expectsCSS && !userWantsControl) {
el._enterCb();
}
}
// 元素从有到无
const leave = (vnode: any, rm: Function) => {
const el: any = vnode.elm
if (isDef(el._enterCb)) {
el._enterCb.cancelled = true
el._enterCb();
}
const data = resolveTransition(vnode.data.transition) as any;
if (isUndef(data) || el.nodeType !== 1) {
return rm()
}
if (isDef(el._leaveCb)) {
return
}
const {
css,
type,
leaveClass,
leaveToClass,
leaveActiveClass,
beforeLeave,
leave,
afterLeave,
leaveCancelled,
delayLeave,
duration
} = data
// const expectsCSS = css !== false && !isIE9
const expectsCSS = true;
const userWantsControl = getHookArgumentsLength(leave)
const explicitLeaveDuration: any = toNumber(
isObject(duration)
? duration.leave
: duration
)
el._leaveCb = once(() => {
if (el.parentNode && el.parentNode._pending) {
el.parentNode._pending[vnode.key] = null;
}
if (expectsCSS) {
removeClass(el, leaveToClass);
removeClass(el, leaveActiveClass);
}
if (el._leaveCb.cancelled) {
if (expectsCSS) {
removeClass(el, leaveClass);
}
leaveCancelled && leaveCancelled(el);
} else {
rm();
afterLeave && afterLeave(el);
}
el._leaveCb = null;
})
if (delayLeave) {
delayLeave(performLeave);
} else {
performLeave();
}
function performLeave() {
if (el._leaveCb.cancelled) {
return;
}
if (!vnode.data.show && el.parentNode) {
(el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key)] = vnode;
}
beforeLeave && beforeLeave(el);
if (expectsCSS) {
addClass(el, leaveClass)
addClass(el, leaveActiveClass)
nextFrame(() => {
removeClass(el, leaveClass)
if (!el._leaveCb?.cancelled) {
addClass(el, leaveToClass)
if (!userWantsControl) {
if (isValidDuration(explicitLeaveDuration)) {
setTimeout(el._leaveCb, explicitLeaveDuration)
} else {
whenTransitionEnds(el, el._leaveCb, type)
}
}
}
})
}
leave && leave(el, el._leaveCb);
if (!expectsCSS && !userWantsControl) {
el._leaveCb();
}
}
}
1.2 Transition组件
const Transition = (props: {
name?: string,
type?: 'transition' | 'animation',
show: boolean,
beforeEnter?: Function,
enter?: Function,
afterEnter?: Function,
enterCancelled?: Function,
beforeLeave?: Function,
leave?: Function,
afterLeave?: Function,
leaveCancelled?: Function,
children?: React.JSX.Element
}) => {
const elRef = useRef(null);
const mounted = useRef<boolean>(false);
const vnode: any = {
data: {
show: true,
transition: {
...props
}
}
}
useEffect(() => {
const el: any = ((elRef.current as unknown as HTMLElement).children[0] as HTMLElement);
vnode.elm = el;
const transition = vnode.data && vnode.data.transition;
if (!mounted.current) {
mounted.current = true;
const originalDisplay = el.__originalDisplay = el.style.display === 'none' ? '' : el.style.display;
// 原v-show逻辑,此处transition必为true
if (props.show && transition) {
enter(vnode, () => el.style.display = originalDisplay);
}
else {
el.style.display = props.show ? originalDisplay : 'none';
}
}
else {
// componentDidUpdate生命周期
if (isUndef(props.show)) return;
if (transition) {
if (props.show) {
enter(vnode, () => {
el.style.display = el.__originalDisplay;
})
} else {
leave(vnode, () => {
el.style.display = 'none';
})
}
} else {
el.style.display = props.show ? el.__originalDisplay : 'none';
}
}
return () => {
el.style.display = el.__originalDisplay;
}
})
return <div ref={elRef}>{props.children}</div>
}
1.3 fade淡入淡出
// fade-in
.fade-in-enter-active,
.fade-in-leave-active {
transition: opacity 0.4s;
}
.fade-in-enter,
.fade-in-leave-to {
opacity: 0;
}
.fade-in-enter-to,
.fade-in-leave {
opacity: 1;
}
// fade-in-linear
.fade-in-linear-enter-active,
.fade-in-linear-leave-active {
transition: opacity 0.4s linear;
}
.fade-in-linear-enter,
.fade-in-linear-leave-to {
opacity: 0;
}
.fade-in-linear-enter-to,
.fade-in-linear-leave {
opacity: 1;
}
2.4 zoom缩放
// zoom-in-top
.zoom-in-top-enter-active,
.zoom-in-top-leave-active {
opacity: 1;
transform: scaleY(1);
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center top; // 变换源点
}
.zoom-in-top-enter,
.zoom-in-top-leave-to {
opacity: 0;
transform: scaleY(0);
}
.zoom-in-top-enter-to,
.zoom-in-top-leave {
opacity: 1;
transform: scaleY(1);
}
// zoom-in-bottom
.zoom-in-bottom-enter-active,
.zoom-in-bottom-leave-active {
opacity: 1;
transform: scaleY(1);
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center bottom;
}
.zoom-in-bottom-enter,
.zoom-in-bottom-leave-to {
opacity: 0;
transform: scaleY(0);
}
.zoom-in-bottom-enter-to,
.zoom-in-bottom-leave {
opacity: 1;
transform: scaleY(1);
}
// zoom-in-center
.zoom-in-center-enter-active,
.zoom-in-center-leave-active {
opacity: 1;
transform: scaleY(1);
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center center;
}
.zoom-in-center-enter,
.zoom-in-center-leave-to {
opacity: 0;
transform: scaleX(0);
}
.zoom-in-center-enter-to,
.zoom-in-center-leave {
opacity: 1;
transform: scaleX(1);
}
1.4.1 Transition组件测试
import CollapseTransition from "@/components/common/Transition/CollapseTransition";
function App() {
const [state, setState] = useState<boolean>(false);
function handleClick() {
setState(v => !v);
}
return <>
<Button onClick={() => handleClick()}>按钮</Button>
<Transition show={state} name="zoom-in-top">
<div className='transition-box'>.zoom-in-top</div>
</Transition>
<Transition show={state} name="zoom-in-bottom">
<div className='transition-box'>.zoom-in-bottom</div>
</Transition>
<Transition show={state} name="zoom-in-center">
<div className='transition-box'>.zoom-in-center</div>
</Transition>
</>
}
1.5 collapse展开折叠
// collapse-transition
.collapse-transition {
transition: height 0.3s ease-in-out, padding-top 0.3s ease-in-out, padding-bottom 0.3s ease-in-out;
}
CollapseTransition.ts
function TransitionHooks() {
const transition = {
beforeEnter,
enter,
afterEnter,
beforeLeave,
leave,
afterLeave
}
function beforeEnter(el: any) {
addClass(el, 'collapse-transition');
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.style.height = '0';
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
function enter(el: any) {
el.dataset.oldOverflow = el.style.overflow;
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + 'px';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
} else {
el.style.height = '';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
el.style.overflow = 'hidden';
}
function afterEnter(el: any) {
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
}
function beforeLeave(el: any) {
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.dataset.oldOverflow = el.style.overflow;
el.style.height = el.scrollHeight + 'px';
el.style.overflow = 'hidden';
}
function leave(el: any) {
if (el.scrollHeight !== 0) {
addClass(el, 'collapse-transition');
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
}
function afterLeave(el: any) {
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
return transition;
}
function CollapseTransition(props: {
name?: string;
show: boolean;
children?: React.JSX.Element;
}) {
const transition = TransitionHooks() as any;
TransitionHooks.prototype[Symbol.iterator] = function () {
return Object.values(this)[Symbol.iterator]();
}
return <Transition show={props.show} {...transition}>
{props.children}
</Transition>
}
测试
import CollapseTransition from "@/components/common/Transition/CollapseTransition";
function App() {
const [state, setState] = useState<boolean>(false);
function handleClick() {
setState(v => !v);
}
return <>
<Button onClick={() => handleClick()}>按钮</Button>
<CollapseTransition show={state}>
<div className="transition-box">
<li>123</li>
<li>123</li>
<li>123</li>
</div>
</CollapseTransition>
</>
}