1. 体验动画

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background-color: pink;
            position: absolute;
        }
    </style>
</head>
<body>
<button>动画</button>
<div class="box" style="left: 0px"></div>


<script>

    var btn = document.getElementsByTagName("button")[0];
    var div = document.getElementsByTagName("div")[0];
    
    //匀速运动
    btn.onclick = function () {
        //定时器,每隔一定的时间向右走一些
        setInterval(function () {
            console.log(parseInt(div.style.left));
//            div.style.left = parseInt(div.style.left)+10+"px";  //NaN不能用
            //动画原理: 盒子未来的位置 = 盒子现在的位置 + 步长;
            //赋值给style.left,用offsetLeft获取值。
            //style.left获取值不方便,获取行内式,如果没有事“”;容易出现NaN;
            //offsetLeft获取值特别方便,而且是现成number方便计算。因为他是只读的不能赋值。
            div.style.left = div.offsetLeft + 10 + "px";
        },300);
    }
    
</script>
</body>
</html>

通过setInterval每隔一定的时间向右走一些,实现了动画的效果,但是一旦动画开始不能停下来。

2. 动画封装

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        .box1 {
            margin: 0;
            padding: 5px;
            height: 200px;
            background-color: #ddd;
            position: relative;
        }
        button {
            margin: 5px;
        }
        .box2 {
            width: 100px;
            height: 100px;
            background-color: red;
            position: absolute;
            left: 0;
        }
    </style>
</head>
<body>
<div class="box1">
    <button>运动到200</button>
    <button>运动到400</button>
    <div class="box2"></div>
</div>


<script>
    var btnArr = document.getElementsByTagName("button");
    var box2 = document.getElementsByClassName("box2")[0];
    var timer = null;

    //绑定事件
    btnArr[0].onclick = function () {
        animate(200);
    };

    btnArr[1].onclick = function () {
        animate(400);
    };


    function animate(target){
        timer = setInterval(function () {
            //盒子自身的位置+步长
            box2.style.left = box2.offsetLeft + 10 + "px";
            //如果停止盒子?清除定时器
            if(box2.offsetLeft === target){
               clearInterval(timer);
            }
        },30);
    }
</script>
</body>
</html>

上面的改进的例子实现了盒子“走”到固定的位置就停下来,但是我们发现,他运动的方向是固定的,而且当第二次触发动画的时候,不会停下来。

3. 去除bug版

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        .box1 {
            margin: 0;
            padding: 5px;
            height: 200px;
            background-color: #ddd;
            position: relative;
        }
        button {
            margin: 5px;
        }
        .box2 {
            width: 100px;
            height: 100px;
            background-color: red;
            position: absolute;
            left: 0;
        }
    </style>
</head>
<body>
<div class="box1">
    <button>运动到200</button>
    <button>运动到400</button>
    <div class="box2"></div>
</div>


<script>

    var btnArr = document.getElementsByTagName("button");
    var box2 = document.getElementsByClassName("box2")[0];
    var timer = null;

    //绑定事件
    btnArr[0].onclick = function () {
        animate(200);
    }
    btnArr[1].onclick = function () {
        animate(400);
    }

    function animate(target){
        //BUG1:点击多次以后,越来越快:每次只能开一个定时器。(执行定时器前面,先清楚定时器)
        //要用定时器,先清定时器。
        clearInterval(timer);
        //BUG2:无法返回。 原因就是步长不能为恒定值。
        // 传递的目标值如果比当前值大,那么步长为+10;
        // 传递的目标值如果比当前值小,那么步长为-10;
        var speed = target>box2.offsetLeft ? 10:-10;
        timer = setInterval(function () {
            //BUG3:二次点击不停止问题。
            //如果当前值===目标值,那么先判断之间的距离还有多少,如果小于步长,那么就别走了,马上清除定时器
            var val = target - box2.offsetLeft;
            //盒子自身的位置+步长
            box2.style.left = box2.offsetLeft + speed + "px";
            //如何停止盒子?清除定时器
            if(Math.abs(val)<Math.abs(speed)){
                box2.style.left = target+ "px";
                clearInterval(timer);
            }
        },30);
    }
