setTimeout 模拟 setInterval

用 setTimeout 递归实现 setInterval 的功能

问题

使用 setTimeout 模拟 setInterval 的功能。

解答

基础实现

function mySetInterval(fn, delay) {
  let timerId = null;

  function loop() {
    fn();
    // 执行完成后再设置下一次定时
    timerId = setTimeout(loop, delay);
  }

  // 启动第一次定时
  timerId = setTimeout(loop, delay);

  // 返回取消方法
  return {
    clear() {
      clearTimeout(timerId);
    }
  };
}

// 使用示例
const timer = mySetInterval(() => {
  console.log('执行时间:', new Date().toLocaleTimeString());
}, 1000);

// 5 秒后停止
setTimeout(() => {
  timer.clear();
  console.log('已停止');
}, 5000);

支持传参的完整版本

function mySetInterval(fn, delay, ...args) {
  let timerId = null;
  let isRunning = true;

  function loop() {
    if (!isRunning) return;
    fn.apply(this, args);
    timerId = setTimeout(loop, delay);
  }

  timerId = setTimeout(loop, delay);

  // 返回一个类似 timerId 的对象
  return {
    clear() {
      isRunning = false;
      clearTimeout(timerId);
    }
  };
}

// 使用示例
const timer = mySetInterval((name) => {
  console.log(`Hello, ${name}!`);
}, 1000, 'World');

为什么要用 setTimeout 模拟?

setInterval 存在任务堆积问题:

// setInterval 的问题
let count = 0;
setInterval(() => {
  count++;
  console.log(`第 ${count} 次执行`);
  
  // 模拟耗时操作(超过间隔时间)
  const start = Date.now();
  while (Date.now() - start < 2000) {}
}, 1000);

// 任务会堆积,因为 setInterval 不管上一次是否执行完
// setTimeout 模拟可以避免堆积
function safeInterval(fn, delay) {
  let timerId = null;

  function loop() {
    fn(); // 等 fn 执行完
    timerId = setTimeout(loop, delay); // 才设置下一次
  }

  timerId = setTimeout(loop, delay);

  return {
    clear: () => clearTimeout(timerId)
  };
}

关键点

  • setTimeout 递归调用自身实现循环
  • 在回调执行完成后再设置下一次定时,避免任务堆积
  • 需要保存 timerId 以支持取消功能
  • setInterval 不等待回调完成就计时,可能导致回调堆积
  • 实际项目中推荐用 setTimeout 模拟,执行时机更可控