react动画开源库代码学习之rc-animate

rc-animate

rc-animate是这个库在npm上的名字,他在github搜索的话得用animate去搜索。(下面就都简称为animate了)
animate是阿里的库,ant-design也是使用的他完成动画的一部分内容。
官网是这样介绍的:

对单个元素根据状态进行动画显示隐藏,需结合 css 或其它第三方动画类一起使用

代码结构

code.PNG

rc-animate的源代码src文件夹下只有Animate,AnimateChild,ChildrenUtils,Utils这四个js文件,后两个utils是工具类js,用来检测组件的props是否含有某个值这样的工作等,不过多介绍。
Animate.js是我们库暴露给我们使用的组件,他会在内部做一些检测,回调等等操作。然后调用AnimateChild组件,AnimateChild是实际实现动画的地方。AnimateChild中使用了css-animate这个库,css-animate才是最核心的操作,他将给dom元素添加指定的css类名。另一个animateUtils是上一层的utils.js这个文件,我写错了,这里说明一下。
先从rc-animate的最外层,我们最先接触到的Animate.js讲起。

Animate.js

简书上写大段的代码太费劲了,我就一小段一小段的解释吧。

constructor

这个没什么难点,就是普通的初始化。currentlyAnimatingKeys和childrenRefs在后续会比较重要,使用次数比较多

constructor(props) {
  super(props);
  // 这个数组用来储存正在进行动画的dom节点的key值
  this.currentlyAnimatingKeys = {};
  // 这两个数组就顾名思义啦
  this.keysToEnter = [];
  this.keysToLeave = [];
  this.state = {
    // 这里的toArrayChildren和getChildrenFromProps都是上面讲的两个工具类里的工具方法
    children: toArrayChildren(getChildrenFromProps(props)),
  };
  // 储存ref
  this.childrenRefs = {};
}

componentDidMount

componentDidMount() {
  const showProp = this.props.showProp;
  let children = this.state.children;
  // showProp是Animate组件提供的一个API,原文解释为:'子级动画的类型,显示或隐藏'
  // 完全不理解他的意思,刚好官网的代码示例也没有用带这个参数,我们就暂且跳过他,给他一个null值吧
  if (showProp) {
    children = children.filter((child) => {
      return !!child.props[showProp];
    });
  }
  // 遍历子节点,并调用performAppear方法,这个方法名翻译一下就是:‘执行显示’,所以这就是我们的css效果的第一个触发点
  // 完成组件挂载后,让组件执行css动画,也就是常见的淡入效果之类的。
  children.forEach((child) => {
    if (child) {
      this.performAppear(child.key);
    }
  });
}

performAppear

performAppear这个方法是Animate组件自己的实例方法

performAppear = (key) => {
  if (this.childrenRefs[key]) {
    // 用key值找到目标组件,然后对currentlyAnimatingKeys数组做一个标识,告诉大家,这个组件正在进行动画
    this.currentlyAnimatingKeys[key] = true;
    // 调用实例上的方法,这个componentWillAppear方法,是Ref对应的实例自己提供的一个实例方法(这里实例对应的Class是AnimateChild)
   // 可以将componentWillAppear理解为AnimateChild的一个生命周期函数,Animate就是执行这个生命周期函数的框架
  // 传递给可以将componentWillAppear的方法是一个回调函数
    this.childrenRefs[key].componentWillAppear(
      this.handleDoneAdding.bind(this, key, 'appear')
    );
  }
}

我想按照调用组件时代码的执行顺序来梳理整个库的代码,所以接下来不一一说明Animate类里的方法了,而是讲他的render函数与AnimateChild.js的内容

render

    render() {
      const props = this.props;
      this.nextProps = props;
      const stateChildren = this.state.children;
      let children = null;
      // 先map拿到被AnimateChild包装好的组件数组
      if (stateChildren) {
        children = stateChildren.map((child) => {
          if (child === null || child === undefined) {
            return child;
          }
          if (!child.key) {
            throw new Error('must set key for <rc-animate> children');
          }
          // 返回的AnimateChild组件,childrenRefs数组里储存的就是AnimateChild组件的实例
          return (
            <AnimateChild
              key={child.key}
              ref={node => this.childrenRefs[child.key] = node}
              animation={props.animation}
              transitionName={props.transitionName}
              transitionEnter={props.transitionEnter}
              transitionAppear={props.transitionAppear}
              transitionLeave={props.transitionLeave}
            >
              {child}
            </AnimateChild>
          );
        });
      }
      const Component = props.component;
      // 这个component是暴露出来的api,用来指定需要替换的标签
      // 这里可以看到,如果没有component的话,会只渲染组件数组的第一项
      // 有component则会全部渲染,因该是为了保证有一个父级节点去包裹,以便进行动画
      if (Component) {
        let passedProps = props;
        if (typeof Component === 'string') {
          passedProps = {
            className: props.className,
            style: props.style,
            ...props.componentProps,
          };
        }
        return <Component {...passedProps}>{children}</Component>;
      }
      return children[0] || null;
    }

AnimateChild