</script>
</body>
</html>

4. 最终版

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        .box1 {
            margin: 0;
            padding: 5px;
            height: 300px;
            background-color: #ddd;
            position: relative;
        }
        button {
            margin: 5px;
        }
        .box2 {
            width: 100px;
            height: 100px;
            background-color: red;
            position: absolute;
            left: 0;
            top: 40px;
        }
        .box3 {
            width: 100px;
            height: 100px;
            background-color: yellow;
            position: absolute;
            left: 0;
            top: 150px;
        }
    </style>
</head>
<body>
<div class="box1">
    <button>运动到200</button>
    <button>运动到400</button>
    <div class="box2"></div>
    <div class="box3"></div>
</div>


<script>

    var btnArr = document.getElementsByTagName("button");
    var box2 = document.getElementsByClassName("box2")[0];
    var box3 = document.getElementsByClassName("box3")[0];

    //绑定事件
    btnArr[0].onclick = function () {
        //如果有一天我们要传递另外一个盒子,那么我们的方法就不好用了
        //所以我们要增加第二个参数,被移动的盒子本身。
        animate(box2,200);
        animate(box3,200);
    }

    btnArr[1].onclick = function () {
        animate(box2,400);
        animate(box3,400);
    }


    function animate(ele,target){
        //要用定时器,先清除定时器
        //一个盒子只能有一个定时器,这样的话,不会和其他盒子出现定时器冲突
        //而定时器本身讲成为盒子的一个属性
        clearInterval(ele.timer);
        //我们要求盒子既能向前又能向后,那么我们的步长就得有正有负
        //目标值如果大于当前值取正,目标值如果小于当前值取负
        var speed = target>ele.offsetLeft?10:-10;
        ele.timer = setInterval(function () {
            //在执行之前就获取当前值和目标值之差
            var val = target - ele.offsetLeft;
            ele.style.left = ele.offsetLeft + speed + "px";
            //目标值和当前值只差如果小于步长,那么就不能再前进了
            //因为步长有正有负,所有转换成绝对值来比较
            if(Math.abs(val)<Math.abs(speed)){
                ele.style.left = target + "px";
                clearInterval(ele.timer);
            }
        },30)
    }
</script>
</body>
</html>

5. window.requestAnimationFrame

编写动画的关键是循环间隔的设置,一方面,循环间隔足够短,动画效果才能显得平滑流畅;另一方面,循环间隔还要足够长,才能确保浏览器有能力渲染产生的变化。

大部分的电脑显示器的刷新频率是60HZ,也就是每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会提升。因此,最平滑动画的最佳循环间隔是 1000ms / 60 ,约为16.7ms。

setTimeout/setInterval 有一个显著的缺陷在于时间是不精确的,setTimeout/setInterval 只能保证延时或间隔不小于设定的时间。因为它们实际上只是把任务添加到了任务队列中,但是如果前面的任务还没有执行完成,它们必须要等待。

requestAnimationFrame 采用的是系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

综上所述,requestAnimationFramesetTimeout/setInterval 在编写动画时相比,优点如下:

  • requestAnimationFrame 不需要设置时间,采用系统时间间隔,能达到最佳的动画效果。
  • requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成。
  • requestAnimationFrame() 运行在后台标签页或者隐藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命(大多数浏览器中)。

参考MDN,下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数是一个double类型,用于存储时间值,表示requestAnimationFrame() 开始去执行回调函数的时刻。

通过一个例子体会一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>practise</title>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  <style>
    #SomeElementYouWantToAnimate {
      width: 100px;
      height: 100px;
      background-color: lightcoral;
    }
  </style>
</head>

<body>
<div id="SomeElementYouWantToAnimate"></div>
<script>
  var start = null;
  var element = document.getElementById('SomeElementYouWantToAnimate');
  element.style.position = 'absolute';

  function step(timestamp) {
    if (!start) start = timestamp;
    var progress = timestamp - start;
    element.style.left = Math.min(progress / 10, 300) + 'px';
    if (progress < 3000) {
      window.requestAnimationFrame(step);
    }
  }

  window.requestAnimationFrame(step);
</script>
</body>
</html>