什么,你还在项目中直接使用setTimeout和setInterval,文末附解决方案

定时器(setTimeout/setInterval)是 JavaScript 异步编程的核心工具,但开发者往往低估其复杂性。本文将揭示这两个 API 在高精度场景下的隐藏风险,并通过 代码示例展示如何构建健壮的定时调度系统。

JavaScript 是单线程语言,它只有一个主线程,所有任务都在这个线程上执行,这意味着一次只能做一件事。为了处理异步操作,比如计时器、网络请求等,JavaScript 使用了事件循环(Event Loop)机制。当一个异步操作完成时,它会被放入回调队列中等待主线程空闲时再执行。

一、setTimeout 和 setInterval 函数:

功能对比
  • setTimeout:用来设定一个计时器,在指定的时间(毫秒)后仅执行一次给定的回调函数。
  • setInterval:类似于 setTimeout,但它会在每隔指定的时间间隔重复执行给定的函数,适用于需要定期执行的任务。
参数结构

两者都接受相似的参数:

  1. callback:当计时器到期时要执行的函数。
  2. time:对于 setTimeout 是延迟时间,对于 setInterval 是每次执行之间的间隔时间,以毫秒为单位。非数字值会被视为 0,负数也会被当作 0 处理。
  3. [arg1, arg2, ...] :可选参数,这些参数将在调用回调函数时传递给它。
返回值
  • setTimeout 和 setInterval 都返回一个定时器 ID,这是一个非零整数值,代表已创建的计时器。这个返回值一般用于取消对应的定时器

那么,我们可以得出一个结论:

setTimeout一定会在指定时间后执行吗?

答案当然是否定的!为什么呢?

因为当你的主线程程序是无限次的循环时,那么主线程没有结束,就不会去执行定时器中的回调函数了。

那下面我们们来看看他们潜在的陷阱:

二、setInterval 的致命陷阱

2.1 回调堆积场景模拟

let executionCount = 0;

setInterval(() => {
  const start = Date.now();
  executionCount++;
  
  // 模拟耗时操作
  while(Date.now() - start < 150) {}
  
  console.log(`Execution ${executionCount}: ${Date.now() - start}ms`);
}, 100);

/* 输出:
Execution 1: 150ms
Execution 2: 150ms (立即执行)
Execution 3: 150ms (立即执行) 
*/

 上图例子,当回调执行时间(150ms)超过间隔时间(100ms)时,后续回调会连续执行,导致:

  • CPU 使用率飙升
  • 内存泄漏风险增加
  • 事件循环被阻塞

2.2 执行时序可视化

时间轴(ms)事件队列状态
0设置定时器[回调1]
100尝试触发回调2[回调1, 回调2]
150完成回调1[回调2]
200尝试触发回调3[回调2, 回调3]
300完成回调2[回调3]

三、setTimeout 的误差累积效应

3.1 误差放大实验

  const startTime = new Date().getTime(), delay = 1000
  let count = 0
  let timer = setTimeout(doFunc, delay)
  function doFunc(){
    count++
    console.log(new Date().getTime() - (startTime + count * 1000) + 'ms')
    if(count < 10){
      timer = setTimeout(doFunc, delay)
    }
  }

3.2:setTimeout 的最短延迟时间

因为如果设置的 timeout 小于 0,则设置为 0,如果嵌套的层级超过了 5 层(计时器嵌套),并且 timeout 小于 4ms,则设置 timeout 为 4ms。并且,在不同浏览器中出现这种最小延迟的情况有所不同

那么有没有什么好的处理方案呢?


四、完整实现方案:

  1. 管理器集成
  • 使用 Map 结构全局管理定时器实例
  • 支持通过唯一ID进行精准控制
  • 自动垃圾回收机制防止内存泄漏

// 全局定时器管理中心
const timerManager = new Map<string, { stop: () => void }>();

function safeScheduler(callback: () => void, delay: number, id?: string) {
  let expected = Date.now() + delay;
  let timerId: ReturnType<typeof setTimeout>;

  const controller = {
    stop: () => {
      clearTimeout(timerId);
      if (id) timerManager.delete(id);
    }
  };

  const timeoutHandler = () => {
    const drift = Date.now() - expected;
    callback();
    
    expected += delay;
    timerId = setTimeout(timeoutHandler, Math.max(0, delay - drift));
  };

  timerId = setTimeout(timeoutHandler, delay);
  if (id) timerManager.set(id, controller);

  return controller;
}

// 全局清除方法
function clearSafeScheduler(id: string) {
  const timer = timerManager.get(id);
  timer?.stop();
}

// 清除所有定时器
function clearAllSchedulers() {
  timerManager.forEach(timer => timer.stop());
  timerManager.clear();
}

调用方法:

// 启动带标识的定时器
const searchTimer = safeScheduler(
  () => console.log('Search refresh'),
  1000,
  'search-api'
);

// 通过ID清除单个定时器
clearSafeScheduler('search-api');

// 紧急停止所有定时器
clearAllSchedulers();

vue组件集成:

// 在组件中
export default defineComponent({
  mounted() {
    this.autoRefresh = safeScheduler(this.fetchData, 5000, 'comp-refresh');
  },
  
  beforeUnmount() {
    clearSafeScheduler('comp-refresh');
  }
})

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值