本文通过一个具体的动画需求,来讲解一下js中animationend事件使用
- 在线演示效果一:ashuai.site/reactExampl...
- 在线演示效果二:ashuai.work:8890/27
前言
- 平常,我们代码中,会做一些事件的监听
- 比如点击事件、右键事件、滚动事件等
- 实际上,js中还提供了动画事件的相关api
- 以便于我们能够应对产品奇怪的需求
需求描述
- 假设我们有一个输入框
- 当用户输入错误的数据的时候
- 进行校验不通过的动画播放提示(让输入框左右抖动三次)
- 在动画播放期间,不允许用户再次点击提交按钮
- 必须要动画播放完毕后,才允许用户点击按钮
- 且在动画播放完毕以后,清除掉用户输入的错误的数据
- 如下效果图:
效果图

- 所以,我们需要通过动画事件进行相关的逻辑控制
动画事件知识点
如下:三个动画相关事件------用于链式动画控制、状态更新操作等交互逻辑
animationstart
:动画开始时触发animationend
:动画完成时触发animationiteration
:动画每完成一次循环时触发
js
const element = document.getElementById('animatedElement');
element.addEventListener('animationstart', () => {
console.log('动画开始了');
});
element.addEventListener('animationend', () => {
console.log('动画结束了');
});
element.addEventListener('animationiteration', () => {
console.log('动画完成一次迭代');
});
注意: 动画事件可以获取事件对象,包含动画名称、属性等信息
此外,还有动画过度事件,比如transitionend
过渡效果完成时触发,这里不赘述了
js
const element = document.getElementById('transitionElement');
element.addEventListener('transitionend', (event) => {
console.log(`过渡完成: ${event.propertyName}`);
});
代码逻辑
在页面加载完成后的useEffect中,去绑定对应动画事件
- 在页面加载完成后的useEffect中,去绑定对应动画事件
- 当动画开始的时候,去禁用按钮
setIsDisabledBtn(true);
- 当动画结束的时候,
setVal('') // 清空输入框
,setErr('') // 清空错误提示
,setIsDisabledBtn(false); // 取消按钮禁用
js
useEffect(() => {
const handleAnimationStart = () => {
console.log('动画开始');
setIsDisabledBtn(true);
}
const handleAnimationIteration = () => {
console.log('动画迭代');
}
const handleAnimationEnd = () => {
console.log('动画结束');
setVal('') // 清空输入框
setErr('') // 清空错误提示
setIsDisabledBtn(false); // 取消按钮禁用
}
inputContainerRef.current?.addEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.addEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.addEventListener('animationend', handleAnimationEnd)
return () => {
inputContainerRef.current?.removeEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.removeEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.removeEventListener('animationend', handleAnimationEnd)
}
}, [])
html结构
使用classNames库,去更好的控制类名的动态切换
html
<div>
<h2>动画事件的应用场景举例</h2>
<h3>请输入文字,至少五个字符</h3>
<div className={classNames('inputContainer', { 'errorInput': err })} ref={inputContainerRef}>
<Input status={err} value={val} ref={inputRef} onChange={onChange} placeholder="请输入" style={{ width: 240 }} />
{err && <div className='errorText'>请输入至少五个字符</div>}
</div>
<br />
<Button onClick={clickBtn} style={{ marginTop: '12px' }} type="primary" disabled={isDisabledBtn}>提交</Button>
</div>
css动画控制
通过transform左右控制,再搭配cubic-bezier曲线,让动画更加有节奏
css
.inputContainer {
padding: 0 12px;
}
.errorText {
color: red;
font-size: 12px;
margin-top: 4px;
}
.errorInput {
/* 动画执行时间为1.5s,执行3次,动画函数为cubic-bezier(0.36, 0.07, 0.19, 0.97) */
animation: shakeLeftRight 1.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) 3;
}
@keyframes shakeLeftRight {
0% {
transform: translateX(0);
}
10% {
transform: translateX(-20px);
}
20% {
transform: translateX(20px);
}
30% {
transform: translateX(-15px);
}
40% {
transform: translateX(15px);
}
50% {
transform: translateX(-10px);
}
60% {
transform: translateX(10px);
}
70% {
transform: translateX(-4px);
}
80% {
transform: translateX(4px);
}
90% {
transform: translateX(-2px);
}
100% {
transform: translateX(0);
}
}
完整React代码
js
import React, { useRef, useEffect, useState } from 'react'
import { Input, Button, InputRef } from 'antd';
import './AnimationEvent.css'
import classNames from 'classnames'
const AnimationEvent: React.FC = () => {
const [val, setVal] = useState<string>('初始值');
const [err, setErr] = useState<'' | 'warning' | 'error' | undefined>('');
const [isDisabledBtn, setIsDisabledBtn] = useState<boolean>(false);
const inputRef = useRef<InputRef>(null);
const inputContainerRef = useRef<HTMLDivElement>(null);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
setVal(target.value);
}
useEffect(() => {
const handleAnimationStart = () => {
console.log('动画开始');
setIsDisabledBtn(true);
}
const handleAnimationIteration = () => {
console.log('动画迭代');
}
const handleAnimationEnd = () => {
console.log('动画结束');
setVal('') // 清空输入框
setErr('') // 清空错误提示
setIsDisabledBtn(false); // 取消按钮禁用
}
inputContainerRef.current?.addEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.addEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.addEventListener('animationend', handleAnimationEnd)
return () => {
inputContainerRef.current?.removeEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.removeEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.removeEventListener('animationend', handleAnimationEnd)
}
}, [])
const clickBtn = () => {
if (val.trim().length < 5) {
setErr('error')
return
}
alert('提交成功')
}
return (
<div>
<h2>动画事件的应用场景举例</h2>
<h3>请输入文字,至少五个字符</h3>
<div className={classNames('inputContainer', { 'errorInput': err })} ref={inputContainerRef}>
<Input status={err} value={val} ref={inputRef} onChange={onChange} placeholder="请输入" style={{ width: 240 }} />
{err && <div className='errorText'>请输入至少五个字符</div>}
</div>
<br />
<Button onClick={clickBtn} style={{ marginTop: '12px' }} type="primary" disabled={isDisabledBtn}>提交</Button>
</div>
)
}
export default AnimationEvent
动画事件的第三个参数
- 实际上,动画事件还有第三个参数
- 方便我们进行配置,从而实现相应的效果
- 上述案例中,我们得先绑定动画事件,最后再去解绑动画事件
- 无形中,增加了代码量
- 动画事件的第三个参数,方便我们直接使用
一次性
的动画事件 - 如下示例
js
element.addEventListener('transitionend', handleTransition, {
once: true, // 只触发一次
capture: false, // 冒泡阶段触发
passive: true // 不调用 preventDefault
});
once: true
最常用,一次性使用,不用再去手动解绑事件,另外两个用的不多
如下效果图,就以once: true,
为案例
效果图

