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很难调出来。

核心要点:Velocity.js的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秒就开始,实现交叠效果。

注意:GSAP的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>
避坑指南:我曾经在项目中遇到一个bug——用户快速点击开关按钮,结果动画队列堆积,页面卡得不行。后来排查发现,就是因为没有正确处理动画取消。每次切换时,前一个动画还在跑,新的动画又开始了,两个动画互相干扰。解决方案就是在@enter@leave中检查组件是否还存活,如果已经卸载或状态变了,就调用done(false)取消。

4.6 实战建议

说了这么多,最后给点实际建议:

  • 简单动画用CSS:比如淡入淡出、滑动,CSS过渡完全够用,别折腾JS
  • 中等复杂度用Velocity.js:需要弹性效果、简单序列动画时,Velocity.js很轻便
  • 复杂动画用GSAP:时间线控制、交叠动画、子元素序列动画,GSAP是首选
  • 别忘了done:无论用哪个库,一定要在动画完成时调用done(),否则Vue会一直等待
  • 处理取消场景:用户快速操作、组件卸载时,记得取消动画,避免内存泄漏和性能问题

嗯,JavaScript钩子的内容就这些。说白了,它给了你完全的控制权,但也意味着你要对动画的生命周期负责。用好了,你的Vue应用可以做出非常流畅、自然的交互动画。