전면에서 어떻게 애니메이션 전환 효과를 실현합니까

소개


애니메이션이라는 개념은 매우 광범위하고 각 분야와 관련된다. 여기서 우리는 범위를 전방 웹 응용 차원으로 축소했다. 게임 분야의 애니메이트는 말할 것도 없고 모든 것은 가장 간단한 것부터 시작한다.
현재 대부분의 웹 응용 프로그램은 프레임워크를 바탕으로 개발된 것이다. 예를 들어 Vue,React 등은 모두 데이터 구동 보기를 바탕으로 한다. 그러면 이런 프레임워크가 없을 때 우리가 어떻게 애니메이션이나 과도 효과를 실현하고 데이터 구동을 사용하면 어떻게 실현하는지 비교해 보자.

기존 변환 애니메이션


애니메이션 효과는 체험에 매우 중요한 효과가 있지만 많은 개발자들에게 매우 약한 부분일 수 있다.css3가 등장한 후에 많은 초보자들이 가장 자주 사용하는 애니메이션 과도는 아마도 css3의 능력일 것이다.

css 변환 애니메이션


css 시작 과도 애니메이션은 매우 간단합니다.transition 속성을 쓰면 됩니다. 다음은 데모를 쓰십시오

<div id="app" class="normal"></div>

.normal {
  width: 100px;
  height: 100px;
  background-color: red;
  transition: all 0.3s;
}
.normal:hover {
  background-color: yellow;
  width: 200px;
  height: 200px;
}
효과는 여전히 훌륭하다. css3의transition은 대부분의 애니메이션 수요를 기본적으로 만족시켰다. 만약에 만족하지 않으면 진정한 css3animation도 있다.
animate-css
명성이 자자한 css 애니메이션 라이브러리, 누가 쓰는지 누가 알겠는가.
css3transition이든 css3animation이든 우리가 간단하게 사용하는 것은class 클래스 이름을 바꾸는 것이다. 만약에 리셋 처리를 하려면 브라우저도ontransitionend,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가 애니메이션을 실현하는 기초는 무엇입니까?
간단하게 말하면 애니메이션이란 시간축에서 어떤 요소의 속성을 끊임없이 업데이트한 다음에 브라우저에 맡기고 다시 그리는 것이다. 시각적으로 애니메이션이 된다.잔말 말고 밤부터 주세요.

 <div id="app" class="normal"></div>

// 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()
타임라인에서 속성을 계속 업데이트하면 setTimeout이나 requestAnimation을 통해 실현할 수 있습니다.Tween 완화 함수는 삽입값과 유사한 개념으로 일련의 변수를 지정한 다음에 구간 구간에서 임의의 시간의 값을 얻을 수 있다. 순수 수학 공식은 거의 모든 애니메이션 프레임워크를 사용하므로 알고 싶은 것은 참고할 수 있다장신욱의 Tween.js
OK, 이 미니멀 데모도 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를 데이터의transition 속성에 추가하여 후속 주입 생명주기에 사용하도록 하는 것이다.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는 먼저domclassName의 보조 방법인addClass/removeClass 등을 봉인했습니다
  • 그리고 생명주기 enter Hook 후에 start Class, 즉 enter Class의 기본 초기 스타일과 active Class를 설정했습니다
  • 이어서 브라우저nextFrame 다음 프레임에서 startClass를 제거하고 toClass를 추가하며 과도 애니메이션의end 이벤트 감청 처리를 추가했습니다
  • end 이벤트를 감청한 후 cb를 이동하여 toClass와 activeClass를 제거했습니다.
  • leave의 프로세스와 enter의 처리 프로세스는 같습니다.class 제거를 반대로 추가하는 것입니다Name
    결론: Vue의 애니메이션 과도 처리 방식은 전통적인dom와 본질적으로 똑같다. 단지 Vue의 각 생명주기에 융합되어 처리되었을 뿐, 본질적으로dom에서 삭제할 시기를 추가하여 처리한다.

    React의 변환 애니메이션


    아, React 문서를 뒤적였지만 과도 애니메이션 처리가 발견되지 않았습니다.어이, 보아하니 정부의 비원생적인 지지가 있는 것 같다.
    그러나 우리는usestate를 통해 하나의 상태를 유지하고 렌더에서 상태에 따라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
       <TransitionGroupContext.Provider value={null}>
        {typeof children === 'function'
         ? children(status, childProps)
         : React.cloneElement(React.Children.only(children), childProps)}
       </TransitionGroupContext.Provider>
      )
     }
    }
    
    Vue와 매우 비슷하지만 React의 각 생명주기 함수로 처리되었습니다.
    여기까지 오면 Vue의transiton 구성 요소든React이transiton-group 구성 요소든 css 속성의 애니메이션에 중점을 두고 처리하는 것을 발견할 수 있습니다.

    데이터 기반 애니메이션


    실제 장면에서 css가 처리할 수 없는 애니메이션을 만날 수 있다. 이때 두 가지 해결 방안이 있다.
    ref를 통해dom를 얻고 우리의 전통적인 js 방안을 사용합니다.
    state 상태 유지보수를 통해dom의 데이터를 그리고, setState를 통해state 클래스 구동 보기를 계속 업데이트합니다.
    이상은 바로 전단이 어떻게 애니메이션 과도 효과를 실현하는지에 대한 상세한 내용입니다. 전단이 애니메이션 과도 효과를 실현하는 것에 대한 더 많은 자료는 저희 다른 관련 글에 주목하세요!

    좋은 웹페이지 즐겨찾기