react-transition-group源码浅析(一):Transition

#1

Props of Transition

目录

1. Props介绍

2. 源码工具函数

3. 从生命周期分析组件源码

阅读本文你会获得:

  • 一个相应的使用案例请看项目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组件如何管理子组件动画,弄清这个是实现复杂动画逻辑的关键。