(编辑:jimmy 日期: 2024/12/23 浏览:2)
动画这个概念非常宽泛,涉及各个领域,这里我们把范围缩小到前端网页应用层面上,不用讲游戏领域的Animate,一切从最简单的开始。
目前大部分网页应用都是基于框架开发的,比如Vue,React等,它们都是基于数据驱动视图的,那么让我们来对比一下,还没有这些框架的时候我们如何实现动画或者过渡效果,然后使用数据驱动又是如何实现的。
动画效果对体验有着非常重要的效果,但是对于很多开发者来讲,可能是个非常薄弱的环节。在css3出现之后,很多初学者最常用的动画过渡可能就是css3的能力了。
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毕竟是自定义编码程序,对于动画的控制力就很强大了,而且能实现各种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实现动画的核心基础了,可以看到我们通过程序完美的控制了过渡值的生成过程,所有其他复杂的动画机制都是这个模式。
通过前面的例子,无论是css过渡还是js过渡,我们都是直接获取到 dom元素的,然后对dom元素进行属性操作。
Vue/React都引入了虚拟dom的概念,数据驱动视图,我们尽量不去操作dom,只控制数据,那么我们如何在数据层面驱动动画呢?
可以先看一遍文档
Vue过渡动画
我们就不讲如何使用了,我们来分析一下Vue提供的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: "htmlcode">function _enter (_: any, vnode: VNodeWithData) { if (vnode.data.show !== true) { enter(vnode) } } export default inBrowser "htmlcode">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: "htmlcode">export let transitionEndEvent = 'transitionend' export let animationEndEvent = 'animationend' export function whenTransitionEnds ( el: Element, expectedType: "_blank" href="https://github.com/reactjs/react-transition-group" rel="external nofollow" >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类驱动视图自动刷新以上就是前端如何实现动画过渡效果的详细内容,更多关于前端实现动画过渡效果的资料请关注其它相关文章!