讲解

浏览器提供两组定时器:setTimeout(fn, ms) 延迟执行一次,setInterval(fn, ms) 每隔一段时间重复执行;对应的清除方法是 clearTimeout 和 clearInterval,参数是创建定时器时返回的 ID。定时器回调属于宏任务,事件循环空闲时才执行——所以延时是「至少这么久」,页面繁忙时会推迟,setTimeout(fn, 0) 也不是立即执行(见事件循环一章)。

setInterval 有个隐患:回调执行时间超过间隔时,任务会堆积或节奏混乱,做轮询更稳的写法是「链式 setTimeout」——在上一次任务完成后再排下一次,保证间隔从任务结束算起。清除定时器是容易被遗忘的基本功:组件销毁、页面切换时不清,定时器会带着闭包里的引用一直跑,既费资源又可能造成内存泄漏和「幽灵更新」(操作已不存在的元素)。

定时器的两大应用模式是防抖(debounce)和节流(throttle):防抖把密集触发合并成最后一次执行(搜索框输入停止 300ms 后才发请求);节流让高频事件按固定节奏执行(滚动监听每 200ms 处理一次)。两者都只有十几行代码,是性能优化的基础武器,也是面试高频题。

示例

// 一次性延时
const timerId = setTimeout(() => {
  console.log('1 秒后执行');
}, 1000);
clearTimeout(timerId); // 清除后回调不会再执行

// 重复执行 + 计数停止
let count = 0;
const intervalId = setInterval(() => {
  count++;
  console.log('第', count, '次');
  if (count >= 3) clearInterval(intervalId);
}, 500);

// 链式 setTimeout 轮询:间隔从任务结束算起,不会堆积
function poll() {
  setTimeout(async () => {
    await checkStatus(); // 任务可能耗时
    poll(); // 完成后再排下一次
  }, 2000);
}
// 防抖:停止触发 delay 毫秒后才执行
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
const onInput = debounce((e) => console.log('搜索:', e.target.value), 300);
document.querySelector('#search').addEventListener('input', onInput);

// 节流:固定节奏执行,间隔内的触发被丢弃
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}
window.addEventListener(
  'scroll',
  throttle(() => console.log('滚动位置:', window.scrollY), 200),
);

常见坑

  • 以为延时精确:setTimeout(fn, 1000) 是「至少 1 秒后」,主线程繁忙时会明显推迟,动画计时别用定时器数帧(用 requestAnimationFrame)。
  • 忘记清除:组件卸载后定时器还在跑,回调里操作已销毁的 DOM 或发起请求,造成报错和泄漏。清理和创建要成对出现。
  • setInterval 任务堆积:回调耗时超过间隔会导致节奏紊乱,轮询用链式 setTimeout。
  • 定时器里丢失 this:setTimeout(function(){ this.x }, 100) 中 this 是默认绑定,用箭头函数或 bind。

小结

setTimeout 一次性、setInterval 周期性,都要记得 clear;轮询用链式 setTimeout;防抖合并、节流限速是高频性能手段。下一章:本地存储。