005-防抖与节流概念-场景-实现

防抖与节流:原理、实现与应用场景

防抖(Debounce)

核心原理

在事件被触发 n 秒后再执行回调,若在 n 秒内事件再次被触发,则重新计时
确保函数只在最后一次触发后执行一次

使用场景

场景 说明
搜索框输入 用户停止输入后再发送请求,避免频繁请求
窗口大小调整 窗口调整结束后再计算布局,避免重复渲染
表单验证 用户输入完成后再进行验证,减少实时验证开销
按钮防重 防止用户快速重复点击导致多次提交
实时保存 文档编辑时,停止输入一段时间后自动保存

基础实现

1
2
3
4
5
6
7
8
9
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

增强版(支持立即执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function debounce(fn, delay, immediate = false) {
let timer = null;
return function(...args) {
clearTimeout(timer);

// 立即执行模式
if (immediate && !timer) {
fn.apply(this, args);
}

timer = setTimeout(() => {
// 非立即执行模式
if (!immediate) {
fn.apply(this, args);
}
timer = null;
}, delay);
};
}

应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 搜索框防抖
const searchInput = document.getElementById('search');
const handleSearch = debounce(query => {
console.log(`搜索: ${query}`);
// 实际发送搜索请求...
}, 500);

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

// 窗口调整防抖
const handleResize = debounce(() => {
console.log('窗口调整完成');
// 重新计算布局...
}, 300);

window.addEventListener('resize', handleResize);

节流(Throttle)

核心原理

在单位时间内,无论事件触发多少次,只执行一次回调函数
确保函数以固定频率执行

使用场景

场景 说明
滚动事件 滚动页面时触发加载更多内容,避免高频执行
鼠标移动 跟踪鼠标位置,限制触发频率
游戏操作 限制角色移动或攻击频率
高频点击 限制按钮点击频率,防止误操作
动画渲染 保证动画帧率稳定,避免过度渲染

时间戳实现

1
2
3
4
5
6
7
8
9
10
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}

定时器实现(支持尾执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function throttle(fn, interval) {
let timer = null;
let lastTime = 0;

return function(...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);

clearTimeout(timer);

// 达到执行间隔
if (remaining <= 0) {
fn.apply(this, args);
lastTime = now;
}
// 设置尾执行
else {
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
}, remaining);
}
};
}

应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 滚动加载节流
let page = 1;
const loadMore = throttle(() => {
const scrollBottom = document.documentElement.scrollHeight -
window.innerHeight -
window.scrollY;

if (scrollBottom < 500) {
console.log(`加载第 ${page} 页数据`);
// 实际加载数据...
page++;
}
}, 500);

window.addEventListener('scroll', loadMore);

// 游戏攻击节流
const playerAttack = throttle(() => {
console.log('玩家发动攻击');
// 实际攻击逻辑...
}, 1000);

attackButton.addEventListener('click', playerAttack);

对比总结

特性 防抖 节流
核心思想 延迟执行,只执行最后一次 固定频率执行
执行时机 停止触发后执行 间隔时间内执行
执行次数 多次触发只执行一次 多次触发按固定频率执行
实现关键 setTimeout + clearTimeout 时间戳或定时器
适用场景 输入类事件 高频触发事件
用户感知 响应最后操作结果 保持操作流畅性

可视化对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
graph LR
A[事件触发] --> B{防抖模式}
A --> C{节流模式}

B --> D[触发1]
D --> E[重置计时]
E --> F[触发2]
F --> G[重置计时]
G --> H[触发3]
H --> I[执行回调]

C --> J[触发1-立即执行]
J --> K[冷却期]
K --> L[触发2-忽略]
L --> M[冷却结束]
M --> N[触发3-执行]

现代开发实践

使用 Lodash 实现

1
2
3
4
5
6
7
8
9
10
11
import { debounce, throttle } from 'lodash';

// 防抖
const searchHandler = debounce(query => {
// 搜索逻辑
}, 500);

// 节流
const scrollHandler = throttle(() => {
// 滚动处理
}, 300);

React Hooks 封装

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
// 防抖Hook
function useDebounce(callback, delay) {
const callbackRef = useRef(callback);

useEffect(() => {
callbackRef.current = callback;
});

return useMemo(() => {
const handler = (...args) => {
clearTimeout(handler.timeout);
handler.timeout = setTimeout(() => {
callbackRef.current(...args);
}, delay);
};
return handler;
}, [delay]);
}

// 节流Hook
function useThrottle(callback, interval) {
const callbackRef = useRef(callback);
const lastTimeRef = useRef(0);

useEffect(() => {
callbackRef.current = callback;
});

return useCallback((...args) => {
const now = Date.now();
if (now - lastTimeRef.current >= interval) {
callbackRef.current(...args);
lastTimeRef.current = now;
}
}, [interval]);
}

性能优化建议

  1. 合理设置延迟时间:防抖250-500ms,节流100-300ms
  2. 避免过度使用:只在必要时应用
  3. 及时清理:组件卸载时清除定时器
  4. 结合RAF:动画场景使用requestAnimationFrame
1
2
3
4
5
6
7
8
9
10
11
12
function rafThrottle(fn) {
let ticking = false;
return function(...args) {
if (!ticking) {
requestAnimationFrame(() => {
fn.apply(this, args);
ticking = false;
});
ticking = true;
}
};
}