[Js进阶]requestAnimationFrame应用
简介
requestAnimationFrame
是一个用于动画效果的 API,它使用浏览器的刷新率来执行回调函数,通常每秒钟执行 60 次。与 setTimeout
和 setInterval
不同的是,requestAnimationFrame
被视为一个特殊的“宏任务”。
宏任务是一个在事件循环队列中排队等待执行的任务。与宏任务不同的是,“微任务”是一个在当前任务完成后立即执行的任务,它通常用于处理异步操作的结果。
在 JavaScript 中,异步操作通常是通过回调函数或 Promise 处理的。当异步操作完成后,回调函数将被添加到微任务队列中,以便在当前任务完成后立即执行。这意味着,微任务的执行顺序优先于下一个宏任务。
对于 requestAnimationFrame
,由于它被视为一个宏任务,因此它的执行顺序优先于微任务。这意味着,如果在 requestAnimationFrame
回调函数中添加了微任务,那么这些微任务将在下一个宏任务(即下一个 requestAnimationFrame
回调函数)执行前被执行。
应用场景
requestAnimationFrame
主要用于实现流畅的动画效果,它可以在浏览器的重绘周期内执行指定的函数,从而避免由于频繁的重绘导致的性能问题。除了动画之外,requestAnimationFrame
还可以应用于以下场景:
- 实现平滑滚动效果。使用
requestAnimationFrame
可以在滚动过程中不断更新滚动位置,并且可以控制滚动的速度和加速度,从而实现更加自然和流畅的滚动效果。 - 实现倒计时效果。使用
requestAnimationFrame
可以控制倒计时的间隔和更新频率,并且可以在倒计时结束之后立即执行指定的函数,从而实现更加精确和可控的倒计时效果。 - 实现拖拽效果。使用
requestAnimationFrame
可以在拖拽过程中不断更新元素的位置,并且可以控制拖拽的速度和加速度,从而实现更加自然和流畅的拖拽效果。 - 实现页面的渐变效果。使用
requestAnimationFrame
可以在一段时间内不断更新页面元素的样式,并且可以控制渐变的速度和方向,从而实现更加自然和流畅的渐变效果。
…
需要注意的是,requestAnimationFrame
并不是万能的,它也有一些局限性。例如,在一些需要高精度计算的场景下,requestAnimationFrame
可能无法满足需求,此时可能需要使用其他更加精细的计时器或者事件来实现。
实现平滑滚动效果
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Smooth Scroll</title>
<style>
body {
height: 8000px;
}
.scroll-btn {
position: fixed;
bottom: 20px;
right: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<button class="scroll-btn">Scroll to Bottom</button>
<script>
const btn = document.querySelector('.scroll-btn');
const scrollHeight = document.body.scrollHeight;
const screenHeight = window.innerHeight;
function scrollToBottom() {
const currentScroll = window.pageYOffset;
const remainingScroll = scrollHeight - currentScroll - screenHeight;
if (remainingScroll > 0) {
window.scrollTo(0, currentScroll + remainingScroll / 20);
requestAnimationFrame(scrollToBottom);
}
}
btn.addEventListener('click', scrollToBottom);
</script>
</body>
</html>
在这个示例中,我们首先获取了页面的滚动高度和屏幕高度。然后,定义了一个scrollToBottom
函数,这个函数会在每一帧中更新滚动位置,并且使用requestAnimationFrame
函数注册下一次动画帧。在scrollToBottom
函数中,我们首先计算出当前滚动位置和还剩余的滚动距离,然后根据剩余滚动距离计算出滚动的距离,并使用window.scrollTo
函数实现滚动。最后,如果还有剩余的滚动距离,就使用requestAnimationFrame
函数注册下一次动画帧,从而实现平滑滚动效果。
实现倒计时效果
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Countdown</title>
<style>
body {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#countdown {
font-size: 48px;
}
</style>
</head>
<body>
<div id="countdown"></div>
<script>
const countdown = document.querySelector('#countdown');
let endTime = Date.now() + 10000; // 倒计时 10s
function updateCountdown() {
const remainingTime = endTime - Date.now();
if (remainingTime > 0) {
const remainingSeconds = Math.floor(remainingTime / 1000);
countdown.innerText = remainingSeconds;
requestAnimationFrame(updateCountdown);
} else {
countdown.innerText = '0';
}
}
// updateCountdown()
requestAnimationFrame(updateCountdown);
</script>
</body>
</html>
在这个示例中,我们首先定义了一个endTime
变量,表示倒计时结束的时间。然后,定义了一个updateCountdown
函数,这个函数会在每一帧中更新倒计时,并且使用requestAnimationFrame
函数注册下一次动画帧。在updateCountdown
函数中,我们首先计算出当前的剩余时间,然后根据剩余时间计算出剩余秒数,并更新倒计时的显示。最后,如果还有剩余时间,就使用requestAnimationFrame
函数注册下一次动画帧,从而实现倒计时效果。
实现拖拽效果
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Drag and Drop</title>
<style>
#drag {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
background-color: #f00;
cursor: move;
}
</style>
</head>
<body>
<div id="drag"></div>
<script>
const drag = document.querySelector('#drag');
let isDragging = false;
let startX = 0;
let startY = 0;
function handleMouseDown(event) {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
}
function handleMouseMove(event) {
if (isDragging) {
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
const currentX = parseInt(drag.style.left) || 0;
const currentY = parseInt(drag.style.top) || 0;
drag.style.left = currentX + deltaX + 'px';
drag.style.top = currentY + deltaY + 'px';
startX = event.clientX;
startY = event.clientY;
requestAnimationFrame(handleMouseMove);
}
}
function handleMouseUp() {
isDragging = false;
}
drag.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
</script>
</body>
</html>
在这个示例中,我们首先定义了一个可拖拽的元素drag
,并且为它添加了鼠标样式和事件。然后,定义了三个函数handleMouseDown
、handleMouseMove
和handleMouseUp
,分别处理鼠标按下、鼠标移动和鼠标抬起事件。在handleMouseDown
函数中,我们记录下鼠标按下时的位置。在handleMouseMove
函数中,我们首先判断鼠标是否按下,如果按下了,就计算出鼠标移动的距离,并根据距离更新元素的位置。最后,如果正在拖拽,就使用requestAnimationFrame
函数注册下一次动画帧,从而实现平滑拖拽效果。在handleMouseUp
函数中,我们记录下鼠标抬起的事件,表示拖拽结束。
实现渐变效果
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>渐变效果示例</title>
</head>
<body>
<div id="myElement" style="width: 200px; height: 200px;"></div>
<script>
const element = document.getElementById('myElement'); // 获取需要渐变的元素
let start = null; // 记录动画开始时间
const duration = 10000; // 动画持续时间(单位:毫秒)
const startColor = [255, 255, 255]; // 起始颜色(白色)
const endColor = [0, 0, 0]; // 结束颜色(黑色)
function animate(timestamp) {
if (!start) start = timestamp; // 如果动画未开始,则记录开始时间
const elapsed = timestamp - start; // 计算动画已经进行的时间
const progress = Math.min(elapsed / duration, 1); // 计算动画进度(0-1)
const color = interpolateColor(startColor, endColor, progress); // 计算当前颜色
element.style.backgroundColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`; // 设置元素背景颜色
if (progress < 1) {
requestAnimationFrame(animate); // 如果动画未结束,则继续执行动画
}
}
requestAnimationFrame(animate); // 开始执行动画
function interpolateColor(startColor, endColor, progress) {
const color = [];
for (let i = 0; i < 3; i++) {
color[i] = Math.round(startColor[i] + (endColor[i] - startColor[i]) * progress);
}
return color;
}
</script>
</body>
</html>
Vue中结合使用
以下是一个简单的自定义指令示例,用于在元素滚动时实现类似于 Parallax 效果
<template>
<div class="parallax" v-parallax="{ speed: 0.2 }">
<img src="image.jpg">
</div>
</template>
<script>
export default {
directives: {
parallax: {
bind: function (el, binding) {
const speed = binding.value.speed || 0.1;
let lastScrollTop = 0;
let animationFrameId = null;
function animate() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
const offset = (scrollTop - lastScrollTop) * speed;
lastScrollTop = scrollTop;
el.style.transform = `translateY(${offset}px)`;
animationFrameId = requestAnimationFrame(animate);
}
animate();
el.__parallax_unbind__ = function() {
cancelAnimationFrame(animationFrameId);
};
},
unbind: function (el) {
el.__parallax_unbind__();
delete el.__parallax_unbind__;
}
}
}
};
</script>
<style scoped>
.parallax {
position: relative;
height: 300px;
overflow: hidden;
}
.parallax img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
}
</style>
在这个例子中,我们创建了一个名为 v-parallax
的指令,并传递了一个 speed
参数用于控制滚动速度。在指令的 bind
钩子中,我们使用 requestAnimationFrame
来实现动画效果,根据滚动的位置计算出偏移量,并将其应用于元素的 transform
属性。
在指令的 unbind
钩子中,我们清除了动画效果,以避免在元素被销毁时仍然继续执行动画。
在模板中,我们将指令应用于包含图片的 div
元素上,并设置了一些基本的样式。当页面滚动时,图片将以不同的速度向上或向下滚动,实现类似于 Parallax 效果。
注意,在这个示例中,我们使用了 window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
来获取滚动的位置。这是因为不同的浏览器可能会使用不同的属性来表示滚动偏移量。
另外,由于 requestAnimationFrame
在每秒钟执行大约 60 次,因此它比使用 setTimeout
或 setInterval
更加高效和流畅。
React中结合使用
实现一个逐渐变暗的背景效果
import React, { useRef, useEffect } from 'react';
function Background() {
const backgroundRef = useRef(null);
useEffect(() => {
let requestId;
let opacity = 1;
function animate() {
opacity -= 0.01;
if (opacity < 0) {
opacity = 0;
}
backgroundRef.current.style.opacity = opacity;
if (opacity > 0) {
requestId = requestAnimationFrame(animate);
}
}
requestId = requestAnimationFrame(animate);
return () => {
cancelAnimationFrame(requestId);
};
}, []);
return (
<div
ref={backgroundRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: '#000',
opacity: 1
}}
/>
);
}
在这个例子中,我们使用 useRef
钩子创建了一个名为 backgroundRef
的引用,并将其绑定到一个 div
元素上。在 useEffect
钩子中,我们使用 requestAnimationFrame
实现了逐渐变暗的背景效果。
在 useEffect
钩子中,我们使用了一个 requestId
变量来存储 requestAnimationFrame
的返回值,以便在组件被卸载时可以清除它。我们还使用了一个 opacity
变量来存储背景的透明度,并在每次 animate
函数调用中减少它。当透明度小于 0 时,我们将其设置为 0,并停止调用 requestAnimationFrame
。
在组件的返回值中,我们将 backgroundRef
绑定到一个 div
元素上,并设置了一些基本的样式。在 useEffect
钩子中,我们使用 backgroundRef 的
current属性来获取该元素,并将其透明度设置为
opacity` 变量的值。在初次渲染时,背景的透明度将被设置为 1。
当组件被卸载时,我们使用 cancelAnimationFrame
函数清除 requestAnimationFrame
的调用。这是为了防止在组件卸载后仍然继续执行动画。
与setTimeout比较
requestAnimationFrame
和 setTimeout
都是用于异步执行代码的 JavaScript API。
异同点:
- 执行时机:
setTimeout
函数会在指定的时间间隔后执行回调函数,而requestAnimationFrame
则会在浏览器下一次重绘之前执行回调函数。通常情况下,浏览器的重绘频率是每秒钟 60 次,因此requestAnimationFrame
通常比setTimeout
更加高效、流畅。 - 回调函数参数:
setTimeout
函数的回调函数会接收一个可选的参数,表示在指定的时间间隔后是否重复执行回调函数。而requestAnimationFrame
的回调函数不接受任何参数。 - 取消执行:通过调用
clearTimeout
函数可以取消setTimeout
的执行,而通过调用cancelAnimationFrame
函数可以取消requestAnimationFrame
的执行。 - 兼容性:
setTimeout
是一个比较老的 API,在所有主流浏览器中都得到了支持。而requestAnimationFrame
则是比较新的 API,在一些旧的浏览器中可能不被支持。
异同点总结:
相同点:都是用于异步执行代码的 JavaScript API。
不同点:
-
执行时机不同,
setTimeout
在指定时间后执行回调函数,requestAnimationFrame
在浏览器下一次重绘前执行回调函数。 -
回调函数参数不同,
setTimeout
的回调函数接受一个可选的参数,表示是否重复执行回调函数,requestAnimationFrame
不接受任何参数。 -
取消执行方式不同,
clearTimeout
取消setTimeout
的执行,cancelAnimationFrame
取消requestAnimationFrame
的执行。 -
兼容性不同,
setTimeout
在所有主流浏览器中都得到了支持,requestAnimationFrame
在一些旧的浏览器中可能不被支持。另外,需要注意的是,
setTimeout
和requestAnimationFrame
适用于不同的场景。setTimeout
通常用于延迟执行一段代码,例如实现一个倒计时功能。而requestAnimationFrame
则通常用于实现动画效果,因为它能够根据浏览器的刷新率自动调整动画的帧率,从而使得动画更加流畅。
与setInterval比较
异同点:
- 执行时机:
setInterval
函数会在指定的时间间隔后重复执行回调函数,而requestAnimationFrame
则会在浏览器下一次重绘之前执行回调函数。与setTimeout
一样,requestAnimationFrame
通常比setInterval
更加高效、流畅。 - 回调函数参数:
setInterval
函数的回调函数会接收一个可选的参数,表示在指定的时间间隔后是否重复执行回调函数。而requestAnimationFrame
的回调函数不接受任何参数。 - 取消执行:通过调用
clearInterval
函数可以取消setInterval
的执行,而通过调用cancelAnimationFrame
函数可以取消requestAnimationFrame
的执行。 - 执行间隔:
setInterval
函数的执行间隔是固定的,而requestAnimationFrame
的执行间隔则会根据浏览器的刷新率自动调整。因此,requestAnimationFrame
更加适合实现动画效果,而setInterval
更适合实现一些周期性的操作,例如定时发送心跳包等。
异同点总结:
相同点:都是用于周期性地执行代码的 JavaScript API。
不同点:
-
执行时机不同,
setInterval
在指定时间间隔后重复执行回调函数,requestAnimationFrame
在浏览器下一次重绘前执行回调函数。 -
回调函数参数不同,
setInterval
的回调函数接受一个可选的参数,表示是否重复执行回调函数,requestAnimationFrame
不接受任何参数。 -
取消执行方式不同,
clearInterval
取消setInterval
的执行,cancelAnimationFrame
取消requestAnimationFrame
的执行。 -
执行间隔不同,
setInterval
的执行间隔是固定的,而requestAnimationFrame
的执行间隔则会根据浏览器的刷新率自动调整。另外,需要注意的是,
setInterval
和requestAnimationFrame
适用于不同的场景。setInterval
通常用于定时执行一些简单的周期性操作,例如每隔一定时间打印一次日志。而requestAnimationFrame
则通常用于实现动画效果,因为它能够根据浏览器的刷新率自动调整动画的帧率,从而使得动画更加流畅。