React学习之扩展动画(三十)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_18661257/article/details/68929217

React为动画提供一个ReactTransitonGroup插件组件作为一个底层的动画API,一个ReactCSSTransitionGroup来简单地实现基本的CSS动画和过渡。

1.高级组件:ReactCSSTransitionGroup(CSS渐变组)

ReactCSSTransitionGroup是基于ReactTransitionGroup的,在React组件进入或者离开DOM的时候进行相关处理,它是一种简单地执行CSS过渡和动画的方式。这个的灵感来自于优秀的ng-animate库(没用过,估计是angularjs中的)。

引用组件

import ReactCSSTransitionGroup from 'react-addons-css-transition-group' // ES6
var ReactCSSTransitionGroup = require('react-addons-css-transition-group') // ES5 with npm
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; // ES5 with react-with-addons.js

这个例子让列表项淡入淡出

class TodoList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {items: ['hello', 'world', 'click', 'me']};
    this.handleAdd = this.handleAdd.bind(this);
  }

  handleAdd() {
    const newItems = this.state.items.concat([
      prompt('Enter some text')
    ]);
    this.setState({items: newItems});
  }

  handleRemove(i) {
    let newItems = this.state.items.slice();
    newItems.splice(i, 1);
    this.setState({items: newItems});
  }

  render() {
    const items = this.state.items.map((item, i) => (
      <div key={item} onClick={() => this.handleRemove(i)}>
        {item}
      </div>
    ));

    return (
      <div>
        <button onClick={this.handleAdd}>Add Item</button>
        <ReactCSSTransitionGroup
          transitionName="example"
          transitionEnterTimeout={500}
          transitionLeaveTimeout={300}>
          {items}
        </ReactCSSTransitionGroup>
      </div>
    );
  }
}

注意

你必须为ReactCSSTransitionGroup的所有的孩子提供key属性,即便孩子只有一项,也必须给出key属性,因为React需要依靠它来确定孩子的一些状态,比如插入,移除,或者没有变化。

这里需要提醒的是,如果你是直接在浏览器中使用React,一定要注意一个问题

react.js
react-with-addons.js
react-dom.js

一定要按照这个顺序引用,至于为什么,后续讲源码的时候就会提到了。

在上面TodoList这个组件当中,当一个新的项被添加到ReactCSSTransitionGroup它将会被添加example-enter类,然后在下一时刻就会被添加example-enter-active CSS。这是一个基于transitionName属性的约定。

所以我们可以有意识的控制进入和退出的动画,通过class,如下

.example-enter {
  opacity: 0.01;
}

.example-enter.example-enter-active {
  opacity: 1;
  transition: opacity 500ms ease-in;
}

.example-leave {
  opacity: 1;
}

.example-leave.example-leave-active {
  opacity: 0.01;
  transition: opacity 300ms ease-in;
}

2.动画的初始绑定

ReactCSSTransitionGroup提供了一个可选择的属性transitionAppear,在组件初始绑定的时候增加额外的过渡阶段,一般transitionAppearfalse所以在组件初始绑定时没有过渡阶段。

render() {
  return (
    <ReactCSSTransitionGroup
      transitionName="example"
      transitionAppear={true}
      transitionAppearTimeout={500}
      transitionEnter={false}
      transitionLeave={false}>
      <h1>Fading at Initial Mount</h1>
    </ReactCSSTransitionGroup>
  );
}

当初始化的时候,ReactCSSTransitionGroup会先增加example-appear,然后再增加 example-appear-active

.example-appear {
  opacity: 0.01;
}

.example-appear.example-appear-active {
  opacity: 1;
  transition: opacity .5s ease-in;
}

在最开始绑定时,ReactCSSTransitionGroup所有的孩子都处于appear状态而没有处于enter状态,然而,如果孩子加入了一个已经存在的ReactCSSTransitionGroup组件中去,那么将只有enter状态而不会经历appear状态

注意

transitionAppearReact0.13版本就已经增加到了ReactCSSTransitionGroup,为了向后兼容,它的默认值被设置成了false

然而transitionEntertransitionLeave的默认值为true,所以你必须要为transitionEnterTimeouttransitionLeaveTimeout两个属性进行赋值,如果你不需要他们可以通过transitionEnter={false}transitionLeave={false}来关掉他们。

3.自定义动画CSS-class

我们可以使用自定义的CSS类名来设计我们过渡,而不是简简单单的使用transitionName=string来固定死CSS类名的使用,我们还可以通过一个对象包含提供enter效果的类名和提供leave效果的类名,或者是enter, enter-active, leave-active,和leave效果的类名,如果你仅仅只是提供了enterleave效果的类名,那么React会自动在这两个类名后面增加'-active',以此为结尾。

下面举两个例子

.en {
  opacity: 0.01;
}

.en.en-active {
  opacity: 1;
  transition: opacity 300ms ease-in;
}

.le {
  opacity: 1;
}

