閱讀本文你會得到:css
一個相應的使用案例請看項目react-music-lhy,文檔在blog中基於react-transition-group的react過渡動畫找到:組件掛載與卸載動畫的能夠藉助appear以及onExit回調函數實現。案例中onExit回調函數主要用於經過路由跳轉卸載組件。node
一個比較有用的技巧:本文中工具函數一節的safeSetState函數;以及TransitionGroup種dom-helpers工具庫的使用以及封裝。react
react-transition-group官方指南,結合react-router的項目使用案例請參照此文檔git
全文中提到的第一次掛載與掛載的概念是指:Transition單獨使用的時候,不區分第一掛載與其餘掛載,只有在父組件是TransitionGroup的時候才區分。這能夠從constructor中以下代碼看出來:github
// 初始化appear:
// 當單獨使用Transition沒有被TransitionGroup包裹時,appear = props.appear
// 當被TransitionGroup包裹的時候,TransitionGroup處於正在掛載階段,子組件Transition是第一次掛載,所以appear = props.appear
// 當被TransitionGroup包裹的時候,TransitionGroup已經掛載完成,說明子組件Transition以前掛載並卸載過,所以appear = props.enter
let parentGroup = context.transitionGroup
let appear =
parentGroup && !parentGroup.isMounting ? props.enter : props.appear
複製代碼
appear主要用於設置:this.appearStatus = ENTERING,詳細分析能夠參考後續對constructor的分析。react-router
type: Function | element
required
複製代碼
某個狀態下須要過渡效果的目標組件,能夠是函數閉包
<Transition timeout={150}>
{(status) => (
<MyComponent className={`fade fade-${status}`} />
)}
</Transition>
複製代碼
每一個狀態'entering', 'entered', 'exiting', 'exited', 'unmounted'的時候執行的回調函數,上面代碼實現的是,每個狀態就給某個子組件增長一個過渡樣式,能夠很是靈活的給任意組件增長樣式,實現過渡效果。app
type: boolean
default: false
複製代碼
用於在enter與exit狀態之間翻轉,默認爲false,表示不掛載組件或者處於exit狀態。dom
type: boolean
default: false
複製代碼
在第一次in={true}即掛載的時候,設置mountOnEnter={true}表示延遲掛載,懶加載組件。函數
type: boolean
default: false
複製代碼
若是爲true,在組件處於exited狀態的時候,卸載組件。
type: boolean
default: false
複製代碼
若是爲true,在組件掛載的時候,展現過渡動畫。默認爲false,第一次掛載過渡動畫不生效。
type: boolean
default: true
複製代碼
若是爲true,表示容許enter狀態的過渡動畫生效,默認爲true
type: boolean
default: true
複製代碼
若是爲true,表示容許exit狀態的過渡動畫生效,默認爲true
type: Function
複製代碼
過渡動畫結束時執行的毀掉函數
type: number | { enter?: number, exit?: number }
複製代碼
addEndListener存在的時候,須要設置timeout,表示過渡動畫時間
timeout={{
enter: 300, //enter狀態動畫時間
exit: 500, //exit狀態動畫時間
}}
複製代碼
type: Function(node: HtmlElement, isAppearing: bool)
default: function noop() {}
複製代碼
源碼內部,status分別爲entering先後,entered以後執行的回調函數,CSSTransition組件便是利用這三個回調函數給組件增長不一樣的樣式,利用CSS動畫實現過渡效果。
type: Function(node: HtmlElement) -> void
default: function noop() {}
複製代碼
源碼內部,status分別爲exiting先後,exited以後執行的回調函數,CSSTransition組件便是利用這三個回調函數給組件增長不一樣的樣式,利用CSS動畫實現過渡效果。
// 經過設置props.timeout,獲取各個組件不一樣狀態下的timeout
getTimeouts() {
const { timeout } = this.props
let exit, enter, appear
exit = enter = appear = timeout
if (timeout != null && typeof timeout !== 'number') {
exit = timeout.exit
enter = timeout.enter
appear = timeout.appear
}
return { exit, enter, appear }
}
複製代碼
//setNextCallback爲一個閉包
// 傳入一個回調函數,返回一個只能執行一次回調函數的函數,能夠手動取消回調函數的執行
//執行一次以後自毀
setNextCallback(callback) {
//標誌位active用於保證只執行一次callback
let active = true
this.nextCallback = event => {
if (active) {
active = false
// 垃圾回收
this.nextCallback = null
callback(event)
}
}
//用於手動取消回調函數的執行
this.nextCallback.cancel = () => {
active = false
}
return this.nextCallback
}
複製代碼
safeSetState(nextState, callback) {
// This shouldn't be necessary, but there are weird race conditions with
// setState callbacks and unmounting in testing, so always make sure that
// we can cancel any pending setState callbacks after we unmount.
callback = this.setNextCallback(callback)
// callback執行一次以後再也不容許執行
this.setState(nextState, callback)
}
複製代碼
入場或者退場過渡動畫結束以後,根據addEndListener以及timeout執行自毀回調函數handler
// handler爲入場或者退場過渡動畫結束以後的處理函數
onTransitionEnd(node, timeout, handler) {
//給this.nextCallback從新設置回調函數
this.setNextCallback(handler)
// 不管是否設置了addEndListener仍是timeout,this.nextCallback都只執行一次
// 執行時機並不肯定,這裏常常會存在一些與預期不符的現象
if (node) {
//若是設置了addEndListener,而且監聽了事件,則事件觸發變執行this.nextCallback
if (this.props.addEndListener) {
// 執行自定義的過渡動畫結束後的回調函數
this.props.addEndListener(node, this.nextCallback)
}
//若是設置了timeout,則timeout以後執行this.nextCallback
if (timeout != null) {
setTimeout(this.nextCallback, timeout)
}
} else {
setTimeout(this.nextCallback, 0)
}
}
複製代碼
//在掛載階段與更新階段根據nextStatus的狀態執行入場或者退場動畫
updateStatus(mounting = false, nextStatus){...}
複製代碼
根據是不是第一次掛載,是否被TransitionGroup包裹,來設置組件的初始state。涉及到的props有: enter,appear,in
// 組件Transition掛載階段
constructor(props, context) {
super(props, context)
// 初始化appear:
// 當單獨使用Transition沒有被TransitionGroup包裹時,appear = props.appear
// 當被TransitionGroup包裹的時候,TransitionGroup處於正在掛載階段,子組件Transition是第一次掛載,所以appear = props.appear
// 當被TransitionGroup包裹的時候,TransitionGroup已經掛載完成,說明子組件Transition以前掛載並卸載過,所以appear = props.enter
let parentGroup = context.transitionGroup
let appear =
parentGroup && !parentGroup.isMounting ? props.enter : props.appear
let initialStatus
this.appearStatus = null
// 初始化this.appearStatus以及this.state.status
// 掛載的時候:
// in = true && appear = true : this.state.status = EXITED , this.appearStatus = ENTERING
// in = true && appear = false : this.state.status = ENTERED
// in = false && ( unmountOnExit = true || mountOnEnter = true ) : this.state.status = UNMOUNTED
// in = false && unmountOnExit = false && mountOnEnter = fasle : this.state.status = EXITED
if (props.in) {
if (appear) {
initialStatus = EXITED
this.appearStatus = ENTERING
} else {
initialStatus = ENTERED
}
} else {
if (props.unmountOnExit || props.mountOnEnter) {
initialStatus = UNMOUNTED
} else {
initialStatus = EXITED
}
}
this.state = { status: initialStatus }
this.nextCallback = null
}
複製代碼
掛載階段該函數返回null,不須要對state修改
static getDerivedStateFromProps({ in: nextIn }, prevState) {
// 掛載階段if條件爲false,返回null,不須要對state修改
// 更新階段,在執行退場動畫的時候,可能會返回{ status: EXITED }
if (nextIn && prevState.status === UNMOUNTED) {
return { status: EXITED }
}
return null
}
複製代碼
render() {
const status = this.state.status
//掛載階段:
// in = false && ( unmountOnExit = true || mountOnEnter = true ),Transition不會渲染任何組件
if (status === UNMOUNTED) {
return null
}
//掛載階段:
// in = true && appear = true : this.state.status = EXITED , this.appearStatus = ENTERING
// in = true && appear = false : this.state.status = ENTERED
// in = false && unmountOnExit = false && mountOnEnter = fasle : this.state.status = EXITED
const { children, ...childProps } = this.props
// filter props for Transtition
// 濾除與Transtition組件功能相關的props,其餘的props依舊能夠正常傳入須要過渡效果的業務組件
delete childProps.in
delete childProps.mountOnEnter
delete childProps.unmountOnExit
delete childProps.appear
delete childProps.enter
delete childProps.exit
delete childProps.timeout
delete childProps.addEndListener
delete childProps.onEnter
delete childProps.onEntering
delete childProps.onEntered
delete childProps.onExit
delete childProps.onExiting
delete childProps.onExited
// 當children === 'function',children函數能夠根據組件狀態執行相應邏輯
// (status) => (
// <MyComponent className={`fade fade-${status}`} />
// )
if (typeof children === 'function') {
return children(status, childProps)
}
//React.Children.only判斷是否只有一個子組件,若是是則返回這個子組件,若是不是則拋出一個錯誤
const child = React.Children.only(children)
return React.cloneElement(child, childProps)
}
複製代碼
開始執行
componentDidMount() {
// 第一次掛載的時候,若是in = true && appear = true,則appearStatus=ENTERING,不然爲null。
this.updateStatus(true, this.appearStatus)
}
複製代碼
其中updateStatus函數爲:appearStatus = ENTERING的時候執行performEnter
updateStatus(mounting = false, nextStatus) {
if (nextStatus !== null) {
// 掛載階段:若是nextStatus !== null,則只會出現 nextStatus = ENTERING
// in = true && appear = true:nextStatus = ENTERING
// nextStatus will always be ENTERING or EXITING.
this.cancelNextCallback() // 掛載階段無操做
const node = ReactDOM.findDOMNode(this) // 掛載階段找到真實DOM
// 掛載階段:若是in = true && appear = true,則執行performEnter
if (nextStatus === ENTERING) {
this.performEnter(node, mounting)
} else {
this.performExit(node)
}
} else if (this.props.unmountOnExit && this.state.status === EXITED) {
this.setState({ status: UNMOUNTED })
}
}
複製代碼
其中performEnter函數爲:執行onEnter回調函數 --> 設置{ status: ENTERING } --> 執行onEntering回調函數 --> 監聽onTransitionEnd過渡動畫是否完成 --> 設置{ status: ENTERED } --> 執行onEntered回調函數
performEnter(node, mounting) {
const { enter } = this.props
// 掛載階段:若是in = true && appear = true,則appearing = true
const appearing = this.context.transitionGroup
? this.context.transitionGroup.isMounting
: mounting
// 獲取timeouts
const timeouts = this.getTimeouts()
// 掛載階段如下if代碼不執行
// no enter animation skip right to ENTERED
// if we are mounting and running this it means appear _must_ be set
if (!mounting && !enter) {
this.safeSetState({ status: ENTERED }, () => {
this.props.onEntered(node)
})
return
}
//執行props.onEnter函數
//掛載階段,若是in = true && appear = true,則appearing始終爲true
// 若是在Transition組件上設置onEnter函數,能夠經過獲取該函數第二參數來獲取第一次掛載的時候是不是enter
this.props.onEnter(node, appearing)
// 改變{ status: ENTERING },改變以後執行一次回調函數
this.safeSetState({ status: ENTERING }, () => {
// 將狀態設置爲ENTERING以後,開始執行過渡動畫
this.props.onEntering(node, appearing)
// FIXME: appear timeout?
// timeouts.enter爲入場enter的持續時間
// 過渡動畫結束,設置{ status: ENTERED },執行onEntered回調函數
this.onTransitionEnd(node, timeouts.enter, () => {
//將狀態設置爲ENTERED,而後再執行onEntered回調函數
this.safeSetState({ status: ENTERED }, () => {
this.props.onEntered(node, appearing)
})
})
})
複製代碼
}
static getDerivedStateFromProps({ in: nextIn }, prevState) {
// 更新階段:
// 若是掛載階段in=true,那麼第一次更新if條件中prevState.status!== UNMOUNTED
// 若是掛載階段in=false,而且(props.mountOnEnter=true||props.mountOnEnter=true)
// 那麼第一次更新if條件中prevState.status === UNMOUNTED,能夠經過in的翻轉改變
// 若是(props.mountOnEnter=true||props.mountOnEnter=true)的時候,設置狀態status的狀態爲EXITED
if (nextIn && prevState.status === UNMOUNTED) {
return { status: EXITED }
}
return null
}
複製代碼
與掛載階段分析相似,組件保持原來狀態。
componentDidUpdate(prevProps) {
let nextStatus = null
if (prevProps !== this.props) {
const { status } = this.state
if (this.props.in) {
//根據in=true判斷此時須要進行入場動畫
if (status !== ENTERING && status !== ENTERED) {
//若是當前狀態既不是正在入場也不是已經入場,則將下一個狀態置爲正在入場
nextStatus = ENTERING
}
} else {
//根據in=false判斷此時須要進行退場動畫
if (status === ENTERING || status === ENTERED) {
//若是當前狀態是正在入場或者已經入場,則將下一個狀態置爲退場
nextStatus = EXITING
}
}
}
//更新狀態,執行過渡動畫,第一參數表示是否正在掛載
//若是Transition組件更新可是prevProps沒有變化,有多是多餘的從新。所以將nextStatus爲null
this.updateStatus(false, nextStatus)
}
複製代碼
其中updateStatus函數爲:
updateStatus(mounting = false, nextStatus) {
if (nextStatus !== null) {
// nextStatus will always be ENTERING or EXITING.
this.cancelNextCallback() // 掛載階段無操做
const node = ReactDOM.findDOMNode(this) // 掛載階段找到真實DOM
// 更新階段nextStatus只有兩種狀態ENTERING與EXITING:
// 若是爲ENTERING執行入場,EXITING執行退場
if (nextStatus === ENTERING) {
this.performEnter(node, mounting)
} else {
this.performExit(node)
}
} else if (this.props.unmountOnExit && this.state.status === EXITED) {
this.setState({ status: UNMOUNTED })
}
}
複製代碼
其中退場動畫performExit函數爲
//與performEnter邏輯類似
performExit(node) {
const { exit } = this.props
const timeouts = this.getTimeouts()
// no exit animation skip right to EXITED
if (!exit) {
this.safeSetState({ status: EXITED }, () => {
this.props.onExited(node)
})
return
}
this.props.onExit(node)
this.safeSetState({ status: EXITING }, () => {
this.props.onExiting(node)
this.onTransitionEnd(node, timeouts.exit, () => {
this.safeSetState({ status: EXITED }, () => {
this.props.onExited(node)
})
})
})
}
複製代碼
本文根據組件生命週期詳細的分析了react-transition-group中關鍵組件Transition的源碼,工做流程。CSSTransition組件就是對Transition組件的封裝,在其props.onEnter等等組件上添加對應的class實現css的動畫。該組件庫還有一個比較重要的地方就是TransitionGroup組件如何管理子組件動畫,弄清這個是實現複雜動畫邏輯的關鍵。