JS 节流(Throttle)与防抖(Debounce)详解
JS 节流(Throttle)与防抖(Debounce)详解
文章目录
- JS 节流(Throttle)与防抖(Debounce)详解
- 一、核心概念对比
- 二、防抖(Debounce)实现与应用
- 1. 基础实现
- 2. 立即执行版本
- 3. 典型应用场景
- 三、节流(Throttle)实现与应用
- 1. 时间戳版(立即执行)
- 2. 定时器版(延迟执行)
- 3. 综合版(首尾都执行)
- 4. 典型应用场景
- 四、可视化执行过程
- 防抖执行流程(延迟500ms)
- 节流执行流程(间隔500ms)
- 五、Lodash 最佳实践
- 1. 防抖使用
- 2. 节流使用
- 六、性能优化建议
- 七、常见问题解答
- Q1:防抖和节流如何选择?
- Q2:为什么我的防抖函数没有立即执行?
- Q3:移动端和PC端的延迟设置是否应该不同?
- 八、手写实现测试题
- 实现一个带取消功能的防抖函数
一、核心概念对比
特性 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
定义 | 连续触发事件时,只在最后执行一次 | 连续触发事件时,固定间隔执行一次 |
执行时机 | 停止触发后的延迟时间到达 | 按照固定时间间隔执行 |
类比 | 电梯关门(最后一个人进入后关门) | 水龙头滴水(固定频率滴水) |
典型场景 | 搜索框输入、窗口resize | 滚动事件、高频点击 |
二、防抖(Debounce)实现与应用
1. 基础实现
function debounce(fn, delay) {let timer = null;return function(...args) {clearTimeout(timer); // 清除之前的定时器timer = setTimeout(() => {fn.apply(this, args);}, delay);};
}
2. 立即执行版本
function debounceImmediate(fn, delay, immediate = true) {let timer = null;return function(...args) {if (timer) clearTimeout(timer);if (immediate && !timer) {fn.apply(this, args);}timer = setTimeout(() => {timer = null;if (!immediate) {fn.apply(this, args);}}, delay);};
}
3. 典型应用场景
// 搜索框输入
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(() => {console.log('发送搜索请求:', searchInput.value);
}, 500));// 窗口resize
window.addEventListener('resize', debounce(() => {console.log('窗口大小调整完成');
}, 200));
三、节流(Throttle)实现与应用
1. 时间戳版(立即执行)
function throttle(fn, delay) {let lastTime = 0;return function(...args) {const now = Date.now();if (now - lastTime >= delay) {fn.apply(this, args);lastTime = now;}};
}
2. 定时器版(延迟执行)
function throttleTimer(fn, delay) {let timer = null;return function(...args) {if (!timer) {timer = setTimeout(() => {fn.apply(this, args);timer = null;}, delay);}};
}
3. 综合版(首尾都执行)
function throttleFull(fn, delay) {let timer = null;let lastTime = 0;return function(...args) {const now = Date.now();const remaining = delay - (now - lastTime);if (remaining <= 0) {if (timer) {clearTimeout(timer);timer = null;}fn.apply(this, args);lastTime = now;} else if (!timer) {timer = setTimeout(() => {fn.apply(this, args);timer = null;lastTime = Date.now();}, remaining);}};
}
4. 典型应用场景
// 滚动事件
window.addEventListener('scroll', throttle(() => {console.log('处理滚动逻辑');
}, 200));// 高频按钮点击
const btn = document.getElementById('submit');
btn.addEventListener('click', throttle(() => {console.log('提交表单');
}, 1000));
四、可视化执行过程
防抖执行流程(延迟500ms)
事件触发: |-----|-----|-----|-----|-----|
函数执行: |_________|(最后一次触发后500ms执行)
节流执行流程(间隔500ms)
事件触发: |-----|-----|-----|-----|-----|
函数执行: |_____|_____|_____|(固定每500ms执行一次)
五、Lodash 最佳实践
1. 防抖使用
import { debounce } from 'lodash';// 基本用法
const handleSearch = debounce(searchAPI, 300);// 带选项配置
const saveEditor = debounce(autoSave, 1000, {leading: true, // 首次立即执行trailing: false // 不执行尾部调用
});
2. 节流使用
import { throttle } from 'lodash';// 基本用法
const handleScroll = throttle(updatePosition, 200);// 带选项配置
const recordTime = throttle(savePlayTime, 1000, {leading: false, // 首次不立即执行trailing: true // 执行尾部调用
});
六、性能优化建议
-
合理设置延迟时间:
- 防抖:输入类建议300-500ms,resize建议100-200ms
- 节流:滚动建议100-200ms,动画建议16.7ms(60fps)
-
内存管理:
// 组件销毁时取消防抖/节流 const debouncedFn = debounce(fn, 300); window.addEventListener('resize', debouncedFn);// 组件卸载时 window.removeEventListener('resize', debouncedFn); debouncedFn.cancel(); // Lodash提供的取消方法
-
RAF节流(适合动画场景):
function throttleRAF(fn) {// 标记变量,用于判断是否已经有动画帧回调在等待执行let ticking = false;/*** 节流处理后的函数* @param {...any} args - 传递给原始函数的参数*/return function(...args) {// 如果没有正在等待执行的动画帧回调if (!ticking) {// 请求浏览器在下一次重绘前执行回调requestAnimationFrame(() => {// 执行原始函数,并保持正确的 this 上下文和参数fn.apply(this, args);// 执行完成后重置标记ticking = false;});// 设置标记,表示已经有动画帧回调在等待ticking = true;}}; }
七、常见问题解答
Q1:防抖和节流如何选择?
- 需要最终状态用防抖(如搜索建议)
- 需要过程状态用节流(如滚动加载)
Q2:为什么我的防抖函数没有立即执行?
检查是否设置了leading: false
(Lodash)或未实现立即执行逻辑
Q3:移动端和PC端的延迟设置是否应该不同?
是的,移动端建议适当增加延迟(如PC用300ms,移动端用500ms)
八、手写实现测试题
实现一个带取消功能的防抖函数
function cancellableDebounce(fn, delay) {// 存储定时器ID的变量,初始为nulllet timer = null;/*** 防抖处理后的函数* @param {...any} args - 传递给原始函数的参数*/const debounced = function(...args) {// 每次调用先清除之前的定时器clearTimeout(timer);// 设置新的定时器timer = setTimeout(() => {// 延迟结束后,使用正确this和参数执行原始函数fn.apply(this, args);}, delay);};/*** 取消当前等待执行的防抖函数*/debounced.cancel = function() {// 清除定时器clearTimeout(timer);// 重置定时器变量timer = null;};// 返回包装后的防抖函数return debounced;
}{// 延迟结束后,使用正确this和参数执行原始函数fn.apply(this, args);}, delay);};/*** 取消当前等待执行的防抖函数*/debounced.cancel = function() {// 清除定时器clearTimeout(timer);// 重置定时器变量timer = null;};// 返回包装后的防抖函数return debounced;
}