setTimeout 模拟实现 setInterval
使用 setTimeout 递归调用的方式来模拟 setInterval 的功能,并解决 setInterval 的一些潜在问题
问题
setInterval 存在一些问题:
- 如果回调函数执行时间过长,可能导致多个回调堆积执行
- 无法保证每次执行的时间间隔完全准确
- 页面不可见时仍会继续执行,浪费资源
我们需要用 setTimeout 来模拟实现一个更可控的 setInterval,并提供清除定时器的功能。
解答
/**
* 使用 setTimeout 模拟实现 setInterval
* @param {Function} callback - 要执行的回调函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Object} 返回包含 clear 方法的对象,用于清除定时器
*/
function mySetInterval(callback, delay) {
let timerId = null;
let isCleared = false;
// 递归执行的函数
function run() {
if (isCleared) return;
// 执行回调函数
callback();
// 继续设置下一次定时器
timerId = setTimeout(run, delay);
}
// 首次执行
timerId = setTimeout(run, delay);
// 返回清除定时器的方法
return {
clear: function() {
isCleared = true;
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
};
}
/**
* 改进版:支持立即执行和传递参数
* @param {Function} callback - 要执行的回调函数
* @param {number} delay - 延迟时间(毫秒)
* @param {boolean} immediate - 是否立即执行第一次
* @returns {Object} 返回包含 clear 方法的对象
*/
function mySetIntervalAdvanced(callback, delay, immediate = false) {
let timerId = null;
let isCleared = false;
function run() {
if (isCleared) return;
callback();
// 等待回调执行完毕后再设置下一次定时器
// 这样可以避免回调执行时间过长导致的堆积问题
timerId = setTimeout(run, delay);
}
// 如果需要立即执行
if (immediate) {
callback();
timerId = setTimeout(run, delay);
} else {
timerId = setTimeout(run, delay);
}
return {
clear: function() {
isCleared = true;
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
};
}
使用示例
// 示例1:基础使用
let count = 0;
const timer1 = mySetInterval(() => {
count++;
console.log(`执行第 ${count} 次`);
// 执行5次后停止
if (count >= 5) {
timer1.clear();
console.log('定时器已清除');
}
}, 1000);
// 示例2:立即执行版本
let count2 = 0;
const timer2 = mySetIntervalAdvanced(() => {
count2++;
console.log(`立即执行版本:第 ${count2} 次`);
if (count2 >= 3) {
timer2.clear();
}
}, 1000, true); // 第三个参数为 true,立即执行
// 示例3:模拟异步操作
const timer3 = mySetInterval(async () => {
console.log('开始异步任务...');
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 500));
console.log('异步任务完成');
}, 2000);
// 5秒后清除
setTimeout(() => {
timer3.clear();
console.log('timer3 已清除');
}, 5000);
// 示例4:对比原生 setInterval
console.log('=== 对比测试 ===');
// 原生 setInterval
const nativeTimer = setInterval(() => {
console.log('原生 setInterval 执行');
}, 1000);
// 自定义实现
const customTimer = mySetInterval(() => {
console.log('自定义 mySetInterval 执行');
}, 1000);
// 3秒后全部清除
setTimeout(() => {
clearInterval(nativeTimer);
customTimer.clear();
console.log('所有定时器已清除');
}, 3000);
关键点
- 递归调用:使用
setTimeout递归调用自身,每次执行完回调后再设置下一次定时器 - 状态标记:使用
isCleared标记来控制定时器是否已被清除,避免清除后继续执行 - 返回清除方法:返回包含
clear方法的对象,提供清除定时器的能力 - 避免堆积问题:在回调执行完成后才设置下一次定时器,确保不会因为回调执行时间过长而导致多个回调堆积
- 闭包保存状态:利用闭包保存
timerId和isCleared状态,确保每个定时器实例独立 - 灵活性扩展:可以添加立即执行、传递参数等功能,使其更加灵活实用
- 与原生差异:
setTimeout实现的版本会等待上一次回调执行完毕,而原生setInterval不会等待,这在某些场景下是优势
目录