- 这里的需求就是点击一个item,让这个item播放动画
- 同时清除掉其他的item身上的动画
- 搭配
once: true
代码更加简约了
完整Vue代码
html
<template>
<div>
<div
:class="['item', 'item' + index, activeClass[index]]"
@click="clc(item, index)"
v-for="(item, index) in arr"
:key="index"
>
{{ item.name }} - {{ item.age }}
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const arr = ref([
{ name: "Tom", age: 18 },
{ name: "Jerry", age: 20 },
{ name: "Jack", age: 22 },
]);
// 用来存储每个 item 的动画状态
const activeClass = ref([]);
setTimeout(() => {
console.log("activeClass", activeClass.value);
}, 1000);
const clc = (item, index) => {
document.querySelector(".item" + index).addEventListener(
"animationend",
() => {
activeClass.value = activeClass.value.map(() => "");
},
{ once: true }
);
// 清空所有元素的动画状态
activeClass.value = activeClass.value.map(() => "");
// 给点击的元素添加动画状态
activeClass.value[index] = "animate";
};
</script>
<style lang="less" scoped>
.item {
border: 1px solid #f00;
width: 100px;
margin: 36px;
cursor: pointer;
}
/* 动画类,触发动画 */
.animate {
animation: donghua 2s 1;
}
@keyframes donghua {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 #589fd7;
}
100% {
transform: scale(1.2);
box-shadow: 0 0 0 12px rgba(204, 73, 152, 0%);
}
}
</style>
A good memory is better than a bad pen. Record it down...