.le.le-active {
  opacity: 0.01;
  transition: opacity 300ms ease-in;
}
.ap {
  opacity: 0.01;
}

.ap.ap-active {
  opacity: 1;
  transition: opacity .5s ease-in;
}
// ...
<ReactCSSTransitionGroup
  transitionName={ {
    enter: 'en',
    enterActive: 'enActive',
    leave: 'le',
    leaveActive: 'leActive',
    appear: 'ap',
    appearActive: 'apActive'
  } }>
  {item}
</ReactCSSTransitionGroup>

<ReactCSSTransitionGroup
  transitionName={ {
    enter: 'en',
    leave: 'le',
    appear: 'ap'
  } }>
  {item2}
</ReactCSSTransitionGroup>
// ...

4.一组动画必须要挂载了才能生效

为了能够给它的孩子也应用过渡效果,ReactCSSTransitionGroup必须已经挂载到了DOM[或者属性transitionAppear被设置为true:尚不确定]。下面的例子不会生效,因为ReactCSSTransitionGroup被挂载到新项,而不是新项被挂载到ReactCSSTransitionGroup里。将这个与上面的高级组件部分比较一下,看看有什么差异。

这里的意思不是说ReactCSSTransitionGroup不能嵌套,而是说承载ReactCSSTransitionGroup的容器必须是一个已经挂载到DOM中的组件,而不是单单的一个React.createElement创建出来的React元素。

render() {
  const items = this.state.items.map((item, i) => (
    <div key={item} onClick={() => this.handleRemove(i)}>
      <ReactCSSTransitionGroup transitionName="example">
        {item}
//当然这里存在一些问题,ReactCSSTransitionGroup的孩子必须是一个组件或者DOM
//而不能只是文本,虽然文本节点也是DOM,但是在最新版本的React会报错误
//所以尽可能不要在里面直接写文本
      </ReactCSSTransitionGroup>
    </div>
  ));

  return (
    <div>
      <button onClick={this.handleAdd}>Add Item</button>
      {items}
    </div>
  );
}

上述代码会报错

5.让一项或者零项动起来

虽然在上面的例子中,我们渲染了一个列表到ReactCSSTransitionGroup里,然而,ReactCSSTransitionGroup的孩子可以是一个或零个项目。这使它能够让一个元素实现进入和离开的动画。同样,你可以通过移动一个新的元素来替换当前元素。随着新元素的移入,当前元素移出。例如,我们可以由此实现一个简单的图片轮播:

import ReactCSSTransitionGroup from 'react-addons-css-transition-group';

function ImageCarousel(props) {
  return (
    <div>
      <ReactCSSTransitionGroup
        transitionName="carousel"
        transitionEnterTimeout={300}
        transitionLeaveTimeout={300}>
        <img src={props.imageSrc} key={props.imageSrc} />
      </ReactCSSTransitionGroup>
    </div>
  );
}

虽然上述的代码出来的效果和性能可能非常懵逼,但是确实是一个不错的例子。

6.禁止动画

如果你想,你可以禁用入场或者出场动画。例如,有些时候,你可能想要一个入场动画,不要出场动画,但是ReactCSSTransitionGroup会在移除DOM节点之前等待一个动画完成。你可以给ReactCSSTransitionGroup添加transitionEnter={false}或者transitionLeave={false} 来禁用这些动画。

注意

当使用ReactCSSTransitionGroup的时候,没有办法通知你在过渡效果结束或者在执行动画的时候做一些复杂的运算。如果你想要更多细粒度的控制,你可以使用底层的ReactTransitionGroup API,该API提供了你自定义过渡效果所需要的函数。

7.底层的API:ReactTransitionGroup

套路走起

import ReactTransitionGroup from 'react-addons-transition-group' // ES6
var ReactTransitionGroup = require('react-addons-transition-group') // ES5 with npm
var ReactTransitionGroup = React.addons.TransitionGroup; // ES5 with react-with-addons.js

ReactTransitionGroup是动画的基础。当孩子被添加或者从其中移除(就像上面的例子)的时候,特殊的生命周期函数就会在它们上面被调用(生命周期的概念前面将组件绑定的时候也说了)。

componentWillAppear()
componentDidAppear()
componentWillEnter()
componentDidEnter()
componentWillLeave()
componentDidLeave()

表现为不同的组件

默认情况下ReactTransitionGroup渲染成span标签。你可以通过提供一个component属性来改变这种行为。

//源码
propTypes: {
    component: React.PropTypes.any,
    childFactory: React.PropTypes.func
  },

  getDefaultProps: function () {
    return {
      component: 'span',
      childFactory: emptyFunction.thatReturnsArgument
    };
  },

以下是你将如何渲染一个<ul>

<ReactTransitionGroup component="ul">
  {/* ... */}
</ReactTransitionGroup>

//上述在HTML变为了
<ul>
  {/* ... */}
</ul>

