過去一年,React 給整個前端界帶來了一種新的開發方式,咱們拋棄了無所不能的 DOM 操做。對於 React 實現動畫這個命題,DOM 操做已是一條死路,而 CSS3 動畫又只能實現一些最簡單的功能。這時候 ReactCSSTransitionGroup Addon,無疑是一枚強心劑,可以幫助咱們以最低的成本實現例如節點初次渲染、節點被刪除時添加動效的需求。本文將會深刻實現原理來玩轉 ReactCSSTransitionGroup。html
在介紹 ReactCSSTransitionGroup 的用法前,先來實現一個常規 transition 動畫,要實現的是刪除某個節點的時候,讓該節點的透明度不斷的變大。前端
handleRemove(item) { const { items } = this.state; const len = items.length; this.setState({ items: items.reduce((result, entry) => { return entry.id === item.id ? [...result, { ...item, isRemoving: true }] : [...result, item]; }, []) }, () => { setTimeout(() => { this.setState({ items: items.reduce((result, entry) => { return entry.id === item.id ? result : [...result, item]; }, []) }); }, 500); }); }, render() { const items = this.state.items.map((item, i) => { return ( <div key={item.id} onClick={this.handleRemove.bind(this, item)} className={item.isRemoving ? 'removing-item' : ''}> {item.name} </div> ); }); return ( <div> <button onClick={this.handleAdd}>Add Item</button> <div> {items} </div> </div> ); }
同時咱們在 CSS 中須要提供以下的樣式react
.removing-item { opacity: 0.01; transition: opacity .5s ease-in; }
相同的需求,使用 ReactCSSTransitionGroup 建立動畫會是怎麼的呢?git
handleRemove(i) { const { items } = this.state; const len = items.length; this.setState({ items: [...items.slice(0, i), ...item.slice(i + 1, len - 1)] }); }, render() { const items = this.state.items.map((item, i) => { return ( <div key={item} onClick={this.handleRemove.bind(this, i)}> {item} </div> ); }); return ( <div> <button onClick={this.handleAdd}>Add Item</button> <ReactCSSTransitionGroup transitionName="example"> {items} </ReactCSSTransitionGroup> </div> ); }
在這個例子中,當新的節點從 ReactCSSTransitionGroup 中刪除時,這個節點會被加上 example-leave
的 class,在下一幀中這個節點還會被加上 example-leave-active
的 class,經過添加如下 CSS 代碼,被刪除的節點就會有動畫的效果。github
.example-leave { opacity: 1; transition: opacity .5s ease-in; } .example-leave.example-leave-active { opacity: 0.01; }
從這個例子,咱們能夠看到 ReactCSSTransition 能夠把開發者從一大堆動畫相關的 state 中解放出來,只須要關心數據的變化,以及 CSS 的 transition 動畫邏輯。瀏覽器
後面將會仔細分析 ReactCSSTransitionGroup 的源碼實現。在看代碼以前,你們能夠先看 官網的文檔,對 ReactCSSTransitionGroup 的用法進一步瞭解。看完之中,能夠想一想兩個問題:app
appear 動畫和 enter 動畫有什麼區別?ide
ReactCSSTransitionGroup 子元素的生命週期是怎樣的?函數
ReactCSSTransitionGroup 的源碼分爲5個模塊,咱們先看看這5個模塊之間的關係:工具
咱們來整理一下這幾個模塊的分工與職責:
ReactTransitionEvents 提供了對各類前綴的 transitionend、animationend 事件的綁定和解綁工具
ReactTransitionChildMapping 提供了對 ReactTransitionGroup 這個 component 的 children 進行格式化的工具
ReactCSSTransitionGroup 會調用 ReactCSSTransitionGroupChild 對 children 中的每一個元素進行包裝,而後將包裝後的 children 做爲 ReactTransitionGroup 的 children 。
從這個關係圖裏面能夠看到,ReactTransitionGroup 和 ReactCSSTransitionGroupChild 纔是實現動畫的關鍵部分,所以,本文會從 ReactTransitionGroup 開始解讀,而後從 ReactCSSTransitionGroupChild 中解讀怎麼實現具體的動畫邏輯。
下面咱們按照 React 生命週期來解讀 ReactTransitionGroup。
在初始化 state 的時候,將 this.props.children
轉化爲對象,其中對象的 key 就是 component key,這個 key 與 children 中的元素一一對應,而後將該對象設置爲 this.state.children;
在初次 render 的時候,將 this.state.children
中每個普通的 child component 經過指定的 childFactory
包裹成一個新的 component,並渲染成指定類型的 component 的子元素。在下面的源碼中也能夠看到,咱們在建立過程當中給每一個 child 設置的 key 也會做爲 ref,方便後續索引。
render: function() { var childrenToRender = []; for (var key in this.state.children) { var child = this.state.children[key]; if (child) { childrenToRender.push(React.cloneElement( this.props.childFactory(child), {ref: key, key: key} )); } } return React.createElement( this.props.component, this.props, childrenToRender ); }
初次 mount 後,遍歷 this.state.children
中的每一個元素,依次執行 appear 動畫的邏輯。
當接收到新的 props 後,先將 nextProps.children
和 this.props.children
合併,而後轉化爲對象,並更新到 this.state.children
。計算在 nextProps
中即將 leave 的 child,若是該元素當前沒有正在運行的動畫,將該元素的 key 保存在 keysToLeave。
對於 nextProps 中新的 child,若是該元素沒有正在運行的動畫的話(也許會疑惑,一個剛進入的元素怎麼會有動畫正在運行呢?下文將會解釋),將該元素的 key 保存在 keysToEnter。從這裏也能看出來,原本在 nextProps 中即將 leave 的 child 會被保留下來以達到動畫效果,等動畫效果結束後纔會被 remove。
component 更新完成後,對 keysToEnter
中的每一個元素執行 enter 動畫的邏輯,對 keysToLeave
中的每一個元素執行 leave 動畫的邏輯。因爲 enter 動畫的邏輯和 appear 動畫的邏輯幾乎如出一轍,無非是變成執行 child 的componentWillEnter
和 componentDidEnter
方法。
leave 動畫稍有不一樣,看下面源碼能夠看到,在 leave 動畫結束後,若是發現該元素從新 enter,這裏會再次執行 enter 動畫,不然的話經過更新 state
中的 children 來刪除相應的節點。這裏也能夠回答,爲何對剛 enter 的元素,也要判斷該元素是否正在進行動畫,由於若是該元素上一次 leave 的動畫尚未結束,那麼這個節點還一直保留在頁面中運行動畫。
另外,你們有沒有注意到一個問題,若是 leave 動畫的回調函數沒有被調用,那麼這個節點將永遠不會被移除。
if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) { // This entered again before it fully left. Add it again. this.performEnter(key); } else { this.setState(function(state) { var newChildren = assign({}, state.children); delete newChildren[key]; return {children: newChildren}; }); }
至此,咱們看到 ReactTransitionGroup 沒有實現任何具體的動畫邏輯。
搞清楚 ReactTransitionGroup 的原理之後,ReactCSSTransitionGroup 作的事情就很簡單了。簡單地說, ReactCSSTransitionGroup 調用了 ReactTransitionGroup ,提供了本身的 childFactory 方法,而這個 childFactory 則是調用了 ReactCSSTRansitionGroupChild 。
_wrapChild: function(child) { // We need to provide this childFactory so that // ReactCSSTransitionGroupChild can receive updates to name, enter, and // leave while it is leaving. return React.createElement( ReactCSSTransitionGroupChild, { name: this.props.transitionName, appear: this.props.transitionAppear, enter: this.props.transitionEnter, leave: this.props.transitionLeave, appearTimeout: this.props.transitionAppearTimeout, enterTimeout: this.props.transitionEnterTimeout, leaveTimeout: this.props.transitionLeaveTimeout, }, child ); }
下面來看 ReactCSSTransitionGroupChild 是怎麼實現節點的動畫的。以 appear 動畫爲例,在 child.componentWillAppear
被調用的時候,給該節點加上 xxx-appear 的 className ,而且在一幀(React 裏是寫死的17ms)後,給該節點加上 xxx-appear-active 的 className ,最後在動畫結束後刪除 xxx-appear 以及 xxx-appear-active 的 className。
enter、leave 動畫的實現相似。到這裏源碼就解讀完了,其中,還有一些細節要去注意的。
key
裏的祕密在源碼解讀的過程當中,咱們發現 ReactTransitionGroup 會將 children 轉化爲對象,而後經過 for...in...
遍歷。對於這一過程,會不會感到有所疑慮,ReactTransitionGroup 怎麼保證子節點渲染的順序。
對於這個問題,React 的處理過程能夠簡化爲下面的代碼,測試結果顯示,當 key 爲字符串類型時,for...in...
遍歷的順序和 children 的順序可以保持一致;可是當 key 爲數值類型時,for...in...
遍歷的順序和 children 的順序就不必定可以保持一致,你們能夠用下面這段簡單的代碼測試一下。
function test (o) { var result = {}; for (var i = 0, len = o.length; i < len; i++) { result[o[i].key] = o[i]; } for (var key in result) { if (result[key]) { console.log(key, result[key]); } } }
所以,咱們知道 ReactCSSTransitionGroup 全部子 component 的 key 千萬不要設置成純數字,必定要是字符串類型的。
在 React 0.14 版本中,React 已經表示將在將來的版本中廢棄監聽 transitionend、 animationend 事件,而是經過設置動畫的 timeout 來達到結束動畫的目的,有沒有想過 React 爲何要放棄原生事件,而改用 setTimeout
。
事實上,緣由很簡單,transitontend 事件在某些狀況是不會被觸發。在 transitionend 的 MDN文檔 中有這麼幾行文字:
In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire. The event will also not fire if the animated element becomes display: none before the transition fully completes.
當動畫元素的 transition 屬性在動畫完成前被移除了,transitionend 事件不會被觸發
當動畫元素在動畫完成前,display 樣式被設置成 "none",這種狀況 transitionend 事件不會被觸發
當動畫還沒完成,當前瀏覽器標籤頁失焦很長的時間(大於動畫時間),transitionend 事件不會被觸發,直到該標籤頁從新聚焦後 transitionend 事件纔會觸發
正是因爲 transitionend
不會觸發,會致使隱形 bug,能夠看其中一個 bug。
appear 動畫是 ReactCSSTransitionGroup 組件初次 mount 後,纔會被添加到 ReactCSSTransitionGroup 的全部子元素上。
enter 動畫是 ReactCSSTransitionGroup 組件更新後,被添加到新增的子元素上。
ReactCSSTransitionGroup 提供建立 CSS 動畫最簡單的方法,對於更加個性化的動畫,你們能夠經過調用 ReactTransitionGroup 自定義動畫。