React學習之擴展動畫(三十)

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的鍵片段

發佈了447 篇原創文章 · 獲贊 471 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章