IT码农库

您当前所在位置:首页 > 网络编程 > JavaScript

JavaScript

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

互联网 donglegend2021-02-05JavaScript2089
这篇文章主要介绍了前端如何实现动画过渡效果,帮助大家更好的理解和学习前端开发网页,感兴趣的朋友可以了解下

简介

动画这个概念非常宽泛,涉及各个领域,这里我们把范围缩小到前端网页应用层面上,不用讲游戏领域的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类驱动视图自动刷新

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

大图广告(830*140)