React動畫實踐
一、 動畫重要性
世界上最難的學問就是研究人。在你的動畫不會過于耗費資源,以至拖慢用戶的設備的前提下,動畫可以顯著改善用戶界面體驗。
可以簡單的把頁面動畫分為以下幾個類型:
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 同一時間)。
-
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/