setTimeout 模拟实现 setInterval

使用 setTimeout 递归调用的方式来模拟 setInterval 的功能,并解决 setInterval 的一些潜在问题

问题

setInterval 存在一些问题:

  1. 如果回调函数执行时间过长,可能导致多个回调堆积执行
  2. 无法保证每次执行的时间间隔完全准确
  3. 页面不可见时仍会继续执行,浪费资源

我们需要用 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 方法的对象,提供清除定时器的能力
  • 避免堆积问题:在回调执行完成后才设置下一次定时器,确保不会因为回调执行时间过长而导致多个回调堆积
  • 闭包保存状态:利用闭包保存 timerIdisCleared 状态,确保每个定时器实例独立
  • 灵活性扩展:可以添加立即执行、传递参数等功能,使其更加灵活实用
  • 与原生差异setTimeout 实现的版本会等待上一次回调执行完毕,而原生 setInterval 不会等待,这在某些场景下是优势