简介

动画这个概念十分宽泛,波及各个领域,这里咱们把范畴放大到前端网页利用层面上,不必讲游戏畛域的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>
#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()

在时间轴上不断更新属性,能够通过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无奈解决的动画,这个时候,能够有两种解决方案:

  1. 通过ref获取dom,而后采纳咱们传统的js计划。
  2. 通过state状态保护绘制dom的数据,一直通过setState更新state类驱动视图主动刷新