一、平滑的动画
大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率。因此,最平滑动画的最佳循环间隔是1000ms/60 = 16.7ms 每帧。以16.7ms这个循环间隔重绘的动画是最平滑的,因为这个最接近浏览器的最高限速。二、setInterval存在的问题
下面一段代码用setInterval实现了每隔半秒计数一次
let count = 0;// 每隔半秒计数一次document.getElementById('count').onclick = () => { setInterval(increment, 500);}const increment = () => { count++; document.getElementById('count').innerText = count;}
然而使用setInterval会有问题,后一个间歇调用可能会在前一个间歇调用结束之前启动,还有可能会出现多个任务同时一次性执行。可以考虑一种特殊的情况:主线程正在执行一个耗时的任务,setInterval新任务A添加到异步任务队列中。又过了500ms,主线程依然非空闲,但是计数器的500ms到了,它会又往任务队列中添加一个任务A。任务队列里面堆积了很多的任务A。当主线程一旦空闲开始执行异步任务,那么这一系列的任务A就会迅速依次执行,本来每个A任务之间应该是有一个500ms的间隔的,但是由于这些A直接堆积不再由定时器控制,所以会一次执行完,中间不再符合预期的500ms间隔。
三、用setTimeout来模拟setInterval
为了解决setInterval这个问题,可以用setTimeout来模拟setInterval来实现相同效果。let count2 = 0;// 每隔半秒计数一次document.getElementById('count2').onclick = () => { increment2();}const increment2 = () => { count2++; document.getElementById('count2').innerText = count2; setTimeout(increment2, 500);}
setTimout()的第二个参数告诉JavaScript再过多长时间把当前任务添加到队列中。如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。
考虑特殊情况:主线程非空闲,setTimeout添加一个新任务A到任务队列中。等待主线程空闲后会执行刚刚添加的任务。当这个任务执行结束时,会再次添加一个新任务A到任务队列中。那么新的定时器中,任务总是在当前定时器任务执行完成后才会开启定新的任务,从而不会出现像setInterval中不断往任务队列中添加任务的情况,因此也就不会出现所有任务一次性执行完中间没有间隔的情况。
四、更精确的动画实现requestAnimationFrame
什么时候绘制下一帧是保证动画平滑的关键。但是setTimeout和setInterval都不精确。传入的第二个参数,实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务执行完成后再执行。 简言之,以毫秒表示的延迟时间,并不代表到时候一定会执行动画代码,而仅代表带时候会把代码添加到任务队列中。如果UI线程繁忙,那么即使把代码加入队列也不会立即执行。 此外,刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同的设备屏幕刷新频率可能会不同,而setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相等。window.requestAnimationFrame()
就很好了,它告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame()
运行在后台标签页或者隐藏的
里时,
requestAnimationFrame()
会被暂停调用以提升性能和电池寿命。
依旧是每隔半秒计数,用requestAnimationFrame()
可以这样实现
// 每隔半秒计数一次let count = 0;document.getElementById('count').onclick = () => { // 首次调用也最好这样写,如果直接increment()调用,timestamp首次打印出的值为NaN window.requestAnimationFrame(increment);}let start = 0;const increment = (timestamp) => { // requestAnimationFrame调用回调函数的时候,会传入一个时间戳 // 通过这个时间戳进行比对来实现自定义延迟 if (timestamp - start > 500) { start = timestamp; count++; document.getElementById('count').innerText = count; } window.requestAnimationFrame(increment);}
五、肉眼可见的卡顿
你也许会有这样的疑问,如果不考虑主线程占用,给setTimeout设置毫秒值为16.7,和直接使用requestAnimationFrame相比,会有差别吗?效果证明还是有肉眼可见的卡顿。 以下代码实现了2个小球滚动的效果 Document * { margin: 0; padding: 0; } .box { height: 100px; width: 100px; background-color: orange; border-radius: 50%; margin: 30px; } setTimeout: requestanimationframe: const box1 = document.querySelector("#box1"); const box2 = document.querySelector("#box2"); let maxDis = 1400; let step = 0; function box1Animate() { if (step >= maxDis) return; step += 5; box1.style.transform = `translateX(${step}px)`; setTimeout(box1Animate, 16.7); } box1Animate(); let start = 0; let step2 = 0; function box2Animate(timestamp) { step2 += 5; box2.style.transform = `translateX(${step2}px)`; if (step2 < maxDis) { window.requestAnimationFrame(box2Animate); } } window.requestAnimationFrame(box2Animate);
六、新手快速上路
讲了那么多,相信你对requestAnimationFrame有了一定的理解。废话不多说,写一个小demo,带你快速上路。 先来看一下要实现的效果,标签滚动旋转,圆环放大缩小 html, body { margin: 0; padding: 0; border: 0; width: 100%; height: 100%; } #container { width: 100%; height: 100%; background-color: #000; } .item { position: absolute; width: 200px; height: 60px; border: 1px solid #00f8ff; border-radius: 10px; box-shadow: 1px 1px 36px rgb(8 255 236 / 50%); color: #575757; line-height: 60px; transition: transform 0.5s; background: #0cb6f8; text-align: center; }
JS部分:
var arr = [];function createElement(num) { var arr = []; for(var i = 0; i < num; i++) { var div = document.createElement("div"); div.innerHTML = `ITEM${i}`; div.className = "item"; arr.push(div); } return arr;}function setPos(arr, num, radius, dRad) { var w = document.querySelector("body").clientWidth; var h = document.querySelector("body").clientHeight; var rad = 2 * Math.PI / num; for(var i = 0; i var r = i * rad; var div = arr[i]; var x = radius * Math.cos(r + dRad) + w / 2; var y = radius * Math.sin(r + dRad) + h / 2; div.style.transform = `translate(${x}px, ${y}px)`; }}function init() { arr = createElement(20); var container = document.querySelector("#container"); for(var i = 0; i < arr.length; i++) { var div = arr[i]; container.appendChild(div); } window.requestAnimationFrame(animate);}var dRad = 0;var radius = 400;function animate(timestamp) { dRad+= 0.01; radius++; if(radius > 400) { radius = 100; } setPos(arr, 20, radius, dRad); window.requestAnimationFrame(animate);}init();