アニメーションのトランジション効果をどのように実現しますか?

17730 ワード

概要
アニメーションという概念は非常に広く、各分野に及んでいます.ここでは、ゲーム分野のアニメーションはもちろん、最も簡単なことから始めます.
現在、ほとんどのWebアプリケーションはフレームワークに基づいて開発されています.例えば、Vue、Reactなど、データ駆動ビューに基づいています.では、これらのフレームワークがない場合、アニメーションや移行効果をどのように実現し、データ駆動を使用してどのように実現するかを比較してみましょう.
従来のトランジションアニメーション
アニメーション効果は体験にとって非常に重要な効果がありますが、多くの開発者にとって、非常に弱い部分かもしれません.css 3が現れた後、多くの初心者が最もよく使うアニメーションの移行はcss 3の能力かもしれません.
css遷移アニメーション
cssは移行アニメーションを起動するのはとても簡単で、transition属性を書くだけでいいです.次はdemoを書きます.
.normal {
    width: 100px;
    height: 100px;
    background-color: red;
    transition: all 0.3s;
}
.normal:hover {
    background-color: yellow;
    width: 200px;
    height: 200px;
}

効果はやはりとても良くて、css 3のtransitionは基本的に大部分のアニメーションの需要を満たして、もし満足しなければ本当のcss 3 animationがあります.animate-cssの有名なcssアニメーションライブラリは、誰が知っていますか.
css 3 transitionでもcss 3 animationでも簡単にclassクラス名を切り替えることで、コールバック処理を行う場合はブラウザにontransitionnd、onanimationendなどのアニメーションフレームイベントが提供され、jsインタフェースで傍受すればよい.
var el = document.querySelector('#app')
el.addEventListener('transitionstart', () => {
    console.log('transition start')
})
el.addEventListener('transitionend', () => {
    console.log('transition end')
})

OK、これがcssアニメーションの基礎であり、jsパッケージによっても大部分のアニメーション遷移需要を実現することができるが、限界はcssがサポートする属性アニメーションを制御するしかないのに対し、制御力はまだ少し弱い.
jsアニメーション
jsはあくまでもカスタムエンコーディングプログラムであり、アニメーションに対する制御力が強く、様々なcssがサポートしていない効果を実現することができます.ではjsがアニメーションを実現する基礎は何ですか?簡単に言えば、アニメーションとは、時間軸上で要素のプロパティを更新し続け、ブラウザに渡して再描画し、視覚的にアニメーションになります.くだらないことは言わないで、まず栗をください.
 
#app {
    width: 100px;
    height: 100px;
    background-color: red;
    border-radius: 50%;
}
// Tween        
var el = document.querySelector('#app')
var time = 0, begin = 0, change = 500, duration = 1000, fps = 1000 / 60;
function startSport() {
    var val = Tween.Elastic.easeInOut(time, begin, change, duration);
    el.style.transform = 'translateX(' + val + 'px)';
    if (time <= duration) {
        time += fps
    } else {
        console.log('        ')
        time = 0;
    }
    setTimeout(() => {
        startSport()
    }, fps)
}
startSport()

時間軸上で属性を更新し続け、settimoutまたはrequestAnimationで実現できます.Tween緩動関数については、補間のような概念で、一連の変数を与え、区間セグメントで任意の時刻の値を取得することができ、純粋な数学式では、ほとんどのアニメーションフレームワークが使用され、知りたいことは張鑫旭のTween.jsを参照してください.
OK、この極めて簡単なdemoもjsがアニメーションを実現する核心の基礎で、私たちがプログラムを通じて移行値の生成過程を完璧に制御したことを見ることができて、その他の複雑なアニメーションのメカニズムはすべてこのモードです.
従来とVue/Reactフレームワークの比較
前の例では、css遷移でもjs遷移でもdom要素を直接取得し、dom要素を属性操作します.Vue/Reactはすべて仮想domの概念を導入して、データはビューを駆動して、私達はできるだけdomを操作しないで、データだけを制御して、それでは私達はどのようにデータの方面でアニメーションを駆動しますか?
Vueフレームの下での遷移アニメーション
まず、ドキュメントVue遷移アニメーションを参照してください.Vueが提供するtransitionコンポーネントがアニメーション移行サポートをどのように実現しているかを分析します.
Transitionコンポーネント
まずtransitionコンポーネントコードを参照してください.パス「src/platforms/web/runtime/components/transition.js」のコアコードは次のとおりです.
//     ,  props   
export function extractTransitionData (comp: Component): Object {
  const data = {}
  const options: ComponentOptions = comp.$options
  // props
  for (const key in options.propsData) {
    data[key] = comp[key]
  }
  // events.
  const listeners: ?Object = options._parentListeners
  for (const key in listeners) {
    data[camelize(key)] = listeners[key]
  }
  return data
}

