Skip to main content

JS setTimeout & setInterval & requestAnimationFrame

· 6 min read

JavaScript 定时调度 大核心 API

  1. setTimeout:延迟执行一次,推荐使用,可灵活控制调度
  2. setInterval:周期重复执行,不推荐使用,存在设计缺陷
  3. requestAnimationFrame:与屏幕刷新同步,动画场景首选

最佳实践:用递归 setTimeout 替代 setInterval,保证间隔准确、支持动态调整。

零延迟调度:setTimeout(fn, 0) 不是立即执行,而是当前同步任务完成后尽快执行,可用于拆分长任务。

关键提醒:定时器回调中的 this 指向全局对象;务必清理不再需要的定时器防止内存泄漏。


setTimeout 基础

基本语法

const timerId = setTimeout(callback, delay, ...args);
  • callback:延迟执行的函数
  • delay:延迟毫秒数(默认 0)
  • ...args:传递给回调的参数
Live Editor
function Demo() {
  function handleClick() {
    setTimeout(() => {
      alert('3 秒到了!');
    }, 3000);
  }
  
  return <button onClick={handleClick}>3 秒后弹出提示</button>;
}
Result
Loading...

取消调度

clearTimeout(timerId);
Live Editor
function Demo() {
  const [timerId, setTimerId] = useState(null);
  
  function start() {
    const id = setTimeout(() => {
      alert('你取消了,我不会出现');
    }, 5000);
    setTimerId(id);
  }
  
  function cancel() {
    clearTimeout(timerId);
    alert('已取消定时器');
  }
  
  return (
    <>
      <button onClick={start}>5 秒后弹出</button>
      <button onClick={cancel} style={{marginLeft: '10px'}}>取消</button>
    </>
  );
}
Result
Loading...

setInterval 基础

基本语法

const timerId = setInterval(callback, delay, ...args);

停止周期执行

clearInterval(timerId);
Live Editor
function Demo() {
  const [count, setCount] = useState(0);
  const [timerId, setTimerId] = useState(null);
  
  function start() {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    setTimerId(id);
  }
  
  function stop() {
    clearInterval(timerId);
  }
  
  return (
    <>
      <div>计时器: {count}</div>
      <button onClick={start}>开始</button>
      <button onClick={stop} style={{marginLeft: '10px'}}>停止</button>
    </>
  );
}
Result
Loading...
caution

技术上 clearTimeoutclearInterval 可以互换,但永远不要混用


setInterval 的致命缺陷

问题:时间间隔不准确

如果回调执行时间超过设定的 delay,两次执行之间没有间隔

预期: |-----|-----|-----|
实际: |========|========|
执行 执行

问题:任务积压

如果页面卡顿或切换到后台,回调会被挂起,恢复后一次性执行所有积压任务


解决方案:递归 setTimeout

原理

在上一次执行完成后,才调度下一次。

function tick() {
console.log('执行任务');
setTimeout(tick, 1000); // 完成后再调度下一次
}

setTimeout(tick, 1000);

优势

  1. 间隔准确:两次执行开始之间的间隔固定
  2. 灵活可调:可根据上一次执行结果动态调整延迟
  3. 无积压:任务不会堆积

动态调整延迟示例

let delay = 1000;

function fetchData() {
fetch('/api/data')
.then(res => res.json())
.then(data => {
if (data.load > 80) {
delay = 3000; // 服务器压力大,降低请求频率
}
setTimeout(fetchData, delay);
});
}

setTimeout(fetchData, delay);
tip
记住:永远优先使用递归 setTimeout 替代 setInterval!

setTimeout(fn, 0) 的真正含义

不是"立即执行",而是"尽快执行"

setTimeout(() => console.log('B'), 0);
console.log('A');

// 输出: A, B

事件循环中的位置

同步任务 → 微任务 → 渲染 → 宏任务(setTimeout)

应用场景 1:拆分长任务

// ❌ 阻塞 UI 1 秒
for (let i = 0; i < 1000000000; i++) {
heavyCalculation();
}
// ✅ 分批次执行,不阻塞 UI
let i = 0;
const CHUNK = 10000;