AnimateChild组件并没有react原生的生命周期函数包括constructor,render函数也只是简单的返回了this.props.children。由此可见,AnimateChild其实是一个类,提供一些接口。下面,主要了解一下他的接口都是做什么的。

先从我们上边讲的performAppear函数调用的componentWillAppear函数开始讲

componentWillAppear

AnimateChild组件的componentWillAppear,componentWillEnter,componentWillLeave全都一样,判断一下是否做动画,做就调用transition方法,不然就调用done。
然后说transition,这是AnimateChild的核心方法

    componentWillAppear(done) {
      if (animUtil.isAppearSupported(this.props)) {
        this.transition('appear', done);
      } else {
        done();
      }
    }

transition

    transition(animationType, finishCallback) {
      const node = ReactDOM.findDOMNode(this);
      const props = this.props;
      const transitionName = props.transitionName;
      const nameIsObj = typeof transitionName === 'object';
      this.stop();
      const end = () => {
        this.stopper = null;
        finishCallback();
      };
      // props.animation是如果使用第三方动画类库需要传入的对象,props[transitionMap[animationType]]是一个布尔值,对应着transitionAppear等三个api
    // 这个判断翻译过来就是,支持css动画或者不使用css类库并且有自己的css类名并且的确要做这一步的动画
      if ((isCssAnimationSupported || !props.animation[animationType]) &&
        transitionName && props[transitionMap[animationType]]) {
        // 这一大串,都是在拼接字符串,拼接出要添加的类名
        const name = nameIsObj ? transitionName[animationType] : `${transitionName}-${animationType}`;
        let activeName = `${name}-active`;
        if (nameIsObj && transitionName[`${animationType}Active`]) {
          activeName = transitionName[`${animationType}Active`];
        }
        // 核心中的核心,调用cssAnimate方法并传入回调
        this.stopper = cssAnimate(node, {
          name,
          active: activeName,
        }, end);
      } else {
        this.stopper = props.animation[animationType](node, end);
      }
    }

cssAnimate

cssAnimate库包含两个js文件,一个index.js是提供给我们使用的,用来触发动画或者过渡。另一个event.js是支撑index.js的运行的,event.js在内部封装了函数,用来检测浏览器是否执行完了css动画。(这是一个新的知识点,之前从未了解过。上边讲的cssAnimate方法是index.js提供的。下面我们先说他。

index

index.js里的工具方法与细枝末节就不说了,直接说cssAnimate这个函数

    const cssAnimation = (node, transitionName, endCallback) => {
      // 先是一大串字符串拼接,得出要添加的class类名
      const nameIsObj = typeof transitionName === 'object';
      const className = nameIsObj ? transitionName.name : transitionName;
      const activeClassName = nameIsObj ? transitionName.active : `${transitionName}-active`;
      let end = endCallback;
      let start;
      let active;
      // 这个classes是引用的component-classes这个库的api,用来跨浏览器操作dom元素的类名,理解为jq吧,但是只有类名的增删改查
      const nodeClasses = classes(node);
      // 如果是对象,就解构一下,拿到真正的回调函数
      if (endCallback && Object.prototype.toString.call(endCallback) === '[object Object]') {
        end = endCallback.end;
        start = endCallback.start;
        active = endCallback.active;
      }
      // 这是cssAnimate自己定义的一个方法,动画结束时的回调
      if (node.rcEndListener) {
        node.rcEndListener();
      }

      node.rcEndListener = (e) => {
        if (e && e.target !== node) {
          return;
        }
  
        if (node.rcAnimTimeout) {
          clearTimeout(node.rcAnimTimeout);
          node.rcAnimTimeout = null;
        }

        clearBrowserBugTimeout(node);

        nodeClasses.remove(className);
        nodeClasses.remove(activeClassName);

        Event.removeEndEventListener(node, node.rcEndListener);
        node.rcEndListener = null;

        // Usually this optional end is used for informing an owner of
        // a leave animation and telling it to remove the child.
        if (end) {
          end();
        }
      };
      // 绑定动画结束的监听事件
      Event.addEndEventListener(node, node.rcEndListener);
      // 真正开始了
      if (start) {
        start();
      }
      nodeClasses.add(className);
      // 这里延时加载了实现动画效果的activeClassName,还有生命周期函数active
      node.rcAnimTimeout = setTimeout(() => {
        node.rcAnimTimeout = null;
        nodeClasses.add(activeClassName);
        if (active) {
          setTimeout(active, 0);
        }
        fixBrowserByTimeout(node);
        // 30ms for firefox
      }, 30);
      // node.rcEndListener在自己被调用之后会将自己置为空,我认为这里的返回是为了提供一个手动停止动画的方法
      return {
        stop() {
          if (node.rcEndListener) {
            node.rcEndListener();
          }
        },
      };
    };

这里就结束了,梳理之后对于react实现动画就有了理解了,用起来不会那么迷糊。其实感觉实现的代码不是特别难,主要是逻辑分层非常清晰。学习到了一个好的代码应该是什么样的。解耦功能,起好变量名,完善而清晰的逻辑,这些是学到的最重要的东西

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容