阅读本文你会获得:
-
一个相应的使用案例请看,文档在blog中找到:组件挂载与卸载动画的可以借助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 | elementrequired复制代码
某个状态下需要过渡效果的目标组件,可以是函数
{(status) => ( 复制代码)}
每个状态'entering', 'entered', 'exiting', 'exited', 'unmounted'的时候执行的回调函数,上面代码实现的是,每一个状态就给某个子组件增加一个过渡样式,可以非常灵活的给任意组件增加样式,实现过渡效果。
in
type: booleandefault: false复制代码
用于在enter与exit状态之间翻转,默认为false,表示不挂载组件或者处于exit状态。
mountOnEnter
type: booleandefault: false复制代码
在第一次in={true}即挂载的时候,设置mountOnEnter={true}表示延迟挂载,懒加载组件。
unmountOnExit
type: booleandefault: false复制代码
如果为true,在组件处于exited状态的时候,卸载组件。
appear
type: booleandefault: false复制代码
如果为true,在组件挂载的时候,展示过渡动画。默认为false,第一次挂载过渡动画不生效。
enter
type: booleandefault: true复制代码
如果为true,表示允许enter状态的过渡动画生效,默认为true
exit
type: booleandefault: 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) -> voiddefault: 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) => ( //// ) 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组件如何管理子组件动画,弄清这个是实现复杂动画逻辑的关键。