防抖与节流:原理与实战

防抖与节流:原理与实战

滚动、输入、窗口缩放等事件都可能在极短时间内触发多次。如果在每次触发时都执行复杂逻辑(如请求接口、重排布局、复杂计算),会造成卡顿与性能浪费。

“防抖(debounce)”与“节流(throttle)”是应对这类高频事件的两种常见手段。本文主要讲清楚:

  • 防抖与节流的核心区别是什么?
  • 如何手写通用的 debounce / throttle 工具函数?
  • 在实际业务中,各自适合什么场景?

一、高频事件带来的问题

典型高频事件:

  • scroll(滚动)
  • resize(窗口大小变化)
  • mousemove / pointermove
  • keyup / keydown / input

问题:

  • 事件触发频率可能高达几十甚至上百次/秒
  • 若每次都执行 DOM 操作、计算或发请求,会:
    • 增大 CPU 占用
    • 导致掉帧、卡顿
    • 给后端带来不必要压力

这时就需要:控制函数的触发频率


二、防抖(Debounce):只在“最后一次”执行

1. 概念

防抖:在一段连续的高频触发中,只在“最后一次触发后的 N 毫秒”才真正执行回调。
若在等待期间又触发了一次,则重新计时。

可以类比电梯:

  • 人不停进电梯,电梯一直等
  • 当一段时间内没人再进电梯,电梯才关门运行

2. 手写防抖函数

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
27
28
29
30
31
32
33
34
35
36
37
38
39
function debounce(fn, delay = 300, options = {}) {
let timer = null;
const { leading = false, trailing = true } = options;
let lastArgs;
let lastThis;
let invoked = false;

function invoke() {
fn.apply(lastThis, lastArgs);
invoked = true;
}

const debounced = function (...args) {
lastArgs = args;
lastThis = this;

if (timer) clearTimeout(timer);

if (leading && !invoked) {
invoke();
}

timer = setTimeout(() => {
if (trailing && (!leading || invoked)) {
invoke();
}
timer = null;
invoked = false;
}, delay);
};

debounced.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
invoked = false;
};

return debounced;
}

3. 使用场景

适用于“只需在用户行为结束后执行一次”的场景:

  • 输入框搜索联想(停止输入 Nms 后发请求)
  • 窗口大小改变后重新布局(停止拖拽窗口后计算)
  • 表单实时校验(输入稳定后校验)
1
2
3
4
5
const handleSearch = debounce((value) => {
// 发起请求
}, 300);

input.addEventListener("input", (e) => handleSearch(e.target.value));

三、节流(Throttle):每隔一段时间最多执行一次

1. 概念

节流:在连续触发的过程中,保证在每个时间窗口内,回调最多只执行一次。

可以类比空调压缩机:

  • 不会每一秒都启动/停止,而是一定时间内控制一次,避免过度频繁切换。

2. 时间戳版节流

1
2
3
4
5
6
7
8
9
10
11
function throttle(fn, wait = 300) {
let lastTime = 0;

return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
fn.apply(this, args);
}
};
}

特点:

  • 触发时立即执行一次(leading = true)
  • 最后一次触发之后,可能不会再执行(trailing = false)

3. 定时器版节流

1
2
3
4
5
6
7
8
9
10
11
function throttleTimer(fn, wait = 300) {
let timer = null;

return function (...args) {
if (timer) return;
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, wait);
};
}

特点:

  • 第一次触发不会立刻执行(leading = false)
  • 在触发结束后还能留有一次执行(trailing = true)

4. 综合版节流(支持 leading / trailing)

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
27
28
29
30
31
32
33
34
35
36
37
38
39
function throttle(fn, wait = 300, options = {}) {
let timer = null;
let lastTime = 0;
const { leading = true, trailing = true } = options;

const throttled = function (...args) {
const now = Date.now();

if (!lastTime && !leading) {
lastTime = now;
}

const remaining = wait - (now - lastTime);
const context = this;

if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(context, args);
} else if (!timer && trailing) {
timer = setTimeout(() => {
lastTime = leading ? Date.now() : 0;
timer = null;
fn.apply(context, args);
}, remaining);
}
};

throttled.cancel = () => {
if (timer) clearTimeout(timer);
timer = null;
lastTime = 0;
};

return throttled;
}

5. 使用场景

适用于“持续反馈,但要控制频率”的场景:

  • 滚动监听(如滚动加载、吸顶导航)
  • 页面 resize 时实时展示尺寸(但不必每次都更新)
  • 拖拽过程中的计算与渲染
1
2
3
4
5
const onScroll = throttle(() => {
console.log(window.scrollY);
}, 200);

window.addEventListener("scroll", onScroll, { passive: true });

四、防抖 vs 节流:如何选择?

用一句话概括:

  • 防抖:一段时间内多次触发,只在“最后一次结束后”执行(如搜索输入)
  • 节流:一段时间内多次触发,按固定节奏间隔执行(如滚动、拖拽)

1. 行为对比图(文字版)

  • 防抖(300ms):
    • 用户停止操作 300ms 后执行一次
    • 期间再次触发会重置计时
  • 节流(300ms):
    • 无论用户多频繁触发,每 300ms 最多执行一次

2. 常见选择建议

  • 输入框搜索、表单实时校验 → 防抖(减少无效请求)
  • 滚动监听、页面 resize → 节流(持续反馈,但不过于频繁)
  • 如果既想有第一次的“即时响应”,又想在结束后再执行一次,可以结合 leading/trailing 选项调整。

五、在框架中的使用(以 React/Vue 为例)

1. React 中使用防抖/节流

可配合 useCallbackuseRef 封装,或使用现成库(如 lodash)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useMemo } from "react";
import { debounce } from "lodash-es";

function SearchInput() {
const debouncedSearch = useMemo(
() => debounce((value) => { /* 请求 */ }, 300),
[]
);

return (
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="搜索..."
/>
);
}

2. Vue 中使用

可在 setup 中使用 ref 保存防抖/节流函数,或直接在 methods 中绑定。

1
2
3
4
5
6
7
8
9
10
11
import { debounce } from "lodash-es";

export default {
setup() {
const handleInput = debounce((value) => {
// 请求
}, 300);

return { handleInput };
},
};

六、工程实践建议

  1. 把 debounce / throttle 封装成通用工具,避免每个组件手写一遍。
  2. 使用第三方库时,要注意:
    • 在组件卸载时调用 cancel(),避免内存泄漏或在已卸载组件上 setState。
  3. 对滚动监听等高频事件,配合 passive: true 提升性能。
  4. 根据业务 UX 需求合理选择:
    • “输入完再触发” → 防抖
    • “持续反馈” → 节流

七、总结

防抖与节流是前端性能优化中最基础、最常用的两种手段:

  • 防抖(Debounce):高频触发中,只在结束后等待一段时间再执行一次。
  • 节流(Throttle):高频触发中,按照固定时间间隔执行。

掌握它们的原理与手写实现,不仅能写出更优雅、高性能的交互逻辑,也能在面试中从容应对相关考点。


防抖与节流:原理与实战
https://sunjc.vip/2025/03/02/防抖与节流原理与实战/
作者
Sunjc
发布于
2025年3月2日
许可协议