首页前端开发JavaScript前端如何实现动画过渡效果

前端如何实现动画过渡效果

时间2024-01-31 18:31:03发布访客分类JavaScript浏览354
导读:收集整理的这篇文章主要介绍了前端如何实现动画过渡效果,觉得挺不错的,现在分享给大家,也给大家做个参考。 目录简介传统过渡动画css过渡动画js动画传统和Vue/React框架对比Vue框...
收集整理的这篇文章主要介绍了前端如何实现动画过渡效果,觉得挺不错的,现在分享给大家,也给大家做个参考。
目录
  • 简介
  • 传统过渡动画
  • css过渡动画
  • js动画
  • 传统和Vue/React框架对比
  • Vue框架下的过渡动画
    • transition组件
    • modules/transition
  • React里的过渡动画
    • 数据驱动的动画

      简介

      动画这个概念非常宽泛,涉及各个领域,这里我们把范围缩小到前端网页应用层面上,不用讲游戏领域的AniMATE,一切从最简单的开始。

      目前大部分网页应用都是基于框架开发的,比如Vue,React等,它们都是基于数据驱动视图的,那么让我们来对比一下,还没有这些框架的时候我们如何实现动画或者过渡效果,然后使用数据驱动又是如何实现的。

      传统过渡动画

      动画效果对体验有着非常重要的效果,但是对于很多开发者来讲,可能是个非常薄弱的环节。在css3出现之后,很多初学者最常用的动画过渡可能就是css3的能力了。

      css过渡动画

      css启动过渡动画非常简单,书写transITion属性就可以了,下面写一个demo

      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基本满足了大部分动画需求,如果不满足还有真正的css3 animation。

      animate-css

      大名鼎鼎的css动画库,谁用谁知道。

      不管是css3 transition 还是 css3 animation,我们简单使用都是通过切换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,这个极简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   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类驱动视图自动刷新

      以上就是前端如何实现动画过渡效果的详细内容,更多关于前端实现动画过渡效果的资料请关注其它相关文章!

      您可能感兴趣的文章:
      • vue实现列表滚动的过渡动画
      • Vue 解决路由过渡动画抖动问题(实例详解)
      • 解决vue的过渡动画无法正常实现问题
      • vue-router之实现导航切换过渡动画效果
      • 使用vue-router切换页面时实现设置过渡动画
      • vue 组件中使用 transition 和 transition-group实现过渡动画
      • Vue运用transition实现过渡动画
      • Vue动画事件详解及过渡动画实例
      • Vue中的基础过渡动画及实现原理解析

      声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!

      前端动画

      若转载请注明出处: 前端如何实现动画过渡效果
      本文地址: https://pptw.com/jishu/594323.html
      算术运算符、赋值运算符和关系运算符的优先级是什么? c++中类的定义是什么

      游客 回复需填写必要信息