需要注意的是ReactTransitionGroup内部不能文本必须含有至少一个DOM元素,除此之外,任何额外的、用户定义的属性将会成为已渲染的组件的属性。例如,以下是你将如何渲染一个带有css类的<ul>

<ReactTransitionGroup component="ul" className="animated-list">
  {/* ... */}
</ReactTransitionGroup>

<ul class="animated-list">
  {/* ... */}
</ul>

每一个React能渲染的DOM组件都是可用的。但是,组件并不需要是一个DOM组件。它可以是任何你想要的React组件;甚至是你自己已经写好的。直接写成component={List},然后再List组件中你将可以通过this.props.children来操作ReactTransitionGroup中的孩子

//源码

render: function () {
    // TODO: we could get rid of the need for the wrapper node
    // by cloning a single child
    var childrenToRender = [];
    for (var key in this.state.children) {
      var child = this.state.children[key];
      if (child) {
        // You may need to apply reactive updates to a child as it is leaving.
        // The normal React way to do it won't work since the child will have
        // already been removed. In case you need this behavior you can provide
        // a childFactory function to wrap every child, even the ones that are
        // leaving.
        childrenToRender.push(React.cloneElement(this.props.childFactory(child), { ref: key, key: key }));
      }
    }

    // 源代码告诉我们是不能通过DOM来获取组件的属性的,因为他们在这里进行了删除。

    var props = _assign({}, this.props);//创建一个继承于this.props新对象。
    delete props.transitionLeave;
    delete props.transitionName;
    delete props.transitionAppear;
    delete props.transitionEnter;
    delete props.childFactory;
    delete props.transitionLeaveTimeout;
    delete props.transitionEnterTimeout;
    delete props.transitionAppearTimeout;
    delete props.component;
    return React.createElement(this.props.component, props, childrenToRender);//这个函数大家应该非常熟悉了。
  }

渲染单一的孩子(如果你想的话)

使用ReactTransitionGroup可以只动态化一个孩子的装载和卸载过程,就像是可伸缩折叠的面板一样,一般ReactTransitionGroup包含所有的孩子在span中(或者是之前所说的自定义组件),这是因为很多React组件都必须只有一个单一的根元素,而ReactTransitionGroup 不再此列中,它没有这个限制,因为它本身就形成一个根元素。

但是如果你只需要一个单一的孩子的话,可以不将它放在`span中,或者其他DOM组件中,直接创建一个自定义的组件返回第一个孩子就可以了。

function FirstChild(props) {
  const childrenArray = React.Children.toArray(props.children);
  return childrenArray[0] || null;//只返回第一个孩子组件
}

<ReactTransitionGroup component={FirstChild}>
  {someCondition ? <MyComponent /> : null}
</ReactTransitionGroup>

这里要注意的是,我们经常用到的组件的props.children说的是组件内部的DOM节点,而不是一个文本,也就是说你在一个组件内部只写一个文本props.children是没有值的。

当然这种处理只能在单一的孩子入场和出场,没有涉及到其他的东时候使用,如果你想实现多个的话,有必要给他们提供给一个公共的DOM父亲。

也就是说让父亲DOM作为childrenArray[0]就可以了。

8.ReactTransitionGroup 函数详解

componentWillAppear()

componentWillAppear()

TransitionGroup中,当一个组件进行绑定时,该函数和componentDidMount()被同时调用,它会阻塞其它动画触发,直到回调函数调用,它只发生在TransitionGroup初始化渲染时。

componentDidAppear()

componentDidAppear()

该函数在传给componentWillAppear的回调函数被调用之后调用。

componentWillEnter()

componentWillEnter(callback)

在组件被添加到已有的TransitionGroup中的时候,该函数和componentDidMount()被同时调用。它会阻塞其它动画触发,直到回调函数被调用。该函数不会在TransitionGroup初始化渲染的时候调用。

componentDidEnter()

componentDidEnter()

该函数在传给componentWillEnter的回调函数被调用之后调用。

componentWillLeave()

componentWillLeave(callback)

该函数在孩子从ReactTransitionGroup中移除的时候调用。虽然孩子被移除了,但是ReactTransitionGroup将会使它继续在DOM中,直到回调函数被调用,也就是谁必须回调函数被执行后才会将这个元素移除。

componentDidLeave()

componentDidLeave()

该函数在componentWillLeave回调函数被调用的时候调用(与componentWillUnmount是同一时间)。

9.隐患

会用渐变组时主要需要注意两点问题:

  1. 就我们调用相关函数会延迟子组件的移除比如说componentWillLeave函数,当回调函数没有被调用执行完后,不仅动画会被阻塞,连移除动作也会阻塞,因为我们的componentWillLeave调用则和componentWillUnmount是同一时刻的,componentWillLeave没有执行完,componentWillUnmount便不会结束。

  2. 必须为每一个子组件设置一个key,这个key要是独一无二的,如果没有设置可能导致动画无法正常的进行。

下一篇将讲React的键片段

阅读更多

没有更多推荐了,返回首页