setInterval可能的坑
1. setInterval会无视代码错误继续执行
setInterval
会无视代码的错误。就算遇到了错误,它还是会一直循环下去,不会停止。这就导致了可能你代码里存在着一些问题(比如你的代码可能有个一定概率下会发生的错误,而你使用setinterval来循环调用它,由于setInterval
不会因为报错停止,所以这个问题可能被隐藏),可是却很难发现。
let count = 1;
setInterval(function () {count++;console.log(count);if (count % 3 === 0) throw new Error('setInterval报错');
}, 1000)
2. setInterval会无视任何情况下定时执行
比如说,我们要实现一个功能,每隔一段时间要向服务器发送请求来查看是否有新数据。此时,若当时用户的网络状态很糟糕,客户端收到请求响应的时间大于interval循环的时间。而setInterval会无视任何情况下定时执行,这就会导致了用户的客户端里充斥着axios请求。这不仅会浪费网络资源,还可能导致服务器过载。
2.1. 解决方法
使用 setTimeout
替代 setInterval
来避免请求堆积,当用户发出去的请求得到响应或者超时后,再使用setTimeout
递归发送下一个请求。
2.2. 示例
使用setInterval
时,若服务器响应时间超过 5 秒,新请求会不断发送,导致请求堆积。
setInterval(() => {fetch('https://api.example.com/check-new-data').then(response => response.json()).then(data => {console.log('新数据:', data);}).catch(error => {console.error('请求失败:', error);});
}, 5000);
优化:使用 setTimeout
递归调用。
每次请求完成后(无论是成功还是失败)再设置下一次请求。
const intervalTime = 5000; // 每次请求的间隔时间function fetchData() {fetch('https://api.example.com/check-new-data').then(response => response.json()).then(data => {console.log('新数据:', data);// 请求成功后,设置下一次请求setTimeout(fetchData, intervalTime);}).catch(error => {console.error('请求失败:', error);// 请求失败后,设置下一次请求setTimeout(fetchData, intervalTime);});
}// 启动第一次请求
fetchData();
3. setInterval可能会让内部函数不能完全调用
若setInterval
的间隔时长 < 小于内部调用函数的执行时长,可能会导致定时器内部的函数不能完全调用。
如果说你的代码执行时间会比较久的话,就会导致setInterval
中的一部分函数调用被略过。因此程序如果依赖于setInterval
的精确执行的话,那么就要小心这一点了。
当然,其实setTimeout
也有这个问题。浏览器的定时器都不是精确执行的。就算你调用setTimeout(fn,0),
它也不能确保马上执行。
4. setTimeout和setInterval都不能精确执行(时间不准)
存在以下四种可能性会导致setTimeout/
setInterval
无法精确执行
4.1. JS是单线程的
JavaScript 是单线程的,意味着代码是顺序执行的。当调用 setTimeout
时,它会将回调函数加入到事件队列中,等待当前线程空闲后执行。因此,如果当前线程有其他任务在执行(比如正在处理一个大的循环或复杂的操作),setTimeout
的回调会延迟执行,从而导致超时不准确。
基于上面的情况下,还有下面的另一种影响
JavaScript 的执行是基于事件循环(Event Loop)的。setTimeout
并不立即执行,而是将回调函数放入事件队列中,直到当前执行栈清空。所以即使设定了精确的延迟,回调的执行时间也可能因为事件循环的队列处理和任务调度的顺序而有所不同。
也可以理解为设置setTimeout
后的执行时间 = 执行栈清空的时间 + duration
。
4.2. 最小延迟时间
在一些浏览器中,setTimeout
的最小延迟时间可能并不是你设置的值。根据不同的环境(特别是在浏览器中),延迟时间可能会被浏览器限制,通常会有一个最小值,通常是 4 毫秒(但在一些浏览器中可能更长)。比如,如果你设置了 setTimeout(func, 1),
可能实际上会延迟 4 毫秒或更长。
当然也有另外的说法是在五级嵌套的情况下才会有这个四毫秒的延时。
4.3. 系统和硬件的影响
不同操作系统和硬件的性能差异也会影响 setTimeout
的精确度。如果系统负载较高,或者操作系统有时间调度策略,setTimeout
可能会稍微延迟执行。
4.4. 失活页面
失活页面(例如,用户切换到其他标签页或者浏览器处于后台时)会影响 setTimeout
和其他定时器的精度
页面不再是活动页面时。浏览器会减少在后台标签页中执行 JavaScript 代码的频率,以节省资源和提升性能
当页面处于非活动状态时,浏览器会延迟执行计时器回调,甚至可能会完全忽略一些定时器,直到页面恢复活动。
4.5. 解决方法
4.5.1. 使用递归 setTimeout
模式+动态设置delay延迟时间
为了避免因为系统延迟或事件队列的影响,使得每次 setTimeout
的实际延迟时间都略微偏离预期,可以通过setTimeout
递归的方式动态调整下次的超时执行时间。也就是说,每次回调完成后,通过递归调用 setTimeout,
同时根据当前时间动态调整下次的超时。
function preciseTimeout(callback, interval) {const startTime = Date.now(); // 获取当前时间function loop() {const elapsedTime = Date.now() - startTime; // 计算过去的时间const delay = Math.max(0, interval - (elapsedTime % interval)); // 计算下次的延迟时间setTimeout(function() {callback();loop(); // 递归调用,以保持精度}, delay);}loop();
}// 示例:每 100 毫秒执行一次任务
preciseTimeout(() => {console.log('执行任务', Date.now());
}, 100);
4.5.2. 使用requestAnimationFrame
(可以用于高精度动画)
使用 requestAnimationFrame
来替代 setTimeout
。
requestAnimationFrame
会在浏览器的每一帧渲染之前调用,这样可以确保任务的执行和屏幕的刷新同步。
function preciseAnimation(callback) {let lastTime = 0;function animate(timestamp) {const deltaTime = timestamp - lastTime; // 获取当前时间和上次执行的时间差if (deltaTime >= 100) { // 每 100 毫秒执行一次callback();lastTime = timestamp;}requestAnimationFrame(animate); // 请求下一帧}requestAnimationFrame(animate);
}// 示例:每 100 毫秒执行一次任务
preciseAnimation(() => {console.log('执行任务', Date.now());
});
requestAnimationFrame 更适合用于动画和精确的时间控制,因为它会自动适应屏幕刷新率,通常是 60 FPS,因此会比setTimeout
更平滑且精确。
虽然requestAnimationFrame 不会收到事件循环的影响,也不受失活页面的影响,但是如果系统运行电脑卡了,会影响到渲染帧,其他页面的卡死也会影响到渲染帧,使得不精准。
4.5.3. 增加多次检查
如果任务执行的时机非常重要,可以通过多次检查和校正延迟来提高精度。通过设定一个非常短的时间间隔并多次调整,可以使误差减少。
例如,假设你每 10 毫秒检查一次时间,然后精确调整任务执行的时机:
function preciseCheck(callback, interval) {const targetTime = Date.now() + interval; // 设定目标时间function check() {if (Date.now() >= targetTime) {callback(); // 执行任务} else {setTimeout(check, Math.max(0, targetTime - Date.now())); // 调整等待时间}}check();
}// 示例:精确地每 100 毫秒执行一次任务
preciseCheck(() => {console.log('执行任务', Date.now());
}, 100);
在这个方法中,我们通过每次检查是否已经达到目标时间,并调整下次检查的超时,来确保任务尽量按预期执行。适用于需要高精度控制时间间隔的任务,特别是对于不太依赖 CPU 密集型操作的任务
4.5.4. 使用Web Workers
在主线程忙碌的时候,多开一个线程来执行计时,不会受到渲染帧和主线程的影响,是比较适合的方案。
// 在主线程中创建一个 Worker
const worker = new Worker('worker.js');// 在 Worker 中执行复杂任务
worker.postMessage('start');worker.onmessage = function(e) {console.log('Worker 回传消息:', e.data);
};// worker.js
onmessage = function() {// 执行复杂的计算或任务setInterval(() => {postMessage('任务完成');}, 100); // 使用 setInterval,Web Worker 中执行任务不会阻塞主线程
};