手写防抖与节流函数深度解析
一句话概括
防抖(debounce)在高频事件停止后才执行一次,节流(throttle)让高频事件每隔固定时间最多执行一次——两者都是用”时间换性能”的思路,核心靠闭包保存定时器/时间戳来实现。面试手写这两道题,基础版 5 行搞定,但面试官真正想看的是你对 this 绑定、立即执行、取消机制这些边角的处理。
1
2
3
4
5
6
7
// 防抖:用户停止输入 500ms 后才发请求
const search = debounce(val => fetch(`/api?q=${val}`), 500);
input.oninput = e => search(e.target.value);
// 节流:滚动时每 200ms 最多执行一次
const scroll = throttle(() => updateUI(window.scrollY), 200);
window.onscroll = scroll;
核心知识点
1. 防抖基础版 —— 定时器的清除与重设
每次触发都”重置倒计时”,只有最后一次触发后安静了 delay 毫秒才真正执行。闭包里只存一个 timer,没有别的花样。
1
2
3
4
5
6
7
8
9
10
11
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer); // 清掉上次的倒计时
timer = setTimeout(() => fn.apply(this, args), delay); // 重新计时
};
}
// 验证:快速连续点击按钮,只有最后一次点击 300ms 后触发
const btn = document.querySelector('#btn');
btn.onclick = debounce(() => console.log('clicked!'), 300);
2. 节流-时间戳版 —— 首次立即执行,末次可能丢失
记下”上次执行时间”,每次触发时算差值。够间隔就执行,不够就跳过。特点是第一次必定立即执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, interval) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
fn.apply(this, args);
last = now;
}
};
}
// 验证:连续滚动,每 200ms 打印一次位置
window.onscroll = throttle(() => console.log(window.scrollY), 200);
3. 节流-定时器版 —— 末次一定执行,首次延迟
换一个思路:用定时器是否存在来卡节奏。有定时器说明”还在冷却中”,直接跳过。搞定之后 timer 置空,开门迎接下一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, interval) {
let timer = null;
return function (...args) {
if (timer) return; // 冷却中,跳过
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; // 冷却结束
}, interval);
};
}
// 验证:输出"尾部"的滚动位置(与时间戳版的差异请自己多滚几次感受一下)
window.onscroll = throttle(() => console.log('tail:', window.scrollY), 200);
4. 完整版防抖 —— immediate + cancel
面试官让你写完全版,其实就是在基础版上多两个零件:① immediate 让首次触发立即执行;② .cancel() 方法在组件卸载时兜底。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function debounce(fn, delay, immediate = false) {
let timer = null;
const debounced = function (...args) {
const callNow = immediate && !timer; // 首次且不在冷却 → 立即执行
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args); // 普通模式:延迟执行
}, delay);
if (callNow) fn.apply(this, args); // immediate 模式:立即执行
};
debounced.cancel = () => {
clearTimeout(timer);
timer = null;
};
return debounced;
}
// 用法
const handler = debounce(submitForm, 1000, true); // 立即执行 + 1s 内防重复
// 组件卸载:handler.cancel()
5. React 中使用防抖/节流 —— useRef 锁定引用
React 组件每次渲染都会重新执行函数体。如果你在组件里直接 const fn = debounce(callback, 300),每次渲染都拿到一个全新的防抖函数,各自带独立的 timer——防抖直接废掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRef, useCallback } from 'react';
function SearchBox() {
// ❌ 错误:const search = debounce(fetchResults, 300); —— 每次 render 新建,防抖失效
const searchRef = useRef(debounce(fetchResults, 300));
// 组件卸载时取消残留定时器
useEffect(() => () => searchRef.current.cancel(), []);
return <input onChange={e => searchRef.current(e.target.value)} />;
}
// 或者用 useCallback + 空依赖(前提是 fn 不依赖 state,否则需要用 ref 存最新 state)
function SubmitButton({ onSubmit }) {
const handle = useCallback(throttle(onSubmit, 1000), []);
return <button onClick={handle}>提交</button>;
}
关键点:useRef 返回同一个对象引用,生命周期内不变;useCallback(fn, []) 同理。只要防抖/节流函数实例稳定,闭包里的 timer 就能正常工作。
其实你每天都在用
- 搜索框输入联想:百度、淘宝的搜索框,你每敲一个字不会立刻发请求,而是等你停下来 300-500ms 再查——标准防抖
- 窗口 resize 重新布局:拖拽浏览器窗口时,resize 事件一秒能触发几十次,节流到 200ms 一次足以
- 滚到底自动加载更多:监听
scroll判断是否接近底部,节流后每 200ms 检测一次,避免无意义计算 - 按钮防重复提交:登录、支付按钮用
immediate防抖,首次点击立刻执行,1s 内连点无效 - lodash 的
_.debounce/_.throttle:你 npm install 的这两个,底层就是上面这些逻辑,只是多了一些边界处理
常见误解
❌ 误区一:”节流就是间隔执行,防抖就是延时执行——就这点区别”
这句话只对了一半。面试时更关键的区别是防抖关注”最后一次”(等用户操作完成),节流关注”均匀频率”(持续操作中保证最低响应密度)。理解了这层,你才能回答”搜索框用防抖,滚动监听用节流”的 why,而不是死记硬背。
❌ 误区二:”手写防抖不用管 this,直接用 fn(…args) 就行”
在 btn.onclick = debounce(handler, 300) 场景下,如果不写 fn.apply(this, args),handler 里的 this 会变成 window(或严格模式的 undefined),拿不到按钮元素。面试官盯着这行代码看的时候,就是在 etc 你有没有处理 this 的意识。
❌ 误区三:”React 里用防抖,把 debounce 写在组件外面就行”
写在外面确实只创建一次实例,但如果你的回调依赖组件的 state 或 props,闭包捕获的是创建时的”过期值”。换个组件实例呢?大家共用一个 debounce 实例、一个 timer,A 组件连续输入把 B 组件的定时器清了——完全乱了。正确做法是用 useRef 存实例,再用一个 useRef 存最新的回调来解决闭包陈旧问题。
❌ 误区四:”时间戳版节流和定时器版节流差不多,写哪个都行”
时间戳版首次立即执行、末次可能丢失,定时器版首次延迟执行、末次一定触发。如果你做滚动吸顶效果需要”第一时间响应”,时间戳版合适;如果你做”收集用户最后一次停留位置”,定时器版合适。面试时说出这个区别比背出两个版本更值钱。
一句话总结
防抖 = 等安静再执行(清除定时器 + 重设定时器),节流 = 按频率执行(比较时间戳 或 用定时器做冷却标记)。手写关键三要素:闭包存状态、
apply绑this、cancel兜底卸载。React 用useRef锁实例。面试先出基础版再展示完整版,比一口气写全更显思路。