JS如何监听动画结束

Posted 燎原之火的部落格

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS如何监听动画结束相关的知识,希望对你有一定的参考价值。

场景描述

在使用JS控制动画时一般需要在动画结束后执行回调去进行DOM的相关操作,所以需要监听动画结束进行回调。JS提供了以下事件用于监听动画的结束,简单总结学习下。

CSS3动画监听事件

transitionEnd事件

transitionEnd事件会在CSS transition动画结束后触发。

动画结束后触发监听事件

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        .demo{
            margin:100px;
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
        }
        .demo:hover{
            width: 200px;
        }
    </style>
</head>
<body>
    <div id="demo" class="demo">
        鼠标移入
    </div>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
    </script>
</body>
</html>

事件多次触发问题

当存在多个属性过渡变化时,结束时会多次触发transitionend事件。看个例子:
当过渡结束时,width和background-color都发生变化,会触发两次transionend事件

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        .demo{
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
        }
        .w200{
            width: 200px;
            background-color: #fef;
        }
    </style>
</head>
<body>
    <div id="demo" class="demo" onmouseover="change()" onmouseout="change()">
    </div>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
        function change() {
            element.className = element.className === \'demo\' ? \'demo w200\': \'demo\'
        }
    </script>
</body>
</html>

事件失效问题及解决方案

1、在transiton动画完成前设置display:none,事件不会触发。

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        .demo{
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
        }
        .w200{
            width: 200px;
        }
    </style>
</head>
<body>
    <div id="demo" class="demo" onmouseover="change()" onmouseout="change()">
    </div>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
        function change() {
            element.className = element.className === \'demo\' ? \'demo w200\': \'demo\'
            // 500ms后设置display:none
            setTimeout(function (){
                element.style.display = \'none\'
            },400)
        }
    </script>
</body>
</html>

2、当transition完成前移除transition一些属性时,事件也不会触发,例如:

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        .demo{
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
        }
        .noTranstion{
            width:100px;
            height: 100px;
            background-color: #ddc;
        }
        .w200{
            width: 200px;
        }
    </style>
</head>
<body>
    <div id="demo" class="demo" onmouseover="change()" onmouseout="change()">
    </div>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
        function change() {
            element.className = element.className === \'demo\' ? \'demo w200\': \'demo\'
            setTimeout(function(){
                element.className = \'noTranstion\'
            },400)
        }
    </script>
</body>
</html>

3、元素从display:none到block,不会有过渡,导致无法触发transitionend事件
例如:元素从display:none 到block opacity从0到1,无法触发过渡效果。

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        body{padding: 50px;}
        .demo{
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
            opacity:0;
            display: none;
        }
        .noTranstion{
            width:100px;
            height: 100px;
            background-color: #ddc;
        }
        .opt{
            display: block;
            opacity:1
        }

        .w200{
            width: 200px;
        }
        button{position: absolute;top: 200px;width: 100px;height: 40px;}
    </style>
</head>
<body>
    <div id="demo" class="demo" onmouseover="change()" onmouseout="change()">
    </div>
    <button onclick="change()">Click</button>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
        function change() {
            element.className = element.className === \'demo\' ? \'demo opt\': \'demo\'
        }
    </script>
</body>
</html>

无法触发过渡效果原因:
元素从none到block,刚生成未能即时渲染,导致过渡失效。所以需要主动触发页面重绘,刷新DOM。页面重绘可以通过改变一些CSS属性来触发,例如:offsetTop、offsetLeft、offsetWidth、scrollTop等。
触发过渡效果解决方案:
1、通过定时器延迟渲染

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        body{padding: 50px;}
        .demo{
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
            opacity: 0;
            display: none;
        }
        .opt{
            display: block;
        }
        button{position: absolute;top: 200px;width: 100px;height: 40px;}
    </style>
