React動畫實踐

KerriGourga 8年前發布 | 53K 次閱讀 React CSS 前端技術

一、 動畫重要性

世界上最難的學問就是研究人。在你的動畫不會過于耗費資源,以至拖慢用戶的設備的前提下,動畫可以顯著改善用戶界面體驗。

可以簡單的把頁面動畫分為以下幾個類型:

1、頁面元素動畫:比如輪播圖等,由用戶操作催化;

2、loading動畫:減少用戶視覺等待時間;

3、裝飾動畫:盡量避免,會分散用戶注意力,值得也不值得;

4、廣告動畫:增加廣告的轉化率;

5、情節動畫:多用于SPA;

以loading動畫為例說明動畫的重要性:為了提升用戶體驗、增加用戶粘性,大家從開發的角度看首先想到的會是從前到后的性能優化,從而減少用戶打開頁面時的等待時間,你或許考慮到了要增加帶寬、減少頁面的http請求、使用數據緩存、優化數據庫、使用負載均衡等,但是由于業務限制和用戶復雜的體驗環境,總會遇到一些瓶頸。這時候,我們需要做的就是如何減少用戶的視覺等待時間,哪怕是給一朵轉動的菊花,但千萬不要不理她,讓人盲目的等待就是你業務流失的方式。不客氣的說,有時候一朵性感菊花的作用并不亞于你去優化數據庫。

二、 動畫實現原則

在實現動畫時,我個人一直遵循以下幾個原則:

1、性能,性能,還是性能:這方面的建議就是在有選擇時,一定要使用基于CSS的動畫,將JS作為備選,因為考慮到硬件加速和性能之后,CSS幾乎總是優于原生JS實現的動畫;

2、微小低調的動畫往往表現更好;

3、大而絢麗的動畫需要帶有目的性:不能只為了“好看”;

4、動畫持續時間要短;

5、讓動畫具有彈性:或者說緩動效果;

6、動畫不要突然停止;

大家可以想一下看看是不是這么回事。

三、 React動畫

(一) 實現方式

書歸正傳,React實現動畫有兩種方式:

1、CSS漸變組;

2、間隔動畫;

CSS漸變組: 簡化了將CSS動畫應用于漸變的過程,在合適的渲染和重繪時間點有策略的添加和移除元素的class。

間隔動畫: 以犧牲性能為代價,提供更多的可擴展性和可控性。需要更多次的渲染,但同時也允許為css之外的內容(比如滾動條位置以及canvas繪圖)添加動畫。

(二) CSS漸變組

ReactCSStransitionGroup是在插件類ReactTransitionGroup這個底層API基礎上進一步封裝的高級API,來簡單的實現基本的CSS動畫和過渡。

1、快速開始

以一個簡單的圖片輪播圖為例:

var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
 
var Carousel = React.createClass({
        propTypes: {
    transitionName: React.PropTypes.string.isRequired,
            imageSrc: React.PropTypes.string.isRequired
        },
        render: function() {
            return (
                <divclassName='carousel'>
                    <ReactCSSTransitionGrouptransitionName={this.props.transitionName} >
                        <imgsrc={this.props.imageSrc} key={this.props.imageSrc} />
                    </ReactCSSTransitionGroup>
                </div>
            );
        }
});

剩下的就是在父組件中為其傳入合適的transitionName以及imageSrc即可。效果如下:

聰明的你一定發現了:在這個組件當中,當一個新的列表項被添加到ReactCSSTransitionGroup,它將會被添加transitionName-enter對應的css類,然后在下一時刻被添加transitionName-enter-active 對應的CSS類;當一個列表項要從ReactCSSTransitionGroup中移除時,他也將會被添加transitionName-leave對應的css類,然后在下一時刻被添加transitionName-leave-active 對應的CSS類,這里要注意的是,當你嘗試移除一項的時候,ReactCSSTransitionGroup仍會保持該項在DOM里,直至動畫結束;

示例中演示的兩個切換效果只需要修改transitionName屬性對應的CSS動畫類即可:

透明度切換效果:

.carousel1-enter {
    opacity: 0;
}
.carousel1-enter-active {
    opacity: 1;
    transition: opacity 300ms ease-in;
}
.carousel1-leave {
    opacity: 1;
}
.carousel1-leave-active {
    opacity: 0;
    transition: opacity 300ms ease-in;
}

位移切換效果:

