干货 ——preact、react实现vue的transition组件

1,137 阅读2分钟
原文链接: divpc.cn

前言

最近在用preact做一个IM聊天软件的时候,特别想念之前用vue的transition做动画的体验了, 所以就照着vue的API简单的撸了一个动画组件;

待实现API

我这里选择了实现vue的transition中最常用的两种使用方式, 使用方式如下:

  1. 简单的css动画, 只需要传入一个name,以及准备一组css过度的样式,即可实现动画,css:
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s;
    }
    .fade-enter, .fade-leave-to {
      opacity: 0;
    }

模版使用:

class Demo extend Component{
    render({}, { visiable }) {
        return (
            <transition
              visiable={ visiable }
              name="fade"
            >
                <div class="demo">我是动画的正文</div>
            </transition>
        )
    }
}
  1. 通过回调函数beforeEnter、enter、afterEnter、beforeLeave、leave、afterLeave来控制DOM实现一些比较复杂的动画
    模版使用:
class Demo extend Component{
    beforeEnter(el) {}
    enter(el, done) {}
    afterEnter(el) {}
    beforeLeave(el) {}
    leave(el, done) {}
    afterLeave(el) {}
    
    render({}, { visiable }) {
        return (
            <transition
              visiable={ visiable }
              beforeEnter={ this.beforeEnter }
              enter={ this.enter }
              afterEnter={ this.afterEnter }
            
              beforeLeave={ this.beforeLeave }
              leave={ this.leave }
              afterLeavee={ this.afterLeave }
            >
                <div class="demo">我是动画的正文</div>
            </transition>
        )
    }
}

代码实现

  1. 首先创建一个基本的preact组件, 限制好组件的入参类型为TransitionProps; 定义一个变量控制组件中的DOM是否显示; 命名一个动画入口函数, 以及css、js动画具体实现的函数,代码如下:
import { h, Component } from "preact";

export interface TransitionProps {
  visiable: boolean,
  name?: string;
  children?: any,
  beforeEnter?: Function;
  enter?: Function;
  afterEnter?: Function;
  beforeLeave?: Function;
  leave?: Function;
  afterLeave?: Function;
}
export interface TransitionState {
  isShow: boolean;
}

export class Transition extends Component<TransitionProps> {
  private noop = function() {};

  public state: TransitionState = {
    isShow: false,
  };
  constructor(props: TransitionProps) {
    super(props);
  }
  private cssAnimation(node) {
  }
  private jsAnimation(node) {
  }
  private animationStart() {
    const node = this.base;
    const { name } = this.props;
    !!name ? this.cssAnimation(node) : this.jsAnimation(node);
  }
  componentWillReceiveProps(nextProps: TransitionProps) {
    nextProps.visiable && this.setState({ isShow: true });
  }
  componentDidUpdate(previousProps) {
    if (previousProps.visiable !== this.props.visiable) {
      this.animationStart();
    }
  }
  render(props: TransitionProps, { isShow }) {
    return (isShow &&  props.children[0]) ? props.children[0] : null;
  }
}

入参变化的时候判断visiable是否为true, 如果是true则需要把节点显示出来再进行接下来的动画; 这里还有一点需要注意的是,在节点更新的时候必须要判断当前的visiable和上一次的是否不一致, 不一致才需要启动动画. 2. 首先我们实现css动画, 也就是来把函数cssAnimation实现出来;

private cssAnimation(node) {
    const { visiable, name } = this.props;
    if (visiable) {
      const activeClass = `  ${name}-enter  ${name}-enter-active `;
      node.className += activeClass;
      setTimeout(() => { this.removeClassName(node, ` ${name}-enter`); });
      this.bindCSS3AnimationEvent(node, activeClass);
    } else {
      const activeClass = ` ${name}-leave-active`;
      node.className += activeClass;
      setTimeout(() => { node.className += ` ${name}-leave`; });
      this.bindCSS3AnimationEvent(node, activeClass);
    }
}

函数根据组件的入参visiable判断组件是enter还是leave状态;并根据不通的状态添加对应的css; 3. 监听css动画是否结束, 结束后需要删除动画的class:

  private bindCSS3AnimationEvent(elem, activeClass) {
    const { visiable } = this.props;
    const eventName = this.transitionend();
    elem.addEventListener(eventName, () => {
      this.removeClassName(elem, activeClass);
      !visiable && this.setState({ isShow: false });
    });
  }
  private transitionend() {
    let t;
    const el = document.createElement('surface');
    const transitions = {
      'transition': 'transitionend',
      'OTransition': 'oTransitionEnd',
      'MozTransition': 'transitionend',
      'WebkitTransition': 'webkitTransitionEnd'
    }

    for(t in transitions){
      if( el.style[t] !== undefined ){
        return transitions[t];
      }
    }
  }
  private removeClassName(elem, className) {
    if (!elem) return;
    const elClassName = elem.className;
    if (elClassName.length === 0) return;
    if(elClassName === className) {
      elem.className = '';
      return;
    }
    const classes = elClassName.split(' ');
    const removeClesses = className.split(' ');
    const newClasses = [];
    classes.forEach(curr => {
      if (curr !== '' && removeClesses.indexOf(curr) < 0) {
        newClasses.push(curr);
      }
    });
    elem.className = newClasses.join(' ');
  }

