JS -定时器setTimeout 和 setInterval

250 阅读3分钟

前言

在 JavaScript 异步编程中,定时器是最基础的工具。但很多开发者只知道它们能延迟执行,却忽略了它们的误差来源、最大值限制以及浏览器后台运行优化等底层细节。本文将带你全面梳理这两个核心 API。## 一、 setTimeout:延时执行的艺术

1. 基础概念

setTimeout 用于在指定的毫秒数后调用函数。

语法: let timeoutId = setTimeout(callback, delay);

  • 单次执行:回调函数只会执行一次。
  • 返回值:返回一个正整数 ID,可通过 clearTimeout(id) 取消执行。

2. 你必须知道的“潜规则”

  • 执行时间误差:由于它是宏任务,必须等待同步代码和微任务队列清空后才会执行。如果主线程被阻塞,实际执行时间会远超预设时间
  • 最大值限制:浏览器以 32 位整型存储延迟时间。这意味着最大值为 23112^{31} - 1 毫秒(约 24.8 天)。如果超过这个值,延迟会被重置为 0,导致立即执行。
  • 非活动标签页限制:为了节省电量和 CPU,浏览器会对未激活的后台标签页进行优化,定时器的最小间隔通常会被强制限制在 1000ms

二、 setInterval:循环执行的利弊

1. 基础概念

setInterval 每隔设定的时间固定调用回调函数,直到被手动清除。

语法: let intervalId = setInterval(callback, delay);

2. 核心演示

let i = 1;
const timerId = setInterval(() => {
    console.log(i++);
    if (i > 5) {
        clearInterval(timerId); // 到达条件后必须清除,否则会造成内存泄漏
    }
}, 1000);

三、 深度对比:两者有什么区别?

特性setTimeoutsetInterval
执行次数仅一次循环往复,直到清除
精准度存在误差(受主线程阻塞影响)存在误差(可能发生“丢帧”或堆积)
应用场景延迟加载、防抖、单次异步轮询、实时进度条、时钟显示

进阶对比:链式 setTimeout vs setInterval

面试中常问:为什么推荐用“链式 setTimeout”来模拟循环,而不是直接用 setInterval

  • setInterval 的问题:它不关心回调函数的执行耗时。如果回调函数执行时间长于间隔时间,两次执行之间会没有任何间隙,甚至发生任务堆积。

  • 链式 setTimeout 的优势

    function repeat() {
      // 逻辑代码...
      setTimeout(repeat, 1000); // 在代码执行完后再设置下一次定时
    }
    

    这样可以确保两次执行之间的间隙始终是 1000ms,不会发生重叠。


四、 面试模拟题

Q1:为什么 setTimeout(fn, 0) 不会立即执行?

参考回答:

  1. 事件循环机制:即使延迟是 0,fn 也会被放入宏任务队列。主线程必须先执行完同步代码和微任务队列。
  2. 最小间隔:HTML5 规范规定,如果定时器嵌套层级超过 5 层,最小延迟会被强制设为 4ms

Q2:如何实现一个准时的定时器?

参考回答:

由于 JS 单线程的特性,无法做到绝对准时。但可以通过以下方式优化:

  1. RequestAnimationFrame:如果是 UI 动画,使用 rAF 随屏幕刷新率执行。
  2. 计算偏移量:在每次执行时,通过 Date.now() 获取当前时间,计算与目标时间的差值,动态调整下一次 setTimeout 的延迟时间。

Q3:如果设置 setInterval(fn, 100),而 fn 执行需要 200ms,会发生什么?

参考回答:

JS 引擎会发现上一个任务还在执行,它会将新的任务放入队列。但为了避免无限堆积,部分浏览器会优化逻辑,如果队列中已有该定时器的回调,则不再添加。这会导致原本应该执行多次的任务看起来像是“连在一起执行”或者“跳过了某些执行”。


五、 总结建议

  • 清理定时器:无论是单次还是循环,在 React/Vue 组件销毁时,务必调用 clear 方法,防止内存泄漏。
  • 首选 setTimeout:对于复杂的循环逻辑,链式 setTimeoutsetInterval 更受控且安全。