export default {
  name: 'transition',
  props: transitionProps,
  abstract: true, //     ,          dom,    

  render (h: Function) {
    //   slots         children
    let children: any = this.$slots.default
    
    const mode: string = this.mode

    const rawChild: VNode = children[0]

    //     key
    // component instance. This key will be used to remove pending leaving nodes
    // during entering.
    const id: string = `__transition-${this._uid}-`
    child.key = getKey(id)
        : child.key
    // data   transition  ,    props     
    const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
    const oldRawChild: VNode = this._vnode
    const oldChild: VNode = getRealChild(oldRawChild)

   
      // important for dynamic transitions!
      const oldData: Object = oldChild.data.transition = extend({}, data)
  // handle transition mode
      if (mode === 'out-in') {
        // return placeholder node and queue update when leave finishes
        this._leaving = true
        mergeVNodeHook(oldData, 'afterLeave', () => {
          this._leaving = false
          this.$forceUpdate()
        })
        return placeholder(h, rawChild)
      } else if (mode === 'in-out') {
        let delayedLeave
        const performLeave = () => { delayedLeave() }
        mergeVNodeHook(data, 'afterEnter', performLeave)
        mergeVNodeHook(data, 'enterCancelled', performLeave)
        mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
      }
    return rawChild
  }
}

このコンポーネント自体の機能は簡単で、slotsを通じてレンダリングする要素childrenを手に入れ、transitionのprops属性データcopyをdataのtranstion属性に送り、後続の注入ライフサイクルに使用し、mergeVNodeHookはライフサイクル管理を行うことがわかります.
modules/transition
次にライフサイクル関連を下に見ます.経路:src/platforms/web/runtime/modules/transition.jsはまずデフォルトのエクスポートを参照します.
function _enter (_: any, vnode: VNodeWithData) {
  if (vnode.data.show !== true) {
    enter(vnode)
  }
}
export default inBrowser ? {
  create: _enter,
  activate: _enter,
  remove (vnode: VNode, rm: Function) {
    if (vnode.data.show !== true) {
      leave(vnode, rm)
    } 
  }
} : {}

ここでinBrowserはtrueとして扱われます.ブラウザ環境を分析しているからです.次にenterとleave関数を見て、まずenterを見ます.
export function addTransitionClass (el: any, cls: string) {
  const transitionClasses = el._transitionClasses || (el._transitionClasses = [])
  if (transitionClasses.indexOf(cls) < 0) {
    transitionClasses.push(cls)
    addClass(el, cls)
  }
}

export function removeTransitionClass (el: any, cls: string) {
  if (el._transitionClasses) {
    remove(el._transitionClasses, cls)
  }
  removeClass(el, cls)
}
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
  const el: any = vnode.elm

  // call leave callback now
  if (isDef(el._leaveCb)) {
    el._leaveCb.cancelled = true
    el._leaveCb()
  }
  //      data transition  
  const data = resolveTransition(vnode.data.transition)
  if (isUndef(data)) {
    return
  }

  /* istanbul ignore if */
  if (isDef(el._enterCb) || el.nodeType !== 1) {
    return
  }

  const {
    css,
    type,
    enterClass,
    enterToClass,
    enterActiveClass,
    appearClass,
    appearToClass,
    appearActiveClass,
    beforeEnter,
    enter,
    afterEnter,
    enterCancelled,
    beforeAppear,
    appear,
    afterAppear,
    appearCancelled,
    duration
  } = data

 
  let context = activeInstance
  let transitionNode = activeInstance.$vnode

  const isAppear = !context._isMounted || !vnode.isRootInsert

  if (isAppear && !appear && appear !== '') {
    return
  }
  //             className
  const startClass = isAppear && appearClass
    ? appearClass
    : enterClass
  const activeClass = isAppear && appearActiveClass
    ? appearActiveClass
    : enterActiveClass
  const toClass = isAppear && appearToClass
    ? appearToClass
    : enterToClass

  const beforeEnterHook = isAppear
    ? (beforeAppear || beforeEnter)
    : beforeEnter
  const enterHook = isAppear
    ? (typeof appear === 'function' ? appear : enter)
    : enter
  const afterEnterHook = isAppear
    ? (afterAppear || afterEnter)
    : afterEnter
  const enterCancelledHook = isAppear
    ? (appearCancelled || enterCancelled)
    : enterCancelled

  const explicitEnterDuration: any = toNumber(
    isObject(duration)
      ? duration.enter
      : duration
  )

  const expectsCSS = css !== false && !isIE9
  const userWantsControl = getHookArgumentsLength(enterHook)
  //            ,      class
  const cb = el._enterCb = once(() => {
    if (expectsCSS) {
      removeTransitionClass(el, toClass)
      removeTransitionClass(el, activeClass)
    }
    if (cb.cancelled) {
      if (expectsCSS) {
        removeTransitionClass(el, startClass)
      }
      enterCancelledHook && enterCancelledHook(el)
    } else {
      afterEnterHook && afterEnterHook(el)
    }
    el._enterCb = null
  })


  // dom   ,  start class    
  beforeEnterHook && beforeEnterHook(el)
  if (expectsCSS) {
   //              
    addTransitionClass(el, startClass)
    addTransitionClass(el, activeClass)
    //                ,  toClass
    //   end    ,       cb
    nextFrame(() => {
      removeTransitionClass(el, startClass)
      if (!cb.cancelled) {
        addTransitionClass(el, toClass)
        if (!userWantsControl) {
          if (isValidDuration(explicitEnterDuration)) {
            setTimeout(cb, explicitEnterDuration)
          } else {
            whenTransitionEnds(el, type, cb)
          }
        }
      }
    })
  }

  if (vnode.data.show) {
    toggleDisplay && toggleDisplay()
    enterHook && enterHook(el, cb)
  }

  if (!expectsCSS && !userWantsControl) {
    cb()
  }
}

