服务器之家:专注于服务器技术及软件下载分享
分类导航

node.js|vue.js|jquery|angularjs|React|json|js教程|

服务器之家 - 编程语言 - JavaScript - js教程 - 前端如何实现动画过渡效果

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

2022-01-17 16:37donglegend js教程

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

简介

动画这个概念非常宽泛,涉及各个领域,这里我们把范围缩小到前端网页应用层面上,不用讲游戏领域的Animate,一切从最简单的开始。

目前大部分网页应用都是基于框架开发的,比如Vue,React等,它们都是基于数据驱动视图的,那么让我们来对比一下,还没有这些框架的时候我们如何实现动画或者过渡效果,然后使用数据驱动又是如何实现的。

传统过渡动画

动画效果对体验有着非常重要的效果,但是对于很多开发者来讲,可能是个非常薄弱的环节。在css3出现之后,很多初学者最常用的动画过渡可能就是css3的能力了。

css过渡动画

css启动过渡动画非常简单,书写transition属性就可以了,下面写一个demo

?
1
<div id="app" class="normal"></div>
?
1
2
3
4
5
6
7
8
9
10
11
.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接口进行监听即可。

?
1
2
3
4
5
6
7
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 实现动画的基础是什么?
简单来讲,所谓动画就是在 时间轴上不断更新某个元素的属性,然后交给浏览器重新绘制,在视觉上就成了动画。废话少说,还是先来个栗子:

?
1
<div id="app" class="normal"></div>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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”
核心代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 辅助函数,复制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
先看默认导出:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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,其实就是监听过渡或者动画结束的事件:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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的分析,这个非常容易理解,反而更简单:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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类驱动视图自动刷新

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

原文链接:https://segmentfault.com/a/1190000039173935

延伸 · 阅读

精彩推荐
  • js教程基于JavaScript实现简单扫雷游戏

    基于JavaScript实现简单扫雷游戏

    这篇文章主要介绍了基于JavaScript实现简单扫雷游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    北冰洋_WH4382021-12-23
  • js教程Javascript生成器(Generator)的介绍与使用

    Javascript生成器(Generator)的介绍与使用

    这篇文章主要给大家介绍了关于Javascript生成器(Generator)的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值...

    疯狂的技术宅3362022-01-12
  • js教程绘制微信小程序验证码功能的实例代码

    绘制微信小程序验证码功能的实例代码

    这篇文章主要介绍了绘制微信小程序验证码功能的实例代码,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参...

    WALL*E8802021-12-27
  • js教程three.js显示中文字体与tween应用详析

    three.js显示中文字体与tween应用详析

    这篇文章主要给大家介绍了关于three.js显示中文字体与tween应用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习...

    郭志强9892021-12-24
  • js教程详解javascript脚本何时会被执行

    详解javascript脚本何时会被执行

    这篇文章主要介绍了详解javascript脚本何时会被执行,帮助大家更好的理解和使用JavaScript,感兴趣的朋友可以了解下...

    皮卡丘和羊宝贝6682022-01-17
  • js教程js动态生成表格(节点操作)

    js动态生成表格(节点操作)

    这篇文章主要为大家详细介绍了js动态生成表格,进行节点操作,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    刘刘刘code3582021-12-30
  • js教程在JavaScript中查找字符串中最长单词的三种方法(推荐)

    在JavaScript中查找字符串中最长单词的三种方法(推荐)

    这篇文章主要介绍了在JavaScript中查找字符串中最长单词的三种方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋...

    Hunter网络安全7332022-01-04
  • js教程JS+CSS实现过渡特效

    JS+CSS实现过渡特效

    这篇文章主要为大家详细介绍了JS+CSS实现过渡特效,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    Dr_空山3602021-12-23