閱讀本文你會獲得:
-
一個相應的使用案例請看專案react-music-lhy,文件在blog中基於react-transition-group的react過渡動畫找到:元件掛載與解除安裝動畫的可以藉助appear以及onExit回撥函式實現。案例中onExit回撥函式主要用於通過路由跳轉解除安裝元件。
-
一個比較有用的技巧:本文中工具函式一節的safeSetState函式;以及TransitionGroup種dom-helpers工具庫的使用以及封裝。
react-transition-group官方指南,結合react-router的專案使用案例請參照此文件
全文中提到的第一次掛載與掛載的概念是指:Transition單獨使用的時候,不區分第一掛載與其他掛載,只有在父元件是TransitionGroup的時候才區分。這可以從constructor中如下程式碼看出來:
// 初始化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的分析。
Props介紹
children
type: Function | element
required
複製程式碼
某個狀態下需要過渡效果的目標元件,可以是函式
<Transition timeout={150}>
{(status) => (
<MyComponent className={`fade fade-${status}`} />
)}
</Transition>
複製程式碼
每個狀態'entering', 'entered', 'exiting', 'exited', 'unmounted'的時候執行的回撥函式,上面程式碼實現的是,每一個狀態就給某個子元件增加一個過渡樣式,可以非常靈活的給任意元件增加樣式,實現過渡效果。
in
type: boolean
default: false
複製程式碼
用於在enter與exit狀態之間翻轉,預設為false,表示不掛載元件或者處於exit狀態。
mountOnEnter
type: boolean
default: false
複製程式碼
在第一次in={true}即掛載的時候,設定mountOnEnter={true}表示延遲掛載,懶載入元件。
unmountOnExit
type: boolean
default: false
複製程式碼
如果為true,在元件處於exited狀態的時候,解除安裝元件。
appear
type: boolean
default: false
複製程式碼
如果為true,在元件掛載的時候,展示過渡動畫。預設為false,第一次掛載過渡動畫不生效。
enter
type: boolean
default: true
複製程式碼
如果為true,表示允許enter狀態的過渡動畫生效,預設為true
exit
type: boolean
default: true
複製程式碼
如果為true,表示允許exit狀態的過渡動畫生效,預設為true
addEndListener
type: Function
複製程式碼
過渡動畫結束時執行的毀掉函式
timeout
type: number | { enter?: number, exit?: number }
複製程式碼
addEndListener存在的時候,需要設定timeout,表示過渡動畫時間
timeout={{
enter: 300, //enter狀態動畫時間
exit: 500, //exit狀態動畫時間
}}
複製程式碼
onEnter,onEntering,onEntered
type: Function(node: HtmlElement, isAppearing: bool)
default: function noop() {}
複製程式碼
原始碼內部,status分別為entering前後,entered之後執行的回撥函式,CSSTransition元件即是利用這三個回撥函式給元件增加不同的樣式,利用CSS動畫實現過渡效果。
onExit,onExiting,onExited
type: Function(node: HtmlElement) -> void
default: function noop() {}
複製程式碼
原始碼內部,status分別為exiting前後,exited之後執行的回撥函式,CSSTransition元件即是利用這三個回撥函式給元件增加不同的樣式,利用CSS動畫實現過渡效果。
原始碼工具函式
getTimeouts函式
// 通過設定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為一個閉包
// 傳入一個回撥函式,返回一個只能執行一次回撥函式的函式,可以手動取消回撥函式的執行
//執行一次之後自毀
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函式:確保setState回撥函式只執行一次
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)
}
複製程式碼
onTransitionEnd函式
入場或者退場過渡動畫結束之後,根據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)
}
}
複製程式碼
updateStatus
//在掛載階段與更新階段根據nextStatus的狀態執行入場或者退場動畫
updateStatus(mounting = false, nextStatus){...}
複製程式碼
原始碼分析
掛載階段
constructor
根據是否是第一次掛載,是否被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
}
複製程式碼
getDerivedStateFromProps
掛載階段該函式返回null,不需要對state修改
static getDerivedStateFromProps({ in: nextIn }, prevState) {
// 掛載階段if條件為false,返回null,不需要對state修改
// 更新階段,在執行退場動畫的時候,可能會返回{ status: EXITED }
if (nextIn && prevState.status === UNMOUNTED) {
return { status: EXITED }
}
return null
}
複製程式碼
render
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
開始執行
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)
})
})
})
複製程式碼
}
更新階段
getDerivedStateFromProps
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
}
複製程式碼
render
與掛載階段分析類似,元件保持原來狀態。
componentDidUpdate
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元件如何管理子元件動畫,弄清這個是實現複雜動畫邏輯的關鍵。