.carousel2-enter {
    left: 100%;
}
.carousel2-enter-active {
    left: 0;
    transition: left 300ms ease-in;
}
.carousel2-leave {
    left: 0;
}
.carousel2-leave-active {
    left: -100%;
    transition: left 300ms ease-in;
}
  •         大家都注意到了,我一直在提transitionName這個屬性,其實對于ReactCSStransitionGroup來說,一個屬性是完全不夠的,下面來詳細介紹下它的屬性們。
  • 2、屬性們

  • (1)、transitionName

{oneOfType([React.PropTypes.string,React.PropTypes.object]).isRequired}

作用:關聯CSS類:

例如:

transitionName-appear;
transitionName-appear-active;
transitionName-enter;
transitionName-enter-active;
transitionName-leave;
transitionName-leave-active;

制定CSS類:

  transitionName={ { 
        enter: ‘enter’,
        enterActive: ‘enterActive’,
        leave: ‘leave’, 
        leaveActive: ‘leaveActive’, 
        appear: ‘appear’, 
        appearActive: ‘appearActive’ 
  } }

(2)、transitionAppear

{React.PropTypes.bool} {false}

作用:初始化掛載動畫。來為在組件初始掛載添加一個額外的過渡階段。 通常在初始化掛載時沒有過渡階段因為transitionAppear 的默認值為false。

例如:

render: function() { 
      return ( 
          <ReactCSSTransitionGrouptransitionName="example" transitionAppear={true} > 
 <h1>FadingatInitialMount</h1> 
          </ReactCSSTransitionGroup> 
      ); 
}

(3)、transitionEnter

{React.PropTypes.bool} {true}

作用:用來禁用 enter動畫

(4)、transitionLeave

{React.PropTypes.bool} {true}

作用:用來禁止leave動畫 ReactCSSTransitionGroup 會在移除你的DOM節點之前等待一個動畫完成。你可以添加transitionLeave={false} 到ReactCSSTransitionGroup 來禁用這些動畫。

(5)、component

{React.PropTypes.any} {‘span’}

作用:默認情況下 ReactTransitionGroup 渲染為一個 span。你可以通過提供一個 component prop 來改變這種行為. 組件不需要是一個DOM組件,它可以是任何你想要的React組件,甚至是你自己寫的。

(6)、className

{ React.PropTypes.string }

作用:給當前的component設置樣式類

例如:

<ReactTransitionGroupcomponent=“ul” className="example" > 
 ... 
</ReactTransitionGroup>

3、 生命周期

當子級被聲明式的從其中添加或移除(就像上面的例子)時,特殊的生命周期掛鉤會在它們上面被調用。

componentWillAppear(callback)

對于被初始化掛載到 CSSTransitionGroup 的組件,它和 componentDidMount() 在相同時間被調用 。它將會阻塞其它動畫發生,直到callback被調用。它只會在CSS TransitionGroup 初始化渲染時被調用。

componentDidAppear()

在 傳給componentWillAppear 的 回調 函數被調用后調用。

componentWillEnter(callback)

對于被添加到已存在的 CSSTransitionGroup 的組件,它和 componentDidUpdate() 在相同時間被調用 。它將會阻塞其它動畫發生,直到callback被調用。它不會在 CSSTransitionGroup 初始化渲染時被調用。

componentDidEnter()

在傳給 componentWillEnter 的回調函數被調用之后調用。

componentWillLeave(callback)

在子級從 ReactCSSTransitionGroup 中移除時調用。雖然子級被移除了,ReactTransitionGroup 將會保持它在DOM中,直到callback被調用。

componentDidLeave()

在willLeave callback 被調用的時候調用(與 componentWillUnmount 同一時間)。

  1. 4、原理簡述

以componentWillEnter為例,偽代碼如下:

componentWillEnter (callback) {
    letel = ReactDOM.findDOMNode(this);
    el.classList.add(styles.enter);
    requestAnimationFrame(() => { 
          el.classList.add(styles.active);
    });
    el.addEventListener('transitionend', () => {
        callback && callback();
        el.classList.remove(styles.enter);
        el.classList.remove(styles.active);
    });
}

在 componentWillEnter 里給 Animation 組件添加了 styles.enter 樣式類,然后在瀏覽器下一個 tick 加入 styles.active 樣式類 – 這里使用了 requestAnimationFrame,也可以使用 setTimeout,另外還監聽 ‘transitionend’ 事件,transitionend 事件發生時執行回調 callback 并移除 styles.enter 與 styles.active 兩個樣式類