Enterでは、遷移またはアニメーション終了のイベントをリスニングする関数whenTransitionEndsが使用されています.
export let transitionEndEvent = 'transitionend'
export let animationEndEvent = 'animationend'
export function whenTransitionEnds (
  el: Element,
  expectedType: ?string,
  cb: Function
) {
  const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
  if (!type) return cb()
  const event: string = type === TRANSITION ? transitionEndEvent : animationEndEvent
  let ended = 0
  const end = () => {
    el.removeEventListener(event, onEnd)
    cb()
  }
  const onEnd = e => {
    if (e.target === el) {
      if (++ended >= propCount) {
        end()
      }
    }
  }
  setTimeout(() => {
    if (ended < propCount) {
      end()
    }
  }, timeout + 1)
  el.addEventListener(event, onEnd)
}

OK、ここに着いたら、上のソースコードの注釈分析に基づいて、私たちは発見することができます:
  • Vueは、まずいくつかのカラム操作dom classNameの補助方法addClass/removeClassなどをカプセル化しています.
  • ライフサイクルenterHookの後、startClass、すなわちenterClassのデフォルトの初期スタイルがすぐに設定され、activeClass
  • もあります.
  • ブラウザnextFrameの次のフレームに続いてstartClassが除去され、toClassが追加され、遷移アニメーションが追加されたendイベント傍受処理
  • が追加される.
  • endイベントを傍受した後、cbを異動し、toClassとactiveClass
  • を除去した.
    leaveのプロシージャはenterのプロシージャと同じですが、classNameを逆方向に追加して削除するだけです.
    結論:Vueのアニメーション遷移処理方式は従来のdomと本質的に同じであり、Vueの各ライフサイクルに溶け込んで処理するにすぎず、本質的にはdomに削除を追加するタイミングで処理する
    Reactの遷移アニメーション
    ああ、Reactのドキュメントをめくっても、移行アニメーションの処理は見つかりませんでした.ねえ、公式の不原生支持のようです.
    しかし、私たちは自分で実現することができます.例えば、useStateでステータスを維持し、renderでステータスに応じてclassNameの切り替えを行うことができますが、複雑な場合はどうすればいいですか.
    幸いにもコミュニティでホイールプラグインreact-transition-groupを見つけました.ええ、ソースコードを直接貼って、前のVueの分析があります.これはとても理解しやすくて、かえって簡単です.
    class Transition extends React.Component {
      static contextType = TransitionGroupContext
    
      constructor(props, context) {
        super(props, context)
        let parentGroup = context
        let appear =
          parentGroup && !parentGroup.isMounting ? props.enter : props.appear
    
        let initialStatus
    
        this.appearStatus = null
    
        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
      }
    
      //   dom   ,        
      componentDidMount() {
        this.updateStatus(true, this.appearStatus)
      }
     // data     ,       
      componentDidUpdate(prevProps) {
        let nextStatus = null
        if (prevProps !== this.props) {
          const { status } = this.state
    
          if (this.props.in) {
            if (status !== ENTERING && status !== ENTERED) {
              nextStatus = ENTERING
            }
          } else {
            if (status === ENTERING || status === ENTERED) {
              nextStatus = EXITING
            }
          }
        }
        this.updateStatus(false, nextStatus)
      }
    
      updateStatus(mounting = false, nextStatus) {
        if (nextStatus !== null) {
          // nextStatus will always be ENTERING or EXITING.
          this.cancelNextCallback()
    
          if (nextStatus === ENTERING) {
            this.performEnter(mounting)
          } else {
            this.performExit()
          }
        } else if (this.props.unmountOnExit && this.state.status === EXITED) {
          this.setState({ status: UNMOUNTED })
        }
      }
    
      performEnter(mounting) {
        const { enter } = this.props
        const appearing = this.context ? this.context.isMounting : mounting
        const [maybeNode, maybeAppearing] = this.props.nodeRef
          ? [appearing]
          : [ReactDOM.findDOMNode(this), appearing]
    
        const timeouts = this.getTimeouts()
        const enterTimeout = appearing ? timeouts.appear : timeouts.enter
        // no enter animation skip right to ENTERED
        // if we are mounting and running this it means appear _must_ be set
        if ((!mounting && !enter) || config.disabled) {
          this.safeSetState({ status: ENTERED }, () => {
            this.props.onEntered(maybeNode)
          })
          return
        }
    
        this.props.onEnter(maybeNode, maybeAppearing)
    
        this.safeSetState({ status: ENTERING }, () => {
          this.props.onEntering(maybeNode, maybeAppearing)
    
          this.onTransitionEnd(enterTimeout, () => {
            this.safeSetState({ status: ENTERED }, () => {
              this.props.onEntered(maybeNode, maybeAppearing)
            })
          })
        })
      }
    
      performExit() {
        const { exit } = this.props
        const timeouts = this.getTimeouts()
        const maybeNode = this.props.nodeRef
          ? undefined
          : ReactDOM.findDOMNode(this)
    
        // no exit animation skip right to EXITED
        if (!exit || config.disabled) {
          this.safeSetState({ status: EXITED }, () => {
            this.props.onExited(maybeNode)
          })
          return
        }
    
        this.props.onExit(maybeNode)
    
        this.safeSetState({ status: EXITING }, () => {
          this.props.onExiting(maybeNode)
    
          this.onTransitionEnd(timeouts.exit, () => {
            this.safeSetState({ status: EXITED }, () => {
              this.props.onExited(maybeNode)
            })
          })
        })
      }
    
      cancelNextCallback() {
        if (this.nextCallback !== null) {
          this.nextCallback.cancel()
          this.nextCallback = null
        }
      }
    
      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)
        this.setState(nextState, callback)
      }
    
      setNextCallback(callback) {
        let active = true
    
        this.nextCallback = event => {
          if (active) {
            active = false
            this.nextCallback = null
    
            callback(event)
          }
        }
    
        this.nextCallback.cancel = () => {
          active = false
        }
    
        return this.nextCallback
      }
      //     end
      onTransitionEnd(timeout, handler) {
        this.setNextCallback(handler)
        const node = this.props.nodeRef
          ? this.props.nodeRef.current
          : ReactDOM.findDOMNode(this)
    
        const doesNotHaveTimeoutOrListener =
          timeout == null && !this.props.addEndListener
        if (!node || doesNotHaveTimeoutOrListener) {
          setTimeout(this.nextCallback, 0)
          return
        }
    
        if (this.props.addEndListener) {
          const [maybeNode, maybeNextCallback] = this.props.nodeRef
            ? [this.nextCallback]
            : [node, this.nextCallback]
          this.props.addEndListener(maybeNode, maybeNextCallback)
        }
    
        if (timeout != null) {
          setTimeout(this.nextCallback, timeout)
        }
      }
    
      render() {
        const status = this.state.status
    
        if (status === UNMOUNTED) {
          return null
        }
    
        const {
          children,
          // filter props for `Transition`
          in: _in,
          mountOnEnter: _mountOnEnter,
          unmountOnExit: _unmountOnExit,
          appear: _appear,
          enter: _enter,
          exit: _exit,
          timeout: _timeout,
          addEndListener: _addEndListener,
          onEnter: _onEnter,
          onEntering: _onEntering,
          onEntered: _onEntered,
          onExit: _onExit,
          onExiting: _onExiting,
          onExited: _onExited,
          nodeRef: _nodeRef,
          ...childProps
        } = this.props
    
        return (
          // allows for nested Transitions
          
            {typeof children === 'function'
              ? children(status, childProps)
              : React.cloneElement(React.Children.only(children), childProps)}
          
        )
      }
    }
    

    Vueと非常に似ていることがわかるが,ここではReactの各ライフサイクル関数で処理するようになった.
    ここまで来ると、VueのtransitonコンポーネントでもReactというtransiton-groupコンポーネントでも、css属性のアニメーションに重点を置いていることがわかります.
    データ駆動のアニメーション
    実際のシーンではcssでは処理できないアニメーションが常に発生します.この場合、2つの解決策があります.
  • はrefによってdomを取得し、従来のjsスキームを採用する.
  • state状態メンテナンスにより描画domのデータを維持し、setState更新stateクラス駆動ビューにより
  • を自動的にリフレッシュする.