JavaScript定时器: 为了写一个时间倒计时组件,我重新封装了倒计时方法

398 阅读4分钟

引言

在前端开发中,我们经常需要处理定时任务或动画效果。JavaScript提供了多种实现方式,其中最常用的是setTimeoutsetInterval,而requestAnimationFrame则是HTML5新增的API,专为动画优化设计。本文将深入探讨这三种定时器的工作原理、优缺点,并展示如何使用requestAnimationFrame模拟前两者的功能,以解决它们存在的问题。

一、setTimeout详解

1.1 基本用法

// 延迟1000毫秒后执行回调函数
const timeoutId = setTimeout(() => {
  console.log('延迟执行');
}, 1000);

// 取消定时器
clearTimeout(timeoutId);

1.2 优点

  • 简单易用:API直观,学习成本低
  • 灵活性高:可以设置任意延迟时间
  • 广泛兼容:所有浏览器都支持

1.3 缺陷

  • 精度问题:实际延迟时间可能大于设定值
  • 最小延迟限制:HTML5规范规定最小延迟为4ms
  • 执行顺序不确定:受主线程任务队列影响
  • 阻塞问题:如果主线程繁忙,回调会被延迟执行

二、setInterval详解

2.1 基本用法

// 每隔1000毫秒执行一次回调函数
const intervalId = setInterval(() => {
  console.log('定时执行');
}, 1000);

// 取消定时器
clearInterval(intervalId);

2.2 优点

  • 自动重复执行:无需手动递归调用
  • 实现简单:适合周期性任务
  • 资源占用稳定:相比递归setTimeout更高效

2.3 缺陷

  • 累积执行问题:如果回调执行时间超过间隔时间,会导致回调堆积,存在跳帧情况,可能多个定时器连续执行
  • 时间漂移:长期运行后,实际执行间隔会偏离预期
  • 无执行完成保证:页面卸载时可能无法执行清理操作
  • 同样存在最小延迟限制:与setTimeout相同

三、requestAnimationFrame详解

3.1 基本用法

function animate(timestamp) {
  // 动画逻辑
  console.log('动画帧执行');
  requestAnimationFrame(animate);
}

// 启动动画
const rafId = requestAnimationFrame(animate);

// 取消动画
cancelAnimationFrame(rafId);

3.2 优点

  • 与屏幕刷新率同步:通常为60fps,避免过度绘制
  • 浏览器优化:后台标签页或隐藏元素时会暂停执行
  • 性能更好:减少CPU占用,延长移动设备电池寿命
  • 时间精度高:使用系统时间戳,不受执行时间影响
  • 自动调整:根据设备性能动态调整帧率

3.3 缺陷

  • 时间间隔不可控:由浏览器决定执行时机
  • 不适合精确计时:无法保证毫秒级精度
  • 兼容性问题:IE9及以下不支持
  • 需要手动递归:实现循环需要显式调用

四、三者性能对比

特性setTimeoutsetIntervalrequestAnimationFrame
执行时机延迟指定时间后每隔指定时间下一帧渲染前
精度低(可能延迟)低(可能漂移)高(与刷新同步)
最小延迟4ms4ms约16ms(60fps)
CPU占用
后台执行延迟增加延迟增加暂停
动画优化

4.1三者动画示例 以及 执行差值校对

对比: 查看动画流畅度 以及 执行偏差(查看log)

4.2、总结

1.setTimeoutsetInterval适用于简单的定时任务,但存在精度和性能问题;
2.requestAnimationFrame专为动画优化,提供更好的性能和用户体验。

下面我们通过模拟实现,我们可以结合requestAnimationFrame的优势来替代传统定时器。

五、用requestAnimationFrame模拟定时器

5.1 模拟setTimeout

// raf.js
function rafTimeout(callback, delay) {  
    const start = performance.now();  
    let id;  

    function loop(timestamp) {  
        // timestamp参数:与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻  
        const elapsed = timestamp - start;  
        if (elapsed >= delay) {  
            callback();  
        } else {  
            id = requestAnimationFrame(loop);  
        }  
    }  

    id = requestAnimationFrame(loop);  
    return id;  
}  
  
// 使用示例  
const timeoutId = rafTimeout(() => {  
console.log('模拟setTimeout执行');  
}, 1000);  
  
// 取消  
cancelAnimationFrame(timeoutId);  

5.2 模拟setInterval

setInterval 就是 在 模拟 setTimeout执行之后 重新计算间隔 做判断

// raf.js
/**  
*  
* @param callback  
* @param delay 延时ms  
* @param interval 模拟setInterval ++++++++  
* @returns {number}  
*/  
function rafTimeout(callback, delay, interval = false) {  
    let start = performance.now();  
    let id;  

    function loop(timestamp) {
        const elapsed = timestamp - start;  
        if (elapsed >= delay) {  
            callback();  
            // ++++++++  新增
            if (interval) {  
                start = timestamp;  
                id = requestAnimationFrame(loop);  
            }  
            // ++++++++  新增
        } else {  
            id = requestAnimationFrame(loop);  
        }  
    }  

    id = requestAnimationFrame(loop);  
    return id;  
}  
  
// 使用示例  
const intervalId = rafTimeout(() => {  
    console.log('模拟setInterval执行');  
}, 1000, true);  
  
// 取消  
// cancelAnimationFrame(intervalId);  
setTimeout(() => {  
    cancelAnimationFrame(intervalId);  
}, 50)  

发现问题: 由于我们是通过 多次执行 requestAnimationFrame 根据时间戳判断时间再做callback的,也就意味着 其实我们返回的 id 其实是一直在变动的,我们错过在第一次执行 requestAnimationFrame 方法之后 就无法正确获取当前 raf的id, 我们可以将id交给引用类型来接管

 // id => { id }
function rafTimeout(callback, delay, interval = false) {  
    // ...
    // let id; // ----  
    const raf = { id: requestAnimationFrame(loop) } // ++++++++
    function loop(timestamp) {  
    // ...
        if (interval) {
        // ...
        // id = requestAnimationFrame(loop) --------
        raf.id = requestAnimationFrame(loop);  // ++++++++
        }  
    } else {  
        // id = requestAnimationFrame(loop) --------
        raf.id = requestAnimationFrame(loop);  // ++++++++
        }  
    }  
    return raf;  // ++++
}

六 完整代码

// raf.js  
/**  
*  
* @param callback  
* @param delay 延时ms  
* @param interval 模拟setInterval 
* @returns {number}  
*/  
function rafTimeout(callback, delay, interval = false) {  
    let start = performance.now();  
    const raf = { id: requestAnimationFrame(loop) }  
    function loop(timestamp) {  
        const elapsed = timestamp - start;  
        if (elapsed >= delay) {  
            callback();  
            if (interval) {  
                start = timestamp;  
                raf.id = requestAnimationFrame(loop);  
            }  
        } else {  
            raf.id = requestAnimationFrame(loop);  
        }  
    }  
    return raf;  
}  
// 取消 rafTimeout  
function cancelRaf(raf) {  
    if (raf && raf.id) cancelAnimationFrame(raf.id)  
}  
  
// 使用示例  
const raf = rafTimeout(() => {  
    console.log('模拟setInterval执行');  
}, 1000, true);  
  
// 间隔2s取消  
setTimeout(() => {  
    cancelRaf(raf);  
}, 2020)