Vue 3 JavaScript动画钩子
1. JavaScript动画钩子概述
1.1 什么是JavaScript动画钩子
JavaScript动画钩子是Vue过渡系统提供的一组事件监听器,允许我们在过渡的各个阶段执行自定义JavaScript代码。这些钩子函数为我们提供了更多的控制权,可以实现复杂的动画效果,甚至可以与第三方动画库集成。
1.2 JavaScript动画钩子的应用场景
- 实现复杂的自定义动画
- 与第三方动画库(如GSAP、Velocity.js)集成
- 执行动画前后的DOM操作
- 实现基于JavaScript的状态驱动动画
- 处理动画的开始、进行中和结束事件
2. JavaScript动画钩子列表
Vue提供了以下JavaScript动画钩子:
| 钩子函数 | 描述 | 调用时机 | 参数 |
|---|---|---|---|
@before-enter |
进入过渡开始前 | 元素被插入DOM之前 | el(元素DOM节点) |
@enter |
进入过渡过程中 | 元素被插入DOM之后 | el, done(结束回调) |
@after-enter |
进入过渡结束后 | 进入过渡完成后 | el |
@enter-cancelled |
进入过渡取消 | 进入过渡被取消时(仅在@enter钩子中调用done之前有效) |
el |
@before-leave |
离开过渡开始前 | 离开过渡开始前 | el |
@leave |
离开过渡过程中 | 离开过渡开始后 | el, done(结束回调) |
@after-leave |
离开过渡结束后 | 离开过渡完成后 | el |
@leave-cancelled |
离开过渡取消 | 离开过渡被取消时(仅在@leave钩子中调用done之前有效) |
el |
3. 基本使用示例
3.1 简单的JavaScript动画
<template>
<div>
<button @click="show = !show">Toggle</button>
<transition
name="custom"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false"
>
<div v-if="show" class="box">
JavaScript Animation
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: true
}
},
methods: {
// 进入过渡钩子
beforeEnter(el) {
console.log('Before enter');
el.style.opacity = 0;
el.style.transform = 'translateX(-100px)';
},
enter(el, done) {
console.log('Enter');
// 手动实现动画
let opacity = 0;
let x = -100;
const duration = 500; // 动画持续时间(毫秒)
const startTime = performance.now();
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 使用easeOutCubic缓动函数
const easeProgress = 1 - Math.pow(1 - progress, 3);
opacity = easeProgress;
x = -100 + (100 * easeProgress);
el.style.opacity = opacity;
el.style.transform = `translateX(${x}px)`;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
done(); // 必须调用done()通知Vue动画结束
}
};
requestAnimationFrame(animate);
},
afterEnter(el) {
console.log('After enter');
// 可以在这里执行动画完成后的操作
},
enterCancelled(el) {
console.log('Enter cancelled');
},
// 离开过渡钩子
beforeLeave(el) {
console.log('Before leave');
el.style.opacity = 1;
el.style.transform = 'translateX(0)';
},
leave(el, done) {
console.log('Leave');
let opacity = 1;
let x = 0;
const duration = 500;
const startTime = performance.now();
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3);
opacity = 1 - easeProgress;
x = 0 - (100 * easeProgress);
el.style.opacity = opacity;
el.style.transform = `translateX(${x}px)`;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
done();
}
};
requestAnimationFrame(animate);
},
afterLeave(el) {
console.log('After leave');
},
leaveCancelled(el) {
console.log('Leave cancelled');
}
}
}
</script>
<style>
.box {
width: 200px;
height: 200px;
background-color: #42b983;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
</style>3.2 与GSAP动画库集成
GSAP(GreenSock Animation Platform)是一个功能强大的JavaScript动画库,与Vue的动画钩子配合使用可以实现复杂的动画效果。
npm install gsap使用示例:
<template>
<div>
<button @click="show = !show">Toggle with GSAP</button>
<transition
name="gsap"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
:css="false"
>
<div v-if="show" class="box">
GSAP Animation
</div>
</transition>
</div>
</template>
<script>
import { gsap } from 'gsap'
export default {
data() {
return {
show: true
}
},
methods: {
beforeEnter(el) {
// 设置初始状态
gsap.set(el, {
opacity: 0,
scale: 0.5,
rotation: -180
})
},
enter(el, done) {
// GSAP动画
gsap.to(el, {
opacity: 1,
scale: 1,
rotation: 0,
duration: 0.6,
ease: 'back.out(1.7)',
onComplete: done // 动画完成时调用done()
})
},
leave(el, done) {
// GSAP离开动画
gsap.to(el, {
opacity: 0,
scale: 0.5,
rotation: 180,
duration: 0.4,
ease: 'power2.in',
onComplete: done
})
}
}
}
</script>
<style>
.box {
width: 200px;
height: 200px;
background-color: #3498db;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
</style>4. 高级用法
4.1 结合CSS过渡和JavaScript钩子
可以同时使用CSS过渡和JavaScript钩子,实现更复杂的动画效果:
<template>
<div>
<button @click="show = !show">Toggle Combined Animation</button>
<transition
name="combined"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
>
<div v-if="show" class="box">
Combined Animation
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: true
}
},
methods: {
beforeEnter(el) {
console.log('Before enter - JS hook');
// 可以在这里执行一些DOM操作
el.dataset.animated = 'true';
},
enter(el, done) {
console.log('Enter - JS hook');
// CSS过渡会自动执行,这里可以添加额外的JavaScript逻辑
// 监听CSS过渡结束事件
const transitionEndHandler = () => {
el.removeEventListener('transitionend', transitionEndHandler);
done();
};
el.addEventListener('transitionend', transitionEndHandler);
},
afterEnter(el) {
console.log('After enter - JS hook');
// 动画完成后的操作
el.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.2)';
}
}
}
</script>
<style>
.box {
width: 200px;
height: 200px;
background-color: #e74c3c;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.4s ease;
}
/* CSS过渡样式 */
.combined-enter-from {
opacity: 0;
transform: translateY(-50px);
}
.combined-enter-active {
transition: all 0.4s ease;
}
.combined-enter-to {
opacity: 1;
transform: translateY(0);
}
.combined-leave-from {
opacity: 1;
transform: translateY(0);
}
.combined-leave-active {
transition: all 0.4s ease;
}
.combined-leave-to {
opacity: 0;
transform: translateY(50px);
}
</style>4.2 动态调整动画参数
可以根据组件状态动态调整动画参数:
<template>
<div>
<button @click="toggle">Toggle Animation</button>
<button @click="changeDuration">Change Duration</button>
<transition
:css="false"
@enter="enter"
@leave="leave"
>
<div v-if="show" class="box">
Dynamic Animation
</div>
</transition>
<p>Current duration: {{ duration }}ms</p>
</div>
</template>
<script>
export default {
data() {
return {
show: true,
duration: 500
}
},
methods: {
toggle() {
this.show = !this.show
},
changeDuration() {
this.duration = this.duration === 500 ? 1000 : 500
},
enter(el, done) {
gsap.fromTo(el,
{
opacity: 0,
x: -100
},
{
opacity: 1,
x: 0,
duration: this.duration / 1000,
ease: 'power2.out',
onComplete: done
}
)
},
leave(el, done) {
gsap.to(el, {
opacity: 0,
x: 100,
duration: this.duration / 1000,
ease: 'power2.in',
onComplete: done
})
}
}
}
</script>5. 性能优化
5.1 使用will-change属性
will-change属性可以提示浏览器哪些属性可能会发生变化,以便浏览器提前优化:
<style>
.box {
/* 提示浏览器这些属性可能会变化 */
will-change: opacity, transform;
}
</style>5.2 避免在动画过程中修改布局
尽量避免在动画过程中修改会导致重排的属性(如width、height、top、left等),优先使用transform和opacity属性,这些属性的变化不会触发重排。
5.3 使用requestAnimationFrame
在JavaScript动画中,使用requestAnimationFrame而不是setTimeout或setInterval,因为requestAnimationFrame会与浏览器的刷新频率同步,提供更流畅的动画效果。
5.4 及时清理事件监听器
在动画结束后,及时清理添加的事件监听器,避免内存泄漏:
enter(el, done) {
const transitionEndHandler = () => {
el.removeEventListener('transitionend', transitionEndHandler);
done();
};
el.addEventListener('transitionend', transitionEndHandler);
}6. 实际应用案例
6.1 滚动触发动画
实现元素进入视口时的滚动触发动画:
<template>
<div class="scroll-container">
<h1>Scroll Down to See Animations</h1>
<div v-for="item in items" :key="item.id" class="scroll-item">
<transition
name="scroll"
:css="false"
@enter="enter"
>
<div v-if="item.visible" class="box">
{{ item.text }}
</div>
</transition>
</div>
</div>
</template>
<script>
import { gsap } from 'gsap'
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1', visible: false },
{ id: 2, text: 'Item 2', visible: false },
{ id: 3, text: 'Item 3', visible: false },
{ id: 4, text: 'Item 4', visible: false },
{ id: 5, text: 'Item 5', visible: false }
]
}
},
mounted() {
// 监听滚动事件
window.addEventListener('scroll', this.handleScroll)
// 初始化检查
this.handleScroll()
},
beforeUnmount() {
// 清理事件监听器
window.removeEventListener('scroll', this.handleScroll)
},
methods: {
handleScroll() {
this.items.forEach(item => {
const element = document.querySelector(`[key="${item.id}"] .box`)
if (element) {
const rect = element.getBoundingClientRect()
// 当元素进入视口时显示
if (rect.top < window.innerHeight * 0.8 && !item.visible) {
item.visible = true
}
}
})
},
enter(el, done) {
gsap.fromTo(el,
{
opacity: 0,
y: 50,
scale: 0.9
},
{
opacity: 1,
y: 0,
scale: 1,
duration: 0.6,
ease: 'power2.out',
onComplete: done
}
)
}
}
}
</script>
<style>
.scroll-container {
min-height: 2000px;
padding: 20px;
}
.scroll-item {
margin: 50px 0;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.box {
width: 200px;
height: 200px;
background-color: #42b983;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
</style>6.2 数字计数器动画
实现数字从0到目标值的计数动画:
<template>
<div>
<button @click="startCount">Start Counting</button>
<transition
name="count"
@enter="enter"
>
<div v-if="show" class="counter">
{{ displayCount }}
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: false,
targetCount: 1000,
displayCount: 0
}
},
methods: {
startCount() {
this.show = false
this.displayCount = 0
// 重新触发过渡
this.$nextTick(() => {
this.show = true
})
},
enter(el, done) {
const startTime = performance.now()
const duration = 2000 // 2秒
const startCount = 0
const endCount = this.targetCount
const count = (currentTime) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// 使用easeOutQuad缓动
const easeProgress = 1 - Math.pow(1 - progress, 2)
this.displayCount = Math.floor(startCount + (endCount - startCount) * easeProgress)
if (progress < 1) {
requestAnimationFrame(count)
} else {
this.displayCount = endCount
done()
}
}
requestAnimationFrame(count)
}
}
}
</script>
<style>
.counter {
font-size: 48px;
font-weight: bold;
color: #3498db;
text-align: center;
margin: 50px 0;
}
/* CSS过渡样式 */
.count-enter-from {
opacity: 0;
transform: scale(0.5);
}
.count-enter-active {
transition: opacity 0.5s ease;
}
.count-enter-to {
opacity: 1;
transform: scale(1);
}
</style>7. 常见问题与解决方案
7.1 动画没有触发
问题:JavaScript动画钩子没有被调用。
解决方案:
- 确保使用了
<transition>组件包裹元素 - 确保元素是通过v-if或v-show来控制显示/隐藏的
- 检查动画钩子的名称是否正确(注意连字符格式)
7.2 动画没有结束
问题:动画开始后,元素一直停留在DOM中,没有被移除。
解决方案:
- 确保在
@enter和@leave钩子中调用了done()函数 - 检查
done()函数是否在正确的时机被调用 - 确保没有在调用
done()后继续修改元素样式
7.3 动画效果不流畅
问题:JavaScript动画效果不流畅,出现卡顿。
解决方案:
- 使用
requestAnimationFrame而不是setTimeout或setInterval - 避免在动画过程中修改会导致重排的属性
- 使用GSAP等优化过的动画库
- 添加
will-change属性提示浏览器优化
7.4 多个动画同时执行
问题:多个元素的动画同时执行,导致性能问题。
解决方案:
- 为动画添加延迟,错开执行时间
- 使用节流或防抖控制动画触发频率
- 考虑使用
transition-group组件处理列表动画
8. 总结
JavaScript动画钩子为Vue的过渡系统提供了强大的扩展能力,允许我们实现各种复杂的动画效果。通过合理使用JavaScript动画钩子,我们可以:
- 实现基于JavaScript的自定义动画
- 与第三方动画库(如GSAP)集成
- 执行动画前后的DOM操作
- 实现状态驱动的动画
- 处理动画的开始、进行中和结束事件
在使用JavaScript动画钩子时,需要注意以下几点:
- 始终在
@enter和@leave钩子中调用done()函数,否则动画将永远不会结束 - 优先使用transform和opacity属性进行动画,避免触发重排
- 使用
requestAnimationFrame实现流畅的动画效果 - 及时清理事件监听器,避免内存泄漏
- 考虑使用
will-change属性优化动画性能
JavaScript动画钩子为我们提供了无限的可能性,可以创建出各种炫酷的动画效果,提升用户体验。但也要注意不要过度使用复杂的JavaScript动画,避免影响应用的性能。
9. 练习
- 使用JavaScript动画钩子实现一个简单的淡入淡出效果
- 集成GSAP库,实现一个复杂的3D旋转动画
- 实现一个滚动触发的动画效果
- 创建一个数字计数器动画,从0计数到指定值
- 实现一个基于鼠标位置的交互式动画
- 结合CSS过渡和JavaScript钩子,实现一个组合动画效果