TAT.李強 React 動畫實踐
In 未分類 on 2016年01月21日 by view: 22,640
15

一、 動畫重要性

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

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

        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、快速開始

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

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

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

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

        透明度切換效果:

        位移切換效果:

  •         大家都注意到了,我一直在提 transitionName 這個屬性,其實對于 ReactCSStransitionGroup 來說,一個屬性是完全不夠的,下面來詳細介紹下它的屬性們。
  • 2、屬性們

  • (1)、transitionName

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

      作用:關聯 CSS 類:

      例如:

      制定 CSS 類:

(2)、transitionAppear

      {React.PropTypes.bool} {false}

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

      例如:

(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 設置樣式類

      例如:

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 里給 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 動畫新特性

      新增屬性:     

      控制動畫持續時間,解決 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 為例,偽代碼如下:

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

(三)間隔動畫

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

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

      1、 requestAnimationFrame

      2、 setTimeout

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

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

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

      1、requestAnimationFrame 實現

      2、requestAnimationFrame 實現

      是不是很簡單呢?

      大家一定會想,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 插件,后續會做進一步的分享。

 

謝謝閱讀

原創文章轉載請注明:

轉載自AlloyTeam:http://www.ecomenagepro.com/2016/01/react-animation-practice/

  1. 我的稱呼是必填的 2017 年 10 月 17 日

    剛想仔細看文章, 突然看到蒼老師, 勾起了懷舊的新, 我老練地抽出兩張紙巾,呵呵, 這文章下次再看了

  2. 文明 2017 年 3 月 7 日

    如果快速切換。。會出現多個 img DOM 在組件中怎么辦呢比如上圖的切換,控制臺就出現了 2 個 DOM,如果動畫很慢,點擊很快怎么辦

  3. _諸葛囧明_ 2017 年 1 月 6 日

    求手寫體配圖的制作工具

    • 大油門 2017 年 6 月 20 日

      求黑板風格手繪制圖工具?找了很久沒找到

  4. 秀逗大胖子 2016 年 8 月 15 日

    我發現用了 reactCssTransitionGroup 組件的 componentWillUnmount 是晚于組件的 componentDidMount 這很要命唉, 求解

    • ian hu 2016 年 11 月 7 日

      盡量少在組件生命周期中處理使用動畫后,元素卸載需要等到動畫結束,所以是先掛載新節點,然后執行新節點的進入動畫和被移除節點的離開動畫,動畫結束后,被刪除節點卸載

  5. 溪楊 2016 年 8 月 12 日

    說好的進一步分享咧

  6. season 2016 年 7 月 5 日

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

  7. pober 2016 年 4 月 6 日

    講的挺深入的。不過對于 Css 第一部分來講,作為一個偽前端,看起來有點壓力。。。不知道 CSSTransitionGroup 那幾個周期函數在哪兒寫,如果能給出一個完整的 Demo,相信對其理解有著醍醐灌頂的幫助…… 謝謝博主……

  8. 郭野 2016 年 2 月 3 日

    配圖好贊

  9. BlackBerry 2016 年 1 月 30 日

    leave 的時候, 動畫經常會不執行.. 很奇怪..

    • TAT.李強

      TAT.李強 2016 年 3 月 22 日

      有沒有設置 transitionLeaveTimeout 呢? 老版本的 react 靠的是監聽動畫結束事件,比如 transitionend,該事件不穩定。建議用最新版 react,然后設置 transitionLeaveTimeout

  10. 康可紫 2016 年 1 月 28 日

    你加油吧!!!

  11. 康可紫 2016 年 1 月 28 日

    說的好,一定回復~

  12. OneNewLife 2016 年 1 月 22 日

    好黃 已撥 110[陰險]

發表評論