阅读本文你会得到: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组件如何管理子组件动画,弄清这个是实现复杂动画逻辑的关键。