</head>
<body>
    <div id="demo" class="demo">
    </div>
    <button id="button" onclick="change()">点击</button>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        var button = document.getElementById(\'button\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
        function change() {
            element.className = element.className === \'demo\' ? \'demo opt\': \'demo\'
            if(element.className === \'demo\'){
                        element.style.opacity = null
                    button.innerHTML = \'点击\'
            }else{
                setTimeout(function(){
                element.style.opacity = \'1\'
                button.innerHTML = \'重置\'
            },10)
            }
        }
    </script>
</body>
</html>

2、强制获取当前内联样式
通过window.getComputedStyle()方法返回应用样式后的元的所有CSS属性的值,并解析这些值可能包含的任何基本计算。也就是说返回的属性值是已计算后的值,即DOM元素的样式已经更新了。然后再改变对应属性值触发过渡效果。例如:

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        body{padding: 50px;}
        .demo{
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
            opacity: 0;
            display: none;
        }
        .opt{
            display: block;
        }
        button{position: absolute;top: 200px;width: 100px;height: 40px;}
    </style>
</head>
<body>
    <div id="demo" class="demo">
    </div>
    <button id="button" onclick="change()">点击</button>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        var button = document.getElementById(\'button\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
        function change() {
            element.className = element.className === \'demo\' ? \'demo opt\': \'demo\'
            if(element.className === \'demo\'){
                        element.style.opacity = null
                    button.innerHTML = \'点击\'
            }else{
                // setTimeout(function(){
                //     element.style.opacity = \'1\'
                //     button.innerHTML = \'重置\'
                // },10)
                window.getComputedStyle(element, null).opacity
                element.style.opacity = \'1\'
                button.innerHTML = \'重置\'
            }
        }
    </script>
</body>
</html>

3、触发重绘刷新DOM
通过clientWidth触发重绘,例如:

<!DOCTYPE html>
<html>
<head>
    <title>transtionend demo</title>
    <style type="text/css">
        *{margin:0;padding: 0;}
        body{padding: 50px;}
        .demo{
            width:100px;
            height: 100px;
            background-color: #ddc;
            transition: all 0.5s ease-out;
            opacity: 0;
            display: none;
        }
        .opt{
            display: block;
        }
        button{position: absolute;top: 200px;width: 100px;height: 40px;}
    </style>
</head>
<body>
    <div id="demo" class="demo">
    </div>
    <button id="button" onclick="change()">点击</button>
    <script type="text/javascript">
        var element = document.getElementById(\'demo\')
        var button = document.getElementById(\'button\')
        element.addEventListener(\'transitionend\', handle, false)
        function handle(){
            alert(\'transitionend事件触发\')
        }
        function change() {
            element.className = element.className === \'demo\' ? \'demo opt\': \'demo\'
            if(element.className === \'demo\'){
                        element.style.opacity = null
                    button.innerHTML = \'点击\'
            }else{
                // setTimeout(function(){
                //     element.style.opacity = \'1\'
                //     button.innerHTML = \'重置\'
                // },10)
                // window.getComputedStyle(element, null).opacity
                element.clientWidth;
                element.style.opacity = \'1\'
                button.innerHTML = \'重置\'
            }
        }
    </script>
</body>
</html>

浏览器兼容性

移动端基本支持 android2.1+、webkit3.2+
详见浏览器兼容性

animationEnd事件

与transitonend事件类似,详见

Zepto中animate结束回调实现

查看了下zepto动画模块的源代码,animate()方法在动画结束后触发回调也是通过transitionend、animationend事件来触发。
另外在一些低版本的Android手机可能无法触发transitionend事件,需要手动触发。

$.fx = {
    off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined),
    speeds: { _default: 400, fast: 200, slow: 600 },
    cssPrefix: prefix,
    transitionEnd: normalizeEvent(\'TransitionEnd\'),
    animationEnd: normalizeEvent(\'AnimationEnd\')
  }
// 手动触发事件
if (duration > 0){
      this.bind(endEvent, wrappedCallback)
      // transitionEnd is not always firing on older Android phones
      // so make sure it gets fired
      setTimeout(function(){
        if (fired) return
        wrappedCallback.call(that)
      }, ((duration + delay) * 1000) + 25)
    }

参考链接

zepto动画模块源码
transitionend事件MDN
transtion属性详解MDN
transitionend事件详解
Window.getComputedStyle() 方法

以上是关于JS如何监听动画结束的主要内容,如果未能解决你的问题,请参考以下文章

jquery动画函数里面可以跟一个回调函数,表示动画结束后执行的代码

transitionend事件 监听 fadeIn fadeOut 两个方法无效(动画结束时无法执行transitionend里面的代码)

聊聊CSS动画的监听

Glide加载webp动画及监听动画播放结束

如何将事件监听器添加到动画变换旋转?

VSCode自定义代码片段——CSS动画