这里transitionend是对transitionend事件的浏览器兼容, removeClass函数就是给节点删除class的方法 4. 实现js动画, 填充实现函数jsAnimation

  private jsAnimation(node) {
    let { visiable, beforeEnter, enter, beforeLeave, leave } = this.props;
    beforeEnter = beforeEnter || this.noop;
    enter = enter || this.noop;
    beforeLeave = beforeLeave || this.noop;
    leave = leave || this.noop;

    if (visiable) {
      beforeEnter(node);
      setTimeout(() => enter(node, this.done.bind(this)));
    } else {
      beforeLeave(node);
      setTimeout(() => leave(node, this.done.bind(this)));
    }
  }

首先判断beforeEnter、enter、beforeLeave、leave回调是否传入, 如果没有传入则默认是一个空函数;在beforeEnter、beforeLeave执行后再调用enter、leave函数; 和vue的一样, enter、leave函数的第二个参数是一个done函数, 只有在enter、leave调用了done函数组件才会执行afterEnter、afterLeave函数

  1. 实现我们的done函数, 代码如下:
  private done() {
    const node = this.base
    let { visiable, afterEnter, afterLeave } = this.props;
    afterEnter = afterEnter || this.noop;
    afterLeave = afterLeave || this.noop;
    if (visiable) {
      afterEnter(node);
    } else {
      afterLeave(node);
      setTimeout(() => this.setState({ isShow: false }));
    }
  }

在这里, afterLeave执行后需要把节点删除,调:this.setState({ isShow: false })

最终代码

import { h, Component } from "preact";

export interface TransitionProps {
  visiable: boolean,
  name?: string;
  children?: any,
  beforeEnter?: Function;
  enter?: Function;
  afterEnter?: Function;
  beforeLeave?: Function;
  leave?: Function;
  afterLeave?: Function;
}
export interface TransitionState {
  isShow: boolean;
}

export class Transition extends Component<TransitionProps> {
  private noop = function() {};

  public state: TransitionState = {
    isShow: false,
  };
  constructor(props: TransitionProps) {
    super(props);
  }
  private cssAnimation(node) {
    const { visiable, name } = this.props;
    if (visiable) {
      const activeClass = `  ${name}-enter  ${name}-enter-active `;
      node.className += activeClass;
      setTimeout(() => { this.removeClassName(node, ` ${name}-enter`); });
      this.bindCSS3AnimationEvent(node, activeClass);
    } else {
      const activeClass = ` ${name}-leave-active`;
      node.className += activeClass;
      setTimeout(() => { node.className += ` ${name}-leave`; });
      this.bindCSS3AnimationEvent(node, activeClass);
    }
  }
  private bindCSS3AnimationEvent(elem, activeClass) {
    const { visiable } = this.props;
    const eventName = this.transitionend();
    elem.addEventListener(eventName, () => {
      this.removeClassName(elem, activeClass);
      !visiable && this.setState({ isShow: false });
    });
  }
  private transitionend() {
    let t;
    const el = document.createElement('surface');
    const transitions = {
      'transition': 'transitionend',
      'OTransition': 'oTransitionEnd',
      'MozTransition': 'transitionend',
      'WebkitTransition': 'webkitTransitionEnd'
    }

    for(t in transitions){
      if( el.style[t] !== undefined ){
        return transitions[t];
      }
    }
  }
  private removeClassName(elem, className) {
    if (!elem) return;
    const elClassName = elem.className;
    if (elClassName.length === 0) return;
    if(elClassName === className) {
      elem.className = '';
      return;
    }
    const classes = elClassName.split(' ');
    const removeClesses = className.split(' ');
    const newClasses = [];
    classes.forEach(curr => {
      if (curr !== '' && removeClesses.indexOf(curr) < 0) {
        newClasses.push(curr);
      }
    });
    elem.className = newClasses.join(' ');
  }
  private jsAnimation(node) {
    let { visiable, beforeEnter, enter, beforeLeave, leave } = this.props;
    beforeEnter = beforeEnter || this.noop;
    enter = enter || this.noop;
    beforeLeave = beforeLeave || this.noop;
    leave = leave || this.noop;

    if (visiable) {
      beforeEnter(node);
      setTimeout(() => enter(node, this.done.bind(this)));
    } else {
      beforeLeave(node);
      setTimeout(() => leave(node, this.done.bind(this)));
    }
  }
  private animationStart() {
    const node = this.base;
    const { name } = this.props;
    !!name ? this.cssAnimation(node) : this.jsAnimation(node);
  }
  private done() {
    const node = this.base
    let { visiable, afterEnter, afterLeave } = this.props;
    afterEnter = afterEnter || this.noop;
    afterLeave = afterLeave || this.noop;
    if (visiable) {
      afterEnter(node);
    } else {
      afterLeave(node);
      setTimeout(() => this.setState({ isShow: false }));
    }
  }
  componentWillReceiveProps(nextProps: TransitionProps) {
    nextProps.visiable && this.setState({ isShow: true });
  }
  componentDidUpdate(previousProps) {
    if (previousProps.visiable !== this.props.visiable) {
      this.animationStart();
    }
  }
  render(props: TransitionProps, { isShow }) {
    return (isShow &&  props.children[0]) ? props.children[0] : null;
  }
}