setTimeout&setInterval&requestAnimationFrame

在这里插入图片描述

requestAnimationFrame 详解及与 setTimeout/setInterval 的比较

requestAnimationFrame(简称 rAF)是浏览器提供的专门用于 动画渲染 的 API,相比 setTimeoutsetInterval,它在性能和流畅度上有显著优势。以下是详细解析和对比:


1. requestAnimationFrame 详解

基本语法

const requestID = requestAnimationFrame(callback);

callback:在浏览器下一次重绘之前执行的函数。
返回值requestID(用于取消:cancelAnimationFrame(requestID))。

核心特点

  1. 与浏览器刷新率同步
    • 默认以 60Hz(16.67ms/帧) 的频率执行(匹配屏幕刷新率)。
    • 避免丢帧或过度渲染,保证动画流畅。

  2. 自动暂停后台标签页
    • 当页面隐藏或最小化时,rAF 会自动暂停,节省 CPU/GPU 资源。

  3. 高性能
    • 浏览器会优化 rAF 的调用,合并同一帧内的多次更新。

  4. 精确的时间戳参数
    • 回调函数接收一个 DOMHighResTimeStamp 参数,表示触发时间:

    requestAnimationFrame((timestamp) => {
        console.log(timestamp); // 精确到微秒
    });
    

示例:动画循环

function animate() {
    // 更新动画状态
    console.log("Animating...");
    
    // 循环调用
    requestAnimationFrame(animate);
}

// 启动动画
animate();

2. requestAnimationFrame vs setTimeout/setInterval

对比维度

特性requestAnimationFramesetTimeout/setInterval
执行频率与屏幕刷新率同步(~60Hz)固定时间间隔(可能不匹配刷新率)
后台标签页行为自动暂停继续执行(浪费资源)
动画流畅度高(无丢帧)可能卡顿(因主线程阻塞或帧率不稳定)
CPU/GPU 负载低(浏览器优化)高(频繁触发回调)
适用场景动画、高频视觉更新延迟任务、低频轮询

关键差异

(1)时间精度与帧率

rAF:按屏幕刷新率(如 60Hz)执行,避免过度渲染
setTimeout(fn, 16)
• 理论上模拟 60Hz,但实际可能因主线程阻塞导致延迟。
• 浏览器最小延迟限制(4ms)可能破坏时序。

(2)资源占用

rAF:浏览器智能调度,合并帧内更新

// 连续调用 rAF 会被优化
requestAnimationFrame(animate);
requestAnimationFrame(animate); // 可能合并到同一帧

setInterval严格按间隔执行,即使前一帧未完成也可能触发新回调,导致堆积。

(3)动画示例对比
setTimeout 实现动画(不推荐)
function animate() {
    console.log("Animating...");
    setTimeout(animate, 16); // 尝试模拟 60Hz
}
animate();

问题
• 可能因主线程阻塞导致卡顿。
• 后台标签页仍执行,浪费资源。

rAF 实现动画(推荐)
function animate() {
    console.log("Animating...");
    requestAnimationFrame(animate);
}
animate();

优势
• 自动匹配刷新率,流畅且节能。
• 后台自动暂停。


3. 如何选择?

使用 requestAnimationFrame 当:

• 需要 流畅动画(如 CSS 变换、Canvas 绘图)。
• 高频更新 UI(如游戏、实时图表)。
• 希望 节省资源(特别是移动端)。

使用 setTimeout/setInterval 当:

• 需要 精确控制延迟(如 1 秒后跳转页面)。
• 执行 非视觉任务(如轮询 API)。
• 兼容旧浏览器(rAF 需 IE10+)。


4. 进阶技巧

(1)计算帧率(FPS)

let lastTime = 0;
function animate(timestamp) {
    const fps = 1000 / (timestamp - lastTime); // 计算帧率
    console.log(`FPS: ${fps.toFixed(2)}`);
    lastTime = timestamp;
    requestAnimationFrame(animate);
}
animate();

(2)降级兼容(旧浏览器)

const rAF = window.requestAnimationFrame || 
            window.webkitRequestAnimationFrame || 
            function(callback) {
                return setTimeout(callback, 16);
            };

(3)控制动画速度

let startTime;
function animate(timestamp) {
    if (!startTime) startTime = timestamp;
    const progress = timestamp - startTime; // 动画已运行时间
    const duration = 2000; // 动画总时长(2秒)
    
    if (progress < duration) {
        const ratio = progress / duration; // 0~1
        console.log(`进度: ${(ratio * 100).toFixed(1)}%`);
        requestAnimationFrame(animate);
    }
}
animate();

5. 总结

