How to realize animation transition effect in front end

  • 2021-10-27 06:09:37
  • OfStack

Directory Introduction
Traditional transition animation
css Transition Animation
js animation
Comparison between traditional and Vue/React frameworks
Transition Animation in Vue Frame
transition component
modules/transition
Transition Animation in React
Data-driven animation

Brief introduction

The concept of animation is very broad, involving various fields. Here, we narrow the scope to the front-end web application level, without talking about Animate in the game field.

At present, most web applications are developed based on frameworks, such as Vue, React, etc. They are all based on data-driven views, so let's compare 1, how do we realize animation or transition effects without these frameworks, and then how do we realize them using data-driven.

Traditional transition animation

Animation effect is very important for experience, but it may be a very weak link for many developers. After the advent of css3, the most commonly used animation transition for many beginners may be the ability of css3.

css Transition Animation

css start transition animation is very simple, write transition attribute is OK, write 1 demo below


<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;
}

The effect is still very good. transition of css3 basically meets most animation requirements. If not, there is a real css3 animation.

animate-css

The famous css animation library, who knows who uses it.

Whether it is css3 transition or css3 animation, we simply use it by switching the class name of class. If we want to do callback processing, the browser also provides animation frame events such as ontransitionend and onanimationend, which can be monitored through js interface.


var el = document.querySelector('#app')
el.addEventListener('transitionstart', () => {
  console.log('transition start')
})
el.addEventListener('transitionend', () => {
  console.log('transition end')
})

ok, this is the basis of css animation. Most animation transition requirements can also be realized through js encapsulation, but the limitation lies in that it can only control the attribute animation supported by css, and the control power is still slightly weaker.

js Animation

After all, js is a custom coding program, which has strong control over animation and can achieve various effects that css does not support. So what is the basis of js animation?
Simply put, the so-called animation is to constantly update the attributes of an element on the timeline, and then give it to the browser for redrawing, which becomes an animation visually. Cut the crap and have a chestnut first:


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

// Tween It's just a slow-moving function 
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(' End of animation and start over ')
    time = 0;
  }
  setTimeout(() => {
    startSport()
  }, fps)
}
startSport()

Constantly updating attributes on the timeline can be achieved through setTimeout or requestAnimation. As for Tween slow-moving function, it is similar to the concept of interpolation. Given a series of variables, you can get the value at any time in the interval. Pure mathematical formula, almost all animation frameworks will be used. If you want to know, you can refer to Zhang Xinxu's Tween.js

OK, this minimalist demo is also the core foundation of js animation. We can see that we perfectly control the generation process of transition values through the program, and all other complex animation mechanisms are in this mode.

Comparison of traditional and Vue/React frameworks

From the previous example, whether it is css transition or js transition, we directly get dom element, and then perform attribute operation on dom element.
Vue/React both introduce the concept of virtual dom. Data-driven view, we try not to operate dom, but only control data, so how can we drive animation at the data level?

Transition Animation in the Framework of Vue

You can look at the document once first

Vue Transition Animation

Let's not talk about how to use it. Let's analyze how the transition component provided by Vue realizes animation transition support.

transition Component

First look at the transition component code, path "src/platforms/web/runtime/components/transition. js"
The core code is as follows:


//  Auxiliary function, copy props Data of 
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, //  Abstract component, which means that it will not be rendered as a real dom Assisted development 

 render (h: Function) {
  //  Pass slots Get the real rendering element children
  let children: any = this.$slots.default
  
  const mode: string = this.mode

  const rawChild: VNode = children[0]

  //  Add only 1key
  // 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 Upward injection transition Property, save the props Transmitted data 
  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
 }
}

It can be seen that the function of this component itself is relatively simple, that is, the element children to be rendered is obtained through slots, and then the props attribute data copy of transition is applied to the transtion attribute of data for subsequent injection into the life cycle, and mergeVNodeHook is for life cycle management.

modules/transition

Then look down at the life cycle correlation, path:
src/platforms/web/runtime/modules/transition.js
Look at the default export first:


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)
  } 
 }
} : {}

Here, inBrowser is treated as true, because we are analyzing the browser environment.
Then look at the enter and leave functions, and first look at 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()
 }
 //  Upper 1 Step injection data Adj. transition Data 
 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
 }
 //  To get the right time, it should be injected 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)
 //  Callback processing after the end of the transition, deleting the 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 When entering, add start class Make a transition 
 beforeEnterHook && beforeEnterHook(el)
 if (expectsCSS) {
  //  Set the default style before the transition starts 
  addTransitionClass(el, startClass)
  addTransitionClass(el, activeClass)
  //  Under browser rendering 1 Frame   Delete the default style and add toClass
  //  Add end Event listening, callback is the above 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 uses a function whenTransitionEnds, which is actually listening for transition or animation end events:


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, at this point, according to the annotation analysis of the above source code, we can find:

Vue first encapsulates a series of auxiliary methods such as addClass/removeClass for operating dom and className. Then, immediately after the lifecycle enterHook, the default initial style of startClass, that is, enterClass, and activeClass are set Then, in the next frame of browser nextFrame, startClass is removed, toClass is added, and end event monitoring processing of transition animation is added After listening for the end event, move cb and remove toClass and activeClass

The process of leave is the same as that of enter, except that className is added and removed in reverse

Conclusion: Vue is essentially the same as the traditional dom, but it is processed in every life cycle of Vue, and it is processed at the time of adding and deleting dom in essence

Transition Animation in React

Oh, we turned over the React document, and we didn't find any transition animation processing. Hey, it seems that the official support is not original.

But we can do it ourselves, for example, maintain a state through useState, and switch className according to the state in render, but what should we do if it is complicated?

Fortunately, I found a wheel plug-in react-transition-group in the community
Well, post the source code directly. With the previous analysis of Vue, this is very easy to understand, but it is simpler:


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
 }

 //  Initial dom Update the default initial state when 
 componentDidMount() {
  this.updateStatus(true, this.appearStatus)
 }
 // data When updating, update the corresponding status 
 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
 }
 //  Monitor transition 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>
  )
 }
}

It can be seen that it is very similar to Vue, except that it is processed in various life cycle functions of React.

At this point, we will find that both the transiton component of Vue and the transiton-group component of React focus on the animation of css attributes.

Data-driven animation

In actual scenes, there will always be animations that css can't handle. At this time, there are two solutions:

Obtain dom through ref, and then adopt our traditional js scheme.
The data of dom is drawn by state state maintenance, and the state class is continuously updated by setState to drive the view to refresh automatically

The above is the front-end how to achieve animation transition effect details, more information about the front-end animation transition effect please pay attention to other related articles on this site!


Related articles: