引言
现在的社会做什么都喜欢跨界,今天我们也玩点新鲜的,看看requestAnimationFrame和setTimeout这两个完全不属于同一范畴的两个东西,能碰撞出什么火花,跨界产出什么样吸引入的东西。
简介
什么是requestAnimationFrame
[MDN描述](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame): window.requestAnimationFrame() 方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。对回调函数的调用频率通常与显示器的刷新率相匹配。虽然 75hz、120hz 和 144hz 也被广泛使用,但是最常见的刷新率还是 60hz(每秒 60 个周期/帧)。为了提高性能和电池寿命,大多数浏览器都会暂停在后台选项卡或者隐藏的 <iframe> 中运行的 requestAnimationFrame()。
语法
requestAnimationFrame(callback)
callback- 该函数会在下一次重绘更新你的动画时被调用到。这个回调函数只会传递一个参数:一个
DOMHighResTimeStamp参数,用于表示上一帧渲染的结束时间(基于 time origin 的毫秒数) - 时间戳是一个以毫秒为单位的十进制数字,最小精度为 1 毫秒。对于
Window对象(而非workers)来说,它等同于document.timeline.currentTime。此时间戳在同一代理上(所有同源的window,更重要的是同源的iframe)运行的所有窗口之间共享——它允许在多个requestAnimationFrame回调函数中执行同步动画。此时间戳值也近似于在回调函数开始时调用performance.now(),但它们永远都不会是相同的值。 - 当
requestAnimationFrame()队列中的多个回调开始在同一帧中触发时,它们都会收到相同的时间戳,即便在计算前一个回调函数工作量时这一帧的时间已经过去。
- 该函数会在下一次重绘更新你的动画时被调用到。这个回调函数只会传递一个参数:一个
返回值
请求 ID 是一个 long 类型整数值,是在回调列表里的唯一标识符。这是一个非零值,但你不能对该值做任何其他假设。你可以将此值传递给 window.cancelAnimationFrame() 函数以取消该刷新回调请求。
什么是setTimeout
全局的 setTimeout() 方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段。
语法
setTimeout(code)
setTimeout(code, delay)
setTimeout(functionRef)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay, param1)
setTimeout(functionRef, delay, param1, param2)
setTimeout(functionRef, delay, param1, param2, /* … ,*/ paramN)
参数
当定时器到期后,将要执行的 function。
这是一个可选语法,允许你包含在定时器到期后编译和执行的字符串而非函数。使用该语法是不推荐的,原因和使用 eval() 一样,有安全风险。
delay 可选
定时器在执行指定的函数或代码之前应该等待的时间,单位是毫秒。如果省略该参数,则使用值 0,意味着“立即”执行,或者更准确地说,在下一个事件循环执行。
注意,无论是哪种情况,实际延迟可能会比预期长一些,参见下方延时比指定值更长的原因一节的叙述。
还要注意的是,如果值不是数字,隐含的类型强制转换会静默地对该值进行转换,使其成为一个数字——这可能导致意想不到的、令人惊讶的结果;见非数字延迟值被静默地强制转化为数字以了解一个示例。
param1, …, paramN 可选
附加参数,一旦定时器到期,它们会作为参数传递给 functionRef 指定的函数。
返回值
返回值 timeoutID 是一个正整数,表示由 setTimeout() 调用创建的定时器的编号。这个值可以传递给 clearTimeout() 来取消该定时器。
requestAnimationFrame跨界实现setTimeout场景
requestAnimationFrame模拟setTimeout定时器
在Web开发中,setTimeout是一个常用的JavaScript API,用于在指定的延迟后执行代码。然而,在某些场景下,setTimeout可能不是最佳选择,特别是在需要更精确时间控制或动画性能优化时。这时,我们可以考虑使用requestAnimationFrame(RAF)来模拟setTimeout的行为。
上面我们介绍了setTimeout的用法,那么用requestAnimationFrame是否可以实现类似的功能呢?答案是肯定的,否则不就是废话了吗?
实现步骤
-
记录开始时间:在调用
requestAnimationFrame之前,记录当前的时间戳。 -
编写回调函数:在回调函数中,计算当前时间与开始时间的差值,判断是否已经达到了设定的延迟时间。
-
执行代码并停止回调:如果达到了延迟时间,执行原计划在
setTimeout中执行的代码,并使用cancelAnimationFrame(如果存在)或通过将回调函数设置为空函数来停止进一步的回调。 -
递归调用:在回调函数的末尾,如果还未达到延迟时间,则递归调用
requestAnimationFrame并传入相同的回调函数。/**
- 使用requestAnimationFrame实现的延迟setTimeout或间隔setInterval调用函数。
- @param fn 要执行的函数。
- @param delay 延迟的时间,单位为ms,默认为0,表示不延迟立即执行。
- @param interval 是否间隔执行,如果为true,则在首次执行后,以delay为间隔持续执行。
- @returns 返回一个对象,包含一个id属性,该id为requestAnimationFrame的调用ID,可用于取消动画帧。 / export function rafTimeout(fn: Function, delay = 0, interval = false): object { let start: number | null = null // 记录动画开始的时间戳 function timeElapse(timestamp: number) { // 定义动画帧回调函数 / timestamp参数:与performance.now()的返回值相同,它表示requestAnimationFrame()开始去执行回调函数的时刻 / if (!start) { // 如果还没有开始时间,则以当前时间为开始时间 start = timestamp } const elapsed = timestamp - start if (elapsed >= delay) { try { fn() // 执行目标函数 } catch (error) { console.error('Error executing rafTimeout function:', error) } if (interval) { // 如果需要间隔执行,则重置开始时间并继续安排下一次动画帧 start = timestamp raf.id = requestAnimationFrame(timeElapse) } } else { raf.id = requestAnimationFrame(timeElapse) } } interface AnimationFrameID { id: number } // 创建一个对象用于存储动画帧的ID,并初始化动画帧 const raf: AnimationFrameID = { id: requestAnimationFrame(timeElapse) } return raf } /*
- 用于取消 rafTimeout 函数
- @param raf - 包含请求动画帧ID的对象。该ID是由requestAnimationFrame返回的。
-
该函数旨在取消之前通过requestAnimationFrame请求的动画帧。 -
如果传入的raf对象或其id无效,则会打印警告。
*/ export function cancelRaf(raf: { id: number }): void { if (raf && raf.id && typeof raf.id === 'number') { cancelAnimationFrame(raf.id) } else { console.warn('cancelRaf received an invalid id:', raf) } }
requestAnimationFrame替代setTimout实现的接口轮询调用
在Web开发中,接口轮询是一种常见的需求,它允许开发者定期从服务器获取最新数据,并在前端展示这些数据。传统上,setTimeout和setInterval是实现这一功能的主要手段。然而,随着对性能要求的提高,requestAnimationFrame(RAF)逐渐成为一个更受欢迎的选择,特别是在需要高精度时间控制的动画。
class EventEmitter {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event, ...args) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(...args));
}
}
}
// Poller 类
class Poller extends EventEmitter {
constructor(url, options = {}) {
super();
this.url = url;
this.targetInterval = options.interval || 5000; // 目标轮询间隔
this.lastTime = null; // 上次执行时间
this.fetch = options.fetch || fetch; // 自定义 fetch 函数
this.rafId = null; // requestAnimationFrame 的 ID
this.isPaused = false; // 是否因页面不可见而暂停
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.pause();
} else {
this.resume();
}
});
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.pause = this.pause.bind(this);
this.resume = this.resume.bind(this);
this.poll = this.poll.bind(this);
this.animate = this.animate.bind(this);
}
start() {
if (!this.rafId) {
this.isPaused = false;
this.rafId = requestAnimationFrame(this.animate);
}
}
stop() {
cancelAnimationFrame(this.rafId);
this.rafId = null;
this.isPaused = true; // 停止时也认为是暂停状态
}
pause() {
this.isPaused = true;
cancelAnimationFrame(this.rafId);
}
resume() {
if (this.isPaused) {
this.isPaused = false;
this.rafId = requestAnimationFrame(this.animate);
}
}
animate(timestamp) {
if (!this.isPaused && this.lastTime) {
const elapsed = timestamp - this.lastTime;
if (elapsed >= this.targetInterval) {
this.pollData();
this.lastTime = timestamp - (elapsed % this.targetInterval);
}
} else {
this.lastTime = timestamp; // 初始化或恢复时记录时间
}
if (!this.isPaused) {
this.rafId = requestAnimationFrame(this.animate);
}
}
pollData() {
this.fetch(this.url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
this.emit('success', data);
})
.catch(error => {
this.emit('error', error);
});
}
}
// 使用示例
const poller = new Poller('https://api.example.com/data', { interval: 3000 });
poller.on('success', data => console.log('Data received:', data));
poller.on('error', error => console.error('Error fetching data:', error));
poller.start();
// 假设在某个时间点需要停止轮询
// poller.stop();
// 如果页面变得不可见,轮询将自动暂停;当页面重新可见时,轮询将自动恢复。
优点与注意事项
优点
- 性能更优:
requestAnimationFrame与浏览器的重绘周期同步,能够减少不必要的重绘,提高性能。 - 资源节省:当页面不可见时,
requestAnimationFrame会自动暂停,避免浪费CPU和电池资源。 - 更准确的时间控制:相比于
setTimeout,requestAnimationFrame的时间控制更加准确,因为它基于浏览器的刷新率。
注意事项
- 避免过度使用:虽然
requestAnimationFrame性能更好,但在高负载或复杂动画场景下仍需注意避免过度消耗CPU和GPU资源。 - 兼容性:虽然现代浏览器都支持
requestAnimationFrame,但在编写跨浏览器代码时仍需考虑兼容性问题。
总之,requestAnimationFrame是一个强大的工具,可以用来替代setTimeout实现更高效、更准确的接口轮询调用。通过合理的封装和使用,我们可以充分利用其优势,提升Web应用的性能和用户体验。