API最佳场景注意事项
requestAnimationFrame动画、高频渲染无需手动控制帧率
setTimeout单次延迟任务避免用于动画(可能卡顿)
setInterval低频轮询(如每 5 秒检查数据)注意清理(clearInterval

黄金法则

凡是涉及 视觉更新 的,优先用 requestAnimationFrame
非视觉任务(如逻辑控制),再用 setTimeout/setInterval


手写 setTimeoutsetInterval(JavaScript 实现)

由于 setTimeoutsetInterval 是浏览器/Node.js 提供的 Web API,我们无法完全用纯 JavaScript 实现它们(因为它们依赖底层事件循环机制)。但我们可以用 JavaScript 模拟 它们的行为,并理解其核心逻辑。


1. 手写 setTimeout(模拟版)

思路

• 使用 Date.now() 计算时间差。
• 用 requestAnimationFrame(浏览器)或 while 循环(Node.js)检查是否到达延迟时间。

代码实现(浏览器环境)

function mySetTimeout(callback, delay) {
    const startTime = Date.now();
    
    function checkTime() {
        const currentTime = Date.now();
        if (currentTime - startTime >= delay) {
            callback(); // 时间到了,执行回调
        } else {
            requestAnimationFrame(checkTime); // 继续检查
        }
    }
    
    requestAnimationFrame(checkTime);
}

// 测试
mySetTimeout(() => console.log("Hello after 1s"), 1000);

说明
requestAnimationFrame 是浏览器 API,用于在下一帧渲染前执行回调(约 60fps)。
• 此方法 不精确requestAnimationFrame 不是严格计时器),但能模拟 setTimeout 的异步行为。


2. 手写 setInterval(模拟版)

思路

• 递归调用 mySetTimeout 实现循环执行。
• 用 clear 方法模拟 clearInterval

代码实现

function mySetInterval(callback, interval) {
    let timerId = null;
    
    function execute() {
        callback();
        timerId = mySetTimeout(execute, interval); // 递归调用
    }
    
    timerId = mySetTimeout(execute, interval);
    
    return {
        clear: () => {
            // 模拟 clearInterval
            if (timerId) {
                // 这里需要实现 clearMyTimeout,但简化版无法真正取消
                console.log("Interval cleared");
                timerId = null;
            }
        }
    };
}

// 测试
const interval = mySetInterval(() => console.log("Tick"), 1000);
setTimeout(() => interval.clear(), 5000); // 5秒后停止

问题
• 由于 mySetTimeout 无法真正取消(没有 clearMyTimeout),此方法 无法完全模拟 setInterval


3. 更精确的实现(基于 Promise + async/await

思路

• 用 Promise + setTimeout 模拟可控的 mySetTimeout
• 用 async/await 实现 mySetInterval

代码

// 精确版 mySetTimeout
function mySetTimeout(callback, delay) {
    return new Promise((resolve) => {
        setTimeout(() => {
            callback();
            resolve();
        }, delay);
    });
}

// 精确版 mySetInterval
async function mySetInterval(callback, interval) {
    while (true) {
        await mySetTimeout(callback, interval);
    }
}

// 测试
(async () => {
    mySetInterval(() => console.log("Tick"), 1000);
})();

特点
• 基于原生 setTimeout计时更精确
• 用 while(true) 实现循环,但 无法直接取消(需额外逻辑)。


4. 终极方案(完整模拟 clearTimeoutclearInterval

思路

• 用 Map 存储所有定时器 ID。
• 提供 clearMyTimeoutclearMyInterval 方法。

完整代码

const timers = new Map();
let id = 0;

// 模拟 setTimeout
function mySetTimeout(callback, delay) {
    const timerId = id++;
    const startTime = Date.now();
    
    function checkTime() {
        const currentTime = Date.now();
        if (currentTime - startTime >= delay) {
            callback();
            timers.delete(timerId); // 执行后移除
        } else if (timers.has(timerId)) {
            requestAnimationFrame(checkTime); // 继续检查
        }
    }
    
    timers.set(timerId, true);
    requestAnimationFrame(checkTime);
    
    return timerId;
}

// 模拟 clearTimeout
function clearMyTimeout(timerId) {
    if (timers.has(timerId)) {
        timers.delete(timerId); // 标记为取消
    }
}

// 模拟 setInterval
function mySetInterval(callback, interval) {
    const timerId = id++;
    
    function execute() {
        if (!timers.has(timerId)) return; // 已取消
        callback();
        mySetTimeout(execute, interval); // 递归调用
    }
    
    timers.set(timerId, true);
    mySetTimeout(execute, interval);
    
    return timerId;
}

// 模拟 clearInterval
function clearMyInterval(timerId) {
    clearMyTimeout(timerId); // 复用逻辑
}

// 测试
const timeoutId = mySetTimeout(() => console.log("Timeout"), 1000);
const intervalId = mySetInterval(() => console.log("Interval"), 1000);

setTimeout(() => {
    clearMyTimeout(timeoutId);
    clearMyInterval(intervalId);
}, 3000);

说明
• 用 Map 存储定时器 ID,clearMyTimeoutclearMyInterval 可以取消任务。
• 仍然依赖 requestAnimationFrame不是严格精确,但能模拟基本行为。


5. 总结

方法优点缺点
mySetTimeout简单模拟异步延迟不精确,依赖 requestAnimationFrame
mySetInterval模拟循环执行无法真正取消
Promise更接近原生行为仍依赖原生 setTimeout
终极方案支持取消,更完整代码较复杂

关键点

  1. setTimeoutsetInterval 是浏览器/Node.js 提供的 API,无法完全用 JS 实现。
  2. 模拟版依赖 requestAnimationFramePromise,无法做到完全精确。
  3. 最佳实践:直接使用原生 setTimeoutsetInterval,除非有特殊需求(如教学、自定义调度)。

希望这份指南帮你理解定时器的底层逻辑! 🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GISer_Jinger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值