setTimeout 模拟 setInterval 不怕堆积

FreeGuideOnline 最新 2026-07-04

javascript function mySetInterval(callback, delay) { function loop() { callback(); setTimeout(loop, delay); } setTimeout(loop, delay); }


与 `setInterval` 的关键区别:

- `setTimeout` 的每次延迟都是在上一次执行**结束之后**才开始计时。
- 即使回调执行时间超过 delay,下一次调用也只会在当前回调完成后等待 delay 再触发,绝不会堆积。
- 任务队列中同一时间只会存在一个定时器回调,内存友好。

## 基础实现:不会堆积的定时器

下面是一个完整的可清理的模拟实现:

```javascript
function createInterval(fn, delay) {
  let timerId = null;

  function start() {
    const loop = () => {
      fn();
      timerId = setTimeout(loop, delay);
    };
    timerId = setTimeout(loop, delay);
  }

  function clear() {
    if (timerId) {
      clearTimeout(timerId);
      timerId = null;
    }
  }

  return { start, clear };
}

// 使用示例
const { start, clear } = createInterval(() => {
  console.log('执行任务', new Date().toISOString());
}, 1000);

start(); // 开始模拟 interval
// clear(); // 停止

这个实现返回一个对象,包含 startclear 方法,用法与 setInterval / clearInterval 类似,但永远不会堆积。

进阶:支持动态延迟和立即调用

有时我们需要根据条件动态调整下一次延迟,或者希望第一次调用立即执行。可以这样扩展:

function createAdvancedInterval(fn, getDelay) {
  let timerId = null;
  let stopped = false;

  function run(immediate = false) {
    if (stopped) return;

    if (immediate) {
      fn();
    }

    const next = () => {
      if (stopped) return;
      fn();
      const delay = typeof getDelay === 'function' ? getDelay() : getDelay;
      timerId = setTimeout(next, delay);
    };

    // 如果不是立即执行,则第一次延迟后调用
    const initialDelay = immediate ? (typeof getDelay === 'function' ? getDelay() : getDelay) : (typeof getDelay === 'function' ? getDelay() : getDelay);
    timerId = setTimeout(next, immediate ? 0 : initialDelay);
  }

  function stop() {
    stopped = true;
    clearTimeout(timerId);
  }

  return { run, stop };
}

// 示例:延迟逐渐增加
let delay = 100;
const { run, stop } = createAdvancedInterval(() => {
  console.log('动态延迟', delay);
}, () => delay += 50);

run(true); // 立即执行第一次,后续每次调用后延迟+50ms

与 setInterval 的行为对比实验

用一个耗时任务来模拟真实场景,观察两种方案的区别:

// 模拟一个耗时操作(200ms)
function heavyTask(id) {
  const start = Date.now();
  while (Date.now() - start < 200) {} // 占用线程200ms
  console.log(`任务 ${id} 完成`);
}

// 方案A:setInterval(堆积版)
let countA = 0;
const intervalId = setInterval(() => {
  countA++;
  heavyTask(countA);
  if (countA >= 5) clearInterval(intervalId);
}, 100);

// 方案B:setTimeout 模拟(防堆积版)
let countB = 0;
function startSimulated() {
  const loop = () => {
    countB++;
    heavyTask(countB);
    if (countB < 5) {
      setTimeout(loop, 100);
    }
  };
  setTimeout(loop, 100);
}
startSimulated();

实际输出对比:

  • setInterval 版:任务间隔混乱,出现连续执行,可能前几个任务几乎同时完成。
  • setTimeout 版:每个任务之间始终保持至少 100ms 的间隔,顺序规整。

常见误区与注意事项

  1. 忘记清理:模拟 interval 仍然返回 timer ID,需要及时 clearTimeout,否则可能造成内存泄漏。
  2. 异步回调中的 this 绑定:在对象方法中使用时注意 this 指向,可以使用箭头函数或 .bind(this)
  3. 与事件循环的关系:即使使用 setTimeout 模拟,回调仍然在宏任务队列中,仍需注意长时间占用主线程会阻塞 UI。
  4. 时间精度:浏览器的定时器最小延迟约为 4ms(非激活标签页甚至更长),模拟 interval 的间隔也会受此限制,但不会影响防堆积特性。

实际应用场景

轮询接口

防止请求堆积导致服务器压力过大,保证上一次请求完成后才发起下一次。

let isPolling = false;
function poll() {
  fetch('/api/status')
    .then(res => res.json())
    .then(data => {
      // 处理数据
      if (!data.finished && !isPolling) {
        setTimeout(poll, 2000);
      }
    });
}
poll();

动画帧控制

代替 setInterval 驱动动画循环,避免掉帧和回调堆积。

节流型定时任务

需要固定间隔执行,但绝不能并发的场景(如日志上报、数据同步)。

完整封装工具函数

可以直接用于生产环境的稳健版本:

function safeInterval(fn, delay, ...args) {
  let timerId = null;
  let stopped = false;

  const execute = () => {
    if (stopped) return;
    fn(...args);
    timerId = setTimeout(execute, delay);
  };

  timerId = setTimeout(execute, delay);

  return () => {
    stopped = true;
    clearTimeout(timerId);
  };
}

// 使用
const clear = safeInterval((msg) => {
  console.log(msg, new Date());
}, 1000, 'hello');

// 5秒后终止
setTimeout(clear, 5000);