window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 (即你的回调函数)。
注意: 若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用
window.requestAnimationFrame()
特点
- requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。如果系统绘制率是 60Hz,那么回调函数就会16.7ms再 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是,requestAnimationFrame的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
- 当
requestAnimationFrame()
运行在后台标签页或者隐藏的iframe
里时,requestAnimationFrame()
会被暂停调用以提升性能和电池寿命。
参数
-
callback
下一次重绘之前更新动画帧所调用的函数 (即上面所说的回调函数)。该回调函数会被传入
DOMHighResTimeStamp
参数,该参数与performance.now()
的返回值相同,它表示requestAnimationFrame()
开始去执行回调函数的时刻。
返回值
一个 long
整数,请求 ID,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame()
以取消回调函数。
应用场景
平滑滚动到页面顶部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>平滑滚动到页面顶部</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
}
.main {
width: 200px;
height: 200vh;
margin: 20px 0;
border-radius: 20px;
background-color: #eee;
overflow-y: auto;
}
</style>
</head>
<body>
<div>top</div>
<div class="main"></div>
<button onclick="scrollToTop()">scrollToTop</button>
<script>
const scrollToTop = () => {
const c = document.documentElement.scrollTop || document.body.scrollTop
if (c > 0) {
window.requestAnimationFrame(scrollToTop)
window.scrollTo(0, c - c / 8)
}
}
</script>
</body>
</html>
大量数据渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>大量数据渲染</title>
</head>
<body>
<div id="container"></div>
<script>
//需要插入的容器
let ul = document.getElementById('container')
// 插入十万条数据
let total = 1000
// 一次插入 20 条
let once = 2
//总页数
let page = total / once
//每条记录的索引
let index = 0
//循环加载数据
function loop (curTotal, curIndex) {
if (curTotal <= 0) {
return false
}
//每页多少条
let pageCount = Math.min(curTotal, once)
window.requestAnimationFrame(function () {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
</script>
</body>
</html>
进度条
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.progress-wrap {
position: relative;
width: 300px;
height: 32px;
margin: 100px;
background-color: #eee;
border-radius: 20px;
line-height: 32px;
text-align: center;
}
.progress-bar {
position: absolute;
top: 0;
left: 0;
background-color: aquamarine;
border-radius: 20px;
}
.progress-num {
position: relative;
z-index: 9;
}
</style>
</head>
<body>
<body>
<div class="progress-wrap">
<div class="progress-bar" style="width: 0px;height:32px;"></div>
<span class="progress-num">0%</span>
</div>
<script>
const progressWrap= document.querySelector('.progress-wrap')
const progressBar = document.querySelector('.progress-bar')
const progressNum = document.querySelector('.progress-num')
progressWrap.onclick = function () {
let timer = requestAnimationFrame(function fn () {
if (parseInt(progressBar.style.width) < 300) {
progressBar.style.width = parseInt(progressBar.style.width) + 3 + 'px';
progressNum.innerHTML = parseInt(progressBar.style.width) / 3 + '%'
timer = requestAnimationFrame(fn)
} else {
cancelAnimationFrame(timer)
}
})
}
</script>
</body>
</body>
</html>
定时执行
模拟setTimeout/setInterval
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模拟 setTimeout-setInterval</title>
<style>
.btn {
width: 100px;
height: 40px;
margin-bottom: 20px;
background-color: #eee;
border-radius: 20px;
text-align: center;
line-height: 40px;
}
</style>
</head>
<body>
<div class="btn setTimeout"> setTimeout</div>
<div class="btn setInterval">setInterval</div>
<script>
function tick (options) {
let lastTime = 0
let rafId = 0
let callBackNum = 0
const step = (timestamp) => {
if (timestamp - lastTime > options.interval) {
options.cb()
lastTime = timestamp
callBackNum ++
}
if (options.type !== 'setTimeout' || callBackNum < 1 ) {
window.requestAnimationFrame(step)
}
}
const start = () => {
lastTime = performance.now()
window.requestAnimationFrame(step)
}
start()
}
function mySetTimeout () {
console.log('mySetTimeout')
tick({
type: 'setTimeout',
interval: 1000,
cb: () => {
console.log('就 一次')
}
})
}
function mySetInterval () {
console.log('mySetInterval')
tick({
type: 'setInterval',
interval: 1000,
cb: () => {
console.log(`1s 一次`)
}
})
}
document.querySelector('.setTimeout').addEventListener('click', () => {
mySetTimeout()
})
document.querySelector('.setInterval').addEventListener('click', () => {
mySetInterval()
})
</script>
</body>
</html>
参考
1、 关于requestAnimationFrame的研究笔记
2、 JS 动画基础: 细说 requestAnimationFrame
3、使用requestAnimationFrame停止并继续动画
4、setTimeout vs requestAnimationFrame