requestAnimationFrame 与setTimeout之--八竿子打不着

89 阅读7分钟

引言

       现在的社会做什么都喜欢跨界,今天我们也玩点新鲜的,看看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)

参数

functionRef

当定时器到期后,将要执行的 function

code

这是一个可选语法,允许你包含在定时器到期后编译和执行的字符串而非函数。使用该语法是不推荐的,原因和使用 eval() 一样,有安全风险。

delay 可选

定时器在执行指定的函数或代码之前应该等待的时间,单位是毫秒。如果省略该参数,则使用值 0,意味着“立即”执行,或者更准确地说,在下一个事件循环执行。

注意,无论是哪种情况,实际延迟可能会比预期长一些,参见下方延时比指定值更长的原因一节的叙述。

还要注意的是,如果值不是数字,隐含的类型强制转换会静默地对该值进行转换,使其成为一个数字——这可能导致意想不到的、令人惊讶的结果;见非数字延迟值被静默地强制转化为数字以了解一个示例。

param1, …, paramN 可选

附加参数,一旦定时器到期,它们会作为参数传递给 functionRef 指定的函数。

返回值

返回值 timeoutID 是一个正整数,表示由 setTimeout() 调用创建的定时器的编号。这个值可以传递给 clearTimeout() 来取消该定时器。

requestAnimationFrame跨界实现setTimeout场景

requestAnimationFrame模拟setTimeout定时器

在Web开发中,setTimeout是一个常用的JavaScript API,用于在指定的延迟后执行代码。然而,在某些场景下,setTimeout可能不是最佳选择,特别是在需要更精确时间控制或动画性能优化时。这时,我们可以考虑使用requestAnimationFrame(RAF)来模拟setTimeout的行为。

上面我们介绍了setTimeout的用法,那么用requestAnimationFrame是否可以实现类似的功能呢?答案是肯定的,否则不就是废话了吗?

实现步骤

  1. 记录开始时间:在调用requestAnimationFrame之前,记录当前的时间戳。

  2. 编写回调函数:在回调函数中,计算当前时间与开始时间的差值,判断是否已经达到了设定的延迟时间。

  3. 执行代码并停止回调:如果达到了延迟时间,执行原计划在setTimeout中执行的代码,并使用cancelAnimationFrame(如果存在)或通过将回调函数设置为空函数来停止进一步的回调。

  4. 递归调用:在回调函数的末尾,如果还未达到延迟时间,则递归调用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开发中,接口轮询是一种常见的需求,它允许开发者定期从服务器获取最新数据,并在前端展示这些数据。传统上,setTimeoutsetInterval是实现这一功能的主要手段。然而,随着对性能要求的提高,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();  
  
// 如果页面变得不可见,轮询将自动暂停;当页面重新可见时,轮询将自动恢复。

优点与注意事项

优点

  1. 性能更优requestAnimationFrame与浏览器的重绘周期同步,能够减少不必要的重绘,提高性能。
  2. 资源节省:当页面不可见时,requestAnimationFrame会自动暂停,避免浪费CPU和电池资源。
  3. 更准确的时间控制:相比于setTimeoutrequestAnimationFrame的时间控制更加准确,因为它基于浏览器的刷新率。

注意事项

  1. 避免过度使用:虽然requestAnimationFrame性能更好,但在高负载或复杂动画场景下仍需注意避免过度消耗CPU和GPU资源。
  2. 兼容性:虽然现代浏览器都支持requestAnimationFrame,但在编写跨浏览器代码时仍需考虑兼容性问题。

总之,requestAnimationFrame是一个强大的工具,可以用来替代setTimeout实现更高效、更准确的接口轮询调用。通过合理的封装和使用,我们可以充分利用其优势,提升Web应用的性能和用户体验。