前端如何实现动画过渡效果
目录
- 简介
- 传统过渡动画
- 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