function process() {
const end = Math.min(i + CHUNK, 1000000000);
while (i < end) {
heavyCalculation();
i++;
}
if (i < 1000000000) {
setTimeout(process, 0);
}
}

process();

应用场景 2:让浏览器先渲染

// ❌ 用户看不到"计算中..."提示
button.onclick = function() {
status.textContent = '计算中...';
heavyCalculation(); // 阻塞 3 秒
status.textContent = '完成';
};
// ✅ 用户能看到提示
button.onclick = function() {
status.textContent = '计算中...';
setTimeout(() => {
heavyCalculation();
status.textContent = '完成';
}, 0);
};

requestAnimationFrame

为什么需要它?

setTimeout/setInterval 不与屏幕刷新同步:

屏幕刷新: | | | | |
定时器: | | | | | |

导致动画不流畅、掉帧、耗电。

语法

const id = requestAnimationFrame(callback);
cancelAnimationFrame(id);

优势

  • 与屏幕刷新频率同步(通常 60fps = 每 16.7ms 一次)
  • 页面后台时自动暂停,节省 CPU
  • 浏览器内部优化,动画更流畅
Live Editor
function SmoothAnimation() {
  const [x, setX] = useState(0);
  const [animId, setAnimId] = useState(null);
  
  function animate() {
    setX(prev => {
      if (prev >= 300) {
        cancelAnimationFrame(animId);
        return 300;
      }
      return prev + 2;
    });
    setAnimId(requestAnimationFrame(animate));
  }
  
  return (
    <>
      <button onClick={animate}>开始平滑动画</button>
      <div style={{
        width: '50px',
        height: '50px',
        background: 'var(--ifm-color-primary)',
        borderRadius: '8px',
        transform: `translateX(${x}px)`,
        marginTop: '10px'
      }} />
    </>
  );
}
Result
Loading...

进阶细节

最小延迟限制

浏览器对嵌套超过 5 层的 setTimeout 强制 4ms 最小延迟

Live Editor
function Demo() {
  const [delays, setDelays] = useState([]);
  
  function test() {
    const times = [];
    let prev = Date.now();
    let start = prev;
    
    function tick() {
      const now = Date.now();
      times.push(now - prev);
      prev = now;
      
      if (now - start < 100) {
        setTimeout(tick, 0);
      } else {
        setDelays(times.join(', '));
      }
    }
    
    setTimeout(tick, 0);
  }
  
  return (
    <>
      <button onClick={test}>测试嵌套延迟</button>
      <div style={{marginTop: '10px'}}>每次调用间隔 (ms): {delays}</div>
    </>
  );
}
Result
Loading...

观察:前 4 次 < 4ms,之后 ≥ 4ms。

this 指向问题

const obj = {
value: 42,
print() {
console.log(this.value);
}
};

setTimeout(obj.print, 1000); // 输出 undefined,不是 42!

原因:setTimeout 调用时丢失了 this 上下文。

修复方案

// 方案 1:箭头函数
setTimeout(() => obj.print(), 1000);

// 方案 2:bind
setTimeout(obj.print.bind(obj), 1000);
caution

即使在严格模式下,setTimeout 回调中的 this 仍然是 window(或 global),不是 undefined!

垃圾回收

定时器会持有回调函数的引用,防止被垃圾回收。

永远记得取消不再需要的定时器,否则会内存泄漏!

// ❌ 组件卸载后定时器仍在运行
useEffect(() => {
setInterval(() => {
console.log('每秒执行');
}, 1000);
}, []);

// ✅ 正确:清理函数
useEffect(() => {
const timer = setInterval(() => {
console.log('每秒执行');
}, 1000);

return () => clearInterval(timer); // 组件卸载时清除
}, []);

总结

特性setTimeoutsetIntervalrequestAnimationFrame
执行次数一次多次每次调度一次
精度受 4ms 限制受函数执行时间影响与刷新同步
后台运行降速降速暂停
推荐场景延迟执行、轮询不推荐动画、视觉更新

核心建议

  1. 需要动画 → 用 requestAnimationFrame
  2. 需要周期执行 → 用 递归 setTimeout
  3. setInterval 几乎没有合理的使用场景

参考资料

  1. Scheduling: setTimeout and setInterval - javascript.info
  2. HTML Standard - Timers