根据vue的transition组件仿写一个简易的react版本的Transition组件

88 阅读4分钟

vue实现过渡动画的过程

在进入/离开的过渡中,会有 6 个 class 切换。

  1. v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。(开始显示) (元素开始状态)
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。 (transition 和 animation)
  3. v-enter-to2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。 (元素结束状态)
  4. v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。(开始隐藏)
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to2.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>
        </> 
}