5、 注意事項

①. 一定要為ReactCSSTransitionGroup的所有子級提供 key屬性。即使只渲染一個項目。React靠key來決定哪一個子級進入,離開,或者停留。

②、動畫持續時間需要被同時在CSS和渲染方法里被指定。這告訴React什么時候從元素中移除動畫類,并且如果它正在離開,決定何時從DOM移除元素。

③、ReactCSSTransitionGroup必須已經掛載到了DOM才能工作。為了使過渡效果應用到子級上,ReactCSSTransitionGroup必須已經掛載到了DOM或者 prop transitionAppear 必須被設置為 true。ReactCSSTransitionGroup 不能隨同新項目被掛載,而是新項目應該在它內部被掛載。

6、 劣勢

ReactCSSTransitionGroup的優勢是非常明顯的,簡化代碼、提高性能等,但是其劣勢我們也需要了解,以在做實際項目時進行適當的取舍。

① 不兼容較老的、不支持CSS3的瀏覽器;

② 不支持為CSS屬性之外的東西(比如滾動條位置或canvas繪畫)添加動畫;

③ 可控粒度不夠細。CSS3動畫只支持start、end、iteration三個事件,不支持對中間狀態進行處理。

④ transitionEnd和animationEnd事件不穩定。

7、 V0.14動畫新特性

新增屬性:

transitionAppearTimeout
transitionEnterTimeout
transitionLeaveTimeout

控制動畫持續時間,解決animationend transitionend 事件不穩定、時有時沒有的現象,v0.15版本將徹底放棄監聽animationend transitionend 事件。

官方原話是: To improve reliability, CSSTransitionGroup will no longer listen to transition events. Instead, you should specify transition durations manually using props such as transitionEnterTimeout={500}.

原理上其實是簡化了,還是以componentWillEnter為例,偽代碼如下:

componentWillEnter (callback) {
    letel = ReactDOM.findDOMNode(this);
    el.classList.add(styles.enter);
    requestAnimationFrame(() => { 
          el.classList.add(styles.active);
    });
    setTimeout( () => {
        callback && callback();
        el.classList.remove(styles.enter);
        el.classList.remove(styles.active);
    }, props.transitionEnterTimeout);
}

所以我們的輪播圖就要改為這樣實現:

<ReactCSSTransitionGrouptransitionName={this.props.transitionName} transitionEnterTimeout={300} transitionLeaveTimeout={300} >
        <imgsrc={this.props.imageSrc} key={this.props.imageSrc} />
</ReactCSSTransitionGroup>

(三) 間隔動畫

深入了解了CSS漸變組,大家也看到了它并不是萬能的,所以需要間隔動畫來做輔助,或者說是第二選擇。

間隔動畫實現方式很簡單,有兩種:

1、 requestAnimationFrame

2、 setTimeout

requestAnimationFrame可以以最小的性能損耗實現最流暢的動畫,它被調用的次數頻繁度超出你想象。在requestAnimationFrame不支持或不可用的情況下,就要考慮降級到不那么智能的setTimeout了。

間隔動畫在實現原理上其實很簡單,就是周期性的觸發組件的狀態更新,通過在組件的render方法中加入這個狀態值,組件能夠在每次狀態更新觸發的重渲染中正確表示當前的動態階段。

以實現元素右移100px為例,代碼實現如下所示:

1、requestAnimationFrame實現

var Todo = React.createClass(
 getInitialState: function() {
 return {
 left: 0
 };
 },
 componentWillUpdate: function() {
 requestAnimationFrame(this. resolveAnimationFrame);
 },
 render: function() {
 return <divstyle={{left: this.state.left}}>This willanimate!</div>;
 },
 resolveAnimationFrame: function() {
 if(this.state.left <= 100) {
 this.setState({
 left: this.state.left + 1
 });
 }
 }
);

2、requestAnimationFrame實現

var Todo = React.createClass(
 getInitialState: function() {
 return {
 left: 0
 };
 },
 componentWillUpdate: function() {
 setTimeout(this. resolveAnimationFrame, this.props.tick);
 },
 render: function() {
 return <divstyle={{left: this.state.left}}>This willanimate!</div>;
 },
 resolveAnimationFrame: function() {
 if(this.state.left <= 100) {
 this.setState({
 left: this.state.left + 1
 });
 }
 }
);

是不是很簡單呢?

大家一定會想,React也提供了我們可以直接操作DOM的接口,我還是不習慣React的寫法,為什么不能像原生js那樣實現動畫效果呢?那么我可以明確的告訴你,React就是不允許你這么做,它就是要規避前端這種肆無忌憚的寫法,規范你的代碼,降低維護成本。

至于性能,這里順便簡單提一下React的渲染過程,大家可以體會下。

首次渲染時,從JSX渲染成真實DOM的大體過程如下:

1、parse過程將JSX解析成Virtual DOM,是一種抽象語法樹(AST);

2、compile過程則將AST通過DOM API 編譯成頁面真實的DOM。

二次渲染過程如下:

1、每次生成的頁面DOM渲染后,其對應的Virtual Dom也會緩存起來;

2、當JSX發生變化,,會首先根據新的JSX生成一個全新的Virtual Dom;

3、新的Virtual Dom生成后,會檢測是否存在舊的Virtual Dom;

4、發現存在,則通過react diff算法比較新舊Virtual Dom之間的差異,得出一個從舊Virtual Dom轉換到新Virtual Dom 的最少操作(minimum operating);

5、最后,頁面舊的真實Dom,根據剛剛react diff算法得出的最少操作,通過Dom api進行節點的增、刪、改,得出新的真實Dom;

大家一定在懷疑diff算法的性能,因為傳統的用遞歸算法來比較兩棵樹的時間復雜度是O(n^3),真是爛到了極致,但是,React通過幾個先驗條件將diff的算法復雜度控制在了O(n)。下面講一下這幾個條件:

1、 只在同層級做比較

在React 的diff算法中,兩個virtual dom樹的比較只在同層級進行。這樣,只需一遍,即可遍歷整棵樹。這樣做,是忽略了節點的跨層移動,因為web中節點的跨層操作較少。同時我們在使用React時,也要盡量避免這樣做。

示例如下:

算法計算得出的操作是:刪除body的子節點p及其子節點,創建div的子節點p,創建p的子節點a。

通過react的diff算法,兩個Virtual Dom 比較后,因移動節點不同級,因此不做移動操作,而是直接刪除重建。

2、 基于組件比較

在React 的diff算法中,virtual dom樹的比較只在同組件進行。對于不同組件,即使結構相似,也不進行比較,而是直接執行刪除+重建操作。這樣做,是強化組件的概念,因為正常情況下,不同組件的頁面結構是不一樣的。

示例如下:

算法計算得出的操作是:刪除body的子節點div及其子節點,創建body子節點div及其子節點p和子節點input。

如使用傳統的diff算法,會計算出只需刪除div的子節點a,并創建div子節點input。

而采用react的diff算法,兩個Virtual Dom 比較時,發現綠框內結構為不同的組件,則綠框內容不做比較,直接刪除重建。

3、節點使用唯一屬性key

在React 的diff算法中,virtual dom樹的節點可以通過key標識其身份,提高節點同級同組移動時的性能。增加身份標識來作為節點是否需要修改的一個條件。

算法計算得出的操作只需要:移動div節點到最后即可。

若使用傳統的diff算法,判斷body第一個子節點,舊的為div,新的為p,節點不一樣,則刪除div節點,新增插入p節點。之后節點操作類似,因此總的需要進行三次節點刪除和新增。

而采用react的diff算法,因為節點多了key來標識,兩個Virtual Dom 比較時,發現level1下的三個節點其實是一樣的(key=1、key=2、key=3)。

相信通過上面的介紹,大家對React有了更進一步的了解。

四、 總結

1、 使用React實現動畫效果時,首先考慮CSS漸變組,實在不行,再去考慮使用間隔渲染實現。

2、 需要定制的功能比較多的話,建議不要使用React自帶額CSSTransitionGroup插件。比如說我們想在動畫結束傳入一個onEnd回調,如果修改React源碼,有一萬多行,CSSTransitionGroup依賴transitionGroup,transitionGroup又依賴其他插件和方法,很難改,也很容易改出問題來。我自己實現了一套CSSTransitionGroup插件,后續會做進一步的分享。

謝謝閱讀

來自: http://www.alloyteam.com/2016/01/react-animation-practice/

 本文由用戶 KerriGourga 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!