react-transition-group源碼淺析(一):Transition.md

閱讀本文你會得到: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

Props介紹

children

type: Function | element
required
複製代碼

某個狀態下須要過渡效果的目標組件,能夠是函數閉包

<Transition timeout={150}>
  {(status) => (
    <MyComponent className={`fade fade-${status}`} />
  )}
</Transition>
複製代碼

每一個狀態'entering', 'entered', 'exiting', 'exited', 'unmounted'的時候執行的回調函數,上面代碼實現的是,每個狀態就給某個子組件增長一個過渡樣式,能夠很是靈活的給任意組件增長樣式,實現過渡效果。app

in

type: boolean
default: false
複製代碼

用於在enter與exit狀態之間翻轉,默認爲false,表示不掛載組件或者處於exit狀態。dom

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組件如何管理子組件動畫,弄清這個是實現複雜動畫邏輯的關鍵。

相關文章
相關標籤/搜索