前端一面:防抖、节流与高频事件优化
“防抖/节流”已经几乎是前端一面的固定题了。这一篇用几个常见高频事件为例,讲讲这两个概念的直觉、实现方式,以及面试里常见的变体题和参考答案。
高频事件的问题:scroll、resize、input、mousemove
在浏览器里,有很多事件可以在极短时间内被触发多次:
scroll/wheel:滚动时可能每帧触发多次;resize:窗口大小调整时连续触发;input/keyup:快速输入时每个按键都触发;mousemove:鼠标移动时频繁触发。
如果在这些事件的回调中直接做 DOM 操作、复杂计算或网络请求,很容易导致:
- 页面卡顿、掉帧;
- 无意义的重复请求;
- 不必要的布局/重绘。
防抖和节流,就是两种常见的“减频”手段。
防抖(Debounce):只在“最后一次”触发后执行
直觉:
不断开会的路人甲,只要他还在说话,就先别记笔记;等他停下来一段时间,再一次性记。
实现思路:
- 在每次事件触发时,取消之前的定时器,重新开启一个新的;
- 如果在设定时间内没有新的触发,就执行真正的回调。
典型应用场景:
- 搜索框输入联想:用户停止输入 300ms 后再发请求;
- 窗口大小调整结束后再做一次布局计算。
节流(Throttle):在一段时间内最多执行一次
直觉:
红绿灯限流:在固定时间窗口内,最多只允许一批车通过。
实现思路:
- 基于时间戳:记录上次执行时间,如果本次触发距离上次执行不足指定间隔,就忽略;
- 基于定时器:如果当前没有定时器,就在第一次触发时开启一个,等定时器到期后再允许下一次执行。
典型应用场景:
- 滚动时实时更新某些轻量 UI(如返回顶部按钮的显隐);
- 鼠标移动时的提示跟随,不需要每一个像素都重新计算。
常见手写:简单版本的防抖和节流
防抖函数(简化版)
1function debounce(fn, delay) {
2 let timer = null;
3 return function (...args) {
4 const ctx = this;
5 clearTimeout(timer);
6 timer = setTimeout(() => {
7 fn.apply(ctx, args);
8 }, delay);
9 };
10}
要点:
- 用闭包保存
timer; - 每次触发都先清理上一次的定时器,再重新启动;
- 使用
apply保留原始this和参数。
节流函数(时间戳版本)
1function throttle(fn, interval) {
2 let lastTime = 0;
3 return function (...args) {
4 const now = Date.now();
5 if (now - lastTime >= interval) {
6 lastTime = now;
7 fn.apply(this, args);
8 }
9 };
10}
要点:
- 通过
lastTime控制“多久执行一次”; - 在某些场景下首尾是否执行,可以通过初始化
lastTime或补一个定时器版本来调整。
常见面试题与参考答案
题 1:什么是防抖?什么是节流?各自适用于哪些场景?
参考答案要点:
- 防抖:在事件高频触发的情况下,只在“最后一次触发结束后的 N 毫秒”执行回调;
- 适合搜索输入、窗口 resize 等场景;
- 节流:保证在一段时间内回调最多执行一次,忽略其间重复触发;
- 适合 scroll、mousemove 等场景。
可以用一句直觉性的类比加深印象:
- 防抖 = 等人说完话再记笔记;
- 节流 = 红绿灯控流,每隔一段时间放一批车。
题 2:如何用代码实现一个防抖函数?
可以给出前面的 debounce 简化实现,并解释关键点:
- 使用闭包保存
timer; - 调用时先
clearTimeout再setTimeout; - 延时结束再执行真正的回调。
如果想多加一分,可以提到“立即执行版防抖”(leading edge):
- 第一次触发立即执行,后续在冷却时间内不再触发;
- 可以通过一个布尔标志和两个分支来实现。
题 3:如何用代码实现一个节流函数?时间戳和定时器版本有什么区别?
可以给出时间戳版本的 throttle,并补充说明:
- 时间戳版:适合需要立即响应第一次触发的场景;
- 定时器版:适合希望在最后一次触发后也能再执行一次的场景;
- 实际工程里可以组合两者,做到“头尾都能执行,同时中间控制频率”。
例如:
1function throttleBoth(fn, interval) {
2 let lastTime = 0;
3 let timer = null;
4
5 return function (...args) {
6 const now = Date.now();
7 const remaining = interval - (now - lastTime);
8 const ctx = this;
9
10 if (remaining <= 0) {
11 if (timer) {
12 clearTimeout(timer);
13 timer = null;
14 }
15 lastTime = now;
16 fn.apply(ctx, args);
17 } else if (!timer) {
18 timer = setTimeout(() => {
19 lastTime = Date.now();
20 timer = null;
21 fn.apply(ctx, args);
22 }, remaining);
23 }
24 };
25}
在一面环境下,不一定要写到这么完整,但如果你能把“什么时候用防抖/节流、核心实现思路、常见变体”讲顺,已经算是这一块的高分回答了。