4、JavaScript钩子:@before-enter/@enter/@after-enter钩子函数、使用Velocity.js或GSAP库、动态控制动画流程、取消动画(done回调)
前面我们聊了CSS过渡和动画,说实话,大部分场景下它们够用了。但如果你做过复杂交互动画,一定遇到过这种情况——CSS动画根本控制不住细节。比如我想让一个元素先弹跳再旋转,中间还要根据用户输入动态调整速度,CSS就有点力不从心了。
这时候,JavaScript钩子就该登场了。
4.1 认识三个核心钩子:@before-enter、@enter、@after-enter
Vue的<Transition>组件提供了几个JavaScript钩子,让你能完全接管动画流程。说白了,就是Vue把动画的控制权交到你手里,你想怎么动就怎么动。
先看一个最基础的例子:
<template>
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
>
<div v-if="show" class="box"></div>
</Transition>
</template>
<script setup>
const onBeforeEnter = (el) => {
// 元素插入前调用
el.style.opacity = '0'
el.style.transform = 'translateX(-100px)'
}
const onEnter = (el, done) => {
// 元素插入后调用,开始动画
el.offsetHeight // 强制回流
el.style.transition = 'all 0.5s ease'
el.style.opacity = '1'
el.style.transform = 'translateX(0)'
// 监听过渡结束
el.addEventListener('transitionend', done)
}
const onAfterEnter = (el) => {
// 动画完成后调用
console.log('入场动画完成')
}
</script>
这三个钩子的执行顺序是这样的:
| 钩子名称 | 触发时机 | 参数 | 典型用途 |
|---|---|---|---|
| @before-enter | 元素插入DOM之前 | (el) | 设置初始状态 |
| @enter | 元素插入DOM之后 | (el, done) | 执行入场动画 |
| @after-enter | 入场动画完成后 | (el) | 清理工作或后续操作 |
@enter中别忘了调用done()。如果不调用,Vue会一直等待,@after-enter永远不会触发。我之前就踩过这个坑,调试了半天才发现是done没调。
4.2 使用Velocity.js:轻量级动画库
我个人习惯在中小型项目中使用Velocity.js。它体积小(压缩后不到10KB),API简洁,而且性能不错。为什么不直接用jQuery的animate?嗯,Velocity.js是用requestAnimationFrame实现的,比jQuery的setInterval方案流畅得多。
安装方式:
npm install velocity-animate
配合Vue的钩子使用:
<template>
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
>
<div v-if="show" class="card">内容卡片</div>
</Transition>
</template>
<script setup>
import Velocity from 'velocity-animate'
const onBeforeEnter = (el) => {
el.style.opacity = '0'
el.style.transform = 'scale(0.5)'
}
const onEnter = (el, done) => {
Velocity(el,
{ opacity: 1, scale: 1 },
{
duration: 600,
easing: 'easeOutBack',
complete: done // 动画完成后调用done
}
)
}
const onLeave = (el, done) => {
Velocity(el,
{ opacity: 0, scale: 0.5 },
{
duration: 400,
easing: 'easeInBack',
complete: done
}
)
}
</script>
你看,代码比纯CSS简洁多了。而且easeOutBack这种弹性效果,用CSS的cubic-bezier很难调出来。
complete回调一定要绑定done函数。这是Vue判断动画是否结束的唯一依据。我曾经在项目中看到有人忘了传done,结果动画卡住了,页面直接假死。
4.3 使用GSAP:专业级动画引擎
如果项目对动画要求很高,比如需要时间线控制、复杂序列动画,我会毫不犹豫选择GSAP。它几乎是业界标准,很多大厂的项目都在用。
安装:
npm install gsap
GSAP配合Vue钩子的写法:
<template>
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
>
<div v-if="show" class="profile-card">
<img src="avatar.jpg" class="avatar" />
<h3>用户名</h3>
<p>个人简介</p>
</div>
</Transition>
</template>
<script setup>
import { gsap } from 'gsap'
const onBeforeEnter = (el) => {
// 设置所有子元素的初始状态
gsap.set(el, { opacity: 0, y: 50 })
gsap.set(el.querySelector('.avatar'), { scale: 0, rotation: -180 })
gsap.set(el.querySelector('h3'), { x: -30, opacity: 0 })
gsap.set(el.querySelector('p'), { x: 30, opacity: 0 })
}
const onEnter = (el, done) => {
// 创建时间线,实现序列动画
const tl = gsap.timeline({
onComplete: done // 整个时间线完成后调用done
})
tl.to(el, { opacity: 1, y: 0, duration: 0.4 })
.to(el.querySelector('.avatar'), {
scale: 1, rotation: 0, duration: 0.5, ease: 'back.out(1.7)'
}, '-=0.2')
.to(el.querySelector('h3'), { x: 0, opacity: 1, duration: 0.3 }, '-=0.3')
.to(el.querySelector('p'), { x: 0, opacity: 1, duration: 0.3 }, '-=0.2')
}
const onAfterEnter = (el) => {
console.log('所有子元素动画完成')
// 可以在这里触发一些后续逻辑,比如数据加载
}
</script>
GSAP的timeline太强了。你可以精确控制每个子元素的出场顺序、重叠时间。上面代码中的'-=0.2'表示前一个动画结束前0.2秒就开始,实现交叠效果。
onComplete和Vue的done是两回事。GSAP的onComplete是GSAP自己的回调,而done是Vue用来通知动画结束的。一定要在GSAP的onComplete里调用done,否则Vue不知道动画啥时候结束。
4.4 动态控制动画流程
你想想看,如果动画参数是固定的,那用CSS就够了。JavaScript钩子的真正价值在于——动态控制。
比如根据用户点击的位置,决定元素从哪个方向飞入:
<template>
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
>
<div v-if="show" class="fly-in-box">
我飞进来了!
</div>
</Transition>
</template>
<script setup>
import { ref } from 'vue'
import { gsap } from 'gsap'
// 动态方向,由用户点击位置决定
const enterDirection = ref('left')
const onBeforeEnter = (el) => {
// 根据方向设置不同的初始位置
const offsetMap = {
left: { x: -200, y: 0 },
right: { x: 200, y: 0 },
top: { x: 0, y: -200 },
bottom: { x: 0, y: 200 }
}
const offset = offsetMap[enterDirection.value]
gsap.set(el, {
opacity: 0,
x: offset.x,
y: offset.y,
scale: 0.5
})
}
const onEnter = (el, done) => {
gsap.to(el, {
opacity: 1,
x: 0,
y: 0,
scale: 1,
duration: 0.6,
ease: 'power2.out',
onComplete: done
})
}
// 模拟用户点击触发
const flyFromLeft = () => {
enterDirection.value = 'left'
show.value = true
}
const flyFromRight = () => {
enterDirection.value = 'right'
show.value = true
}
</script>
这种动态控制,CSS动画根本做不到。你想想看,CSS的@keyframes是写死的,没法根据运行时状态改变初始位置。
4.5 取消动画:done回调的正确用法
这里有个容易忽略的点——done回调其实可以取消动画。Vue官方文档里提到,如果在@enter或@leave中调用了done(false),Vue会认为动画被取消了。
什么场景下需要取消?比如用户快速切换显示状态,或者组件被销毁了:
<template>
<Transition
@enter="onEnter"
@leave="onLeave"
>
<div v-if="show" class="modal">
模态框内容
</div>
</Transition>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
import Velocity from 'velocity-animate'
const isCancelled = ref(false)
const onEnter = (el, done) => {
// 如果组件已经被销毁,取消动画
if (isCancelled.value) {
done(false) // 传入false表示取消
return
}
Velocity(el,
{ opacity: 1, translateY: '0px' },
{
duration: 500,
complete: () => {
if (!isCancelled.value) {
done()
} else {
done(false)
}
}
}
)
}
const onLeave = (el, done) => {
// 离开动画同理
if (isCancelled.value) {
done(false)
return
}
Velocity(el,
{ opacity: 0, translateY: '50px' },
{
duration: 300,
complete: () => {
if (!isCancelled.value) {
done()
} else {
done(false)
}
}
}
)
}
// 组件卸载时标记取消
onUnmounted(() => {
isCancelled.value = true
})
</script>
@enter和@leave中检查组件是否还存活,如果已经卸载或状态变了,就调用done(false)取消。
4.6 实战建议
说了这么多,最后给点实际建议:
- 简单动画用CSS:比如淡入淡出、滑动,CSS过渡完全够用,别折腾JS
- 中等复杂度用Velocity.js:需要弹性效果、简单序列动画时,Velocity.js很轻便
- 复杂动画用GSAP:时间线控制、交叠动画、子元素序列动画,GSAP是首选
- 别忘了done:无论用哪个库,一定要在动画完成时调用
done(),否则Vue会一直等待 - 处理取消场景:用户快速操作、组件卸载时,记得取消动画,避免内存泄漏和性能问题
嗯,JavaScript钩子的内容就这些。说白了,它给了你完全的控制权,但也意味着你要对动画的生命周期负责。用好了,你的Vue应用可以做出非常流畅、自然的交互动画。