讲解
浏览器提供两组定时器: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;防抖合并、节流限速是高频性能手段。下一章:本地存储。