JavaScript防抖与节流全解析
文章目录
- 前言:为什么需要防抖和节流
- 基本概念与区别
- 防抖(Debounce)
- 节流(Throttle)
- 关键区别
- 防抖(Debounce)详解
- 1. 基本防抖函数实现
- 2. 防抖函数的使用
- 3. 防抖函数的工作流程
- 4. 防抖函数进阶 - 立即执行选项
- 节流(Throttle)详解
- 1. 基本节流函数实现
- 时间戳法(第一次会立即执行)
- 定时器法(第一次会延迟执行)
- 2. 节流函数的使用
- 3. 节流函数的工作流程
- 4. 节流函数进阶 - 首尾调用控制
- 实际应用场景
- 1. 搜索框输入防抖
- 2. 滚动加载节流
- 3. 按钮点击防抖(防止重复提交)
- 4. 窗口调整大小节流
- 防抖与节流的进阶实现
- 1. 可取消的防抖函数
- 2. 可取消的节流函数
- 3. 带返回值的防抖函数(使用Promise)
- 常见问题与解决方案
- 1. 防抖函数内无法访问this和事件对象
- 2. React组件中使用防抖/节流
- 3. 函数依赖项变化时重置防抖/节流
- 第三方库的实现
- 1. Lodash
- 2. Underscore
- 3. RxJS
- 总结与最佳实践
- 1. 选择合适的技术
- 2. 性能考虑
- 3. 最佳实践
- 4. 回顾关键概念
- 2. 性能考虑
- 3. 最佳实践
- 4. 回顾关键概念
前言:为什么需要防抖和节流
在前端开发中,我们经常会遇到一些高频触发的事件,例如:
- 浏览器窗口调整大小(resize)
- 页面滚动(scroll)
- 鼠标移动(mousemove)
- 键盘输入(keyup、keydown)
- 频繁点击按钮等
如果不加控制,这些事件处理函数可能会在短时间内被频繁调用,导致以下问题:
- 性能问题:函数频繁执行,特别是复杂计算或DOM操作,会导致页面卡顿
- 资源浪费:例如输入搜索时,频繁发送不必要的API请求
- 不良用户体验:例如按钮重复点击导致的重复提交表单
看一个具体例子:
// 不做任何处理的搜索输入框
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', function() {// 每次输入都会执行,即使用户正在快速输入console.log('执行搜索:', this.value);fetchSearchResults(this.value); // 发送请求获取搜索结果
});
在上面的例子中,当用户快速输入"JavaScript"这个词时,可能会依次触发以下请求:
- 搜索"J"
- 搜索"Ja"
- 搜索"Jav"
- 搜索"Java"
- 搜索"JavaS"
- …
- 搜索"JavaScript"
显然,除了最后一个请求,前面的所有请求都是不必要的,这不仅浪费了网络资源,还可能导致服务器压力过大。
这就是为什么我们需要防抖和节流技术:它们帮助我们控制函数的执行频率,优化性能和用户体验。
基本概念与区别
防抖(Debounce)
概念:函数防抖是指在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。
形象比喻:电梯关门 - 当有人进入电梯后,电梯会等待一段时间再关门,如果在这段时间内又有人进入,电梯会重新计时等待,直到一段时间内没有人进入才会关门。
典型场景:
- 搜索框输入,等用户输入完毕后再发送请求
- 窗口调整大小完成后执行重排重绘
- 按钮提交事件防止重复提交
节流(Throttle)
概念:函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
形象比喻:水龙头控制水流 - 无论你如何快速地多次拧开水龙头,水流速度都不会超过水管的限制。
典型场景:
- 滚动事件处理
- 射击游戏中的武器发射频率限制
- 鼠标移动事件处理
关键区别
特性 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
执行时机 | 在一段时间内没有再次触发事件后执行 | 在一段时间内只执行一次 |
适用场景 | 需要等待操作完全结束后执行 | 需要保持一定的执行频率 |
执行频率 | 不稳定,取决于事件触发频率和间隔 | 稳定,保证一定时间内执行一次 |
最后一次是否执行 | 延迟执行,一定会执行 | 可能不会执行最后一次(取决于实现) |
下面通过可视化图表来理解两者的区别:
连续事件触发:
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
│ │ │ │ │ │ │ │ │ │
0 1 2 3 4 5 6 7 8 9 (时间轴)防抖(延迟3秒):
─────────────────────→ (只在最后一次事件后等待3秒执行)↓9+3=12节流(间隔3秒):
↓ ↓ ↓ (每隔3秒执行一次)
│ │ │
0 3 6 (时间轴)
防抖(Debounce)详解
1. 基本防抖函数实现
/*** 基础版防抖函数* @param {Function} func - 需要防抖的函数* @param {number} wait - 等待时间,单位毫秒* @returns {Function} - 防抖处理后的函数*/
function debounce(func, wait) {let timeout;return function() {const context = this; // 保存this指向const args = arguments; // 保存传入的参数// 清除之前的定时器clearTimeout(timeout);// 设置新的定时器timeout = setTimeout(function() {func.apply(context, args);}, wait);};
}
2. 防抖函数的使用
// 定义一个可能频繁调用的函数
function handleSearch(searchTerm) {console.log('Searching for:', searchTerm);// 发送API请求等操作...
}// 获取输入框元素
const searchInput = document.getElementById('search-input');// 未使用防抖的版本 - 每次输入都会执行
/*
searchInput.addEventListener('input', function() {handleSearch(this.value);
});
*/// 使用防抖后的版本 - 停止输入300毫秒后才执行
const debouncedSearch = debounce(function() {handleSearch(this.value);
}, 300);searchInput.addEventListener('input', debouncedSearch);
3. 防抖函数的工作流程
以搜索框为例,当用户连续输入"hello"这个词:
时间轴: 0ms 100ms 200ms 300ms 400ms 700ms
操作: h e l l o (停止输入)
函数调用: 无 无 无 无 无 执行搜索"hello"
每次按键都会重置定时器,只有当用户停止输入300ms后,才会执行一次搜索。
4. 防抖函数进阶 - 立即执行选项
在某些场景下,我们可能希望第一次触发事件时立即执行函数,然后等待一段时间再允许执行下一次。例如点击提交按钮时,我们希望立即响应第一次点击,然后暂时禁用后续点击。
/*** 带立即执行选项的防抖函数* @param {Function} func - 需要防抖的函数* @param {number} wait - 等待时间,单位毫秒* @param {boolean} immediate - 是否立即执行* @returns {Function} - 防抖处理后的函数*/
function debounce(func, wait, immediate) {let timeout;return function() {const context = this;const args = arguments;const later = function() {timeout = null;if (!immediate) func.apply(context, args);};const callNow = immediate && !timeout;clearTimeout(timeout);timeout = setTimeout(later, wait);if (callNow) func.apply(context, args);};
}// 使用立即执行的防抖 - 第一次点击立即响应,后续点击被防抖
const button = document.getElementById('submit-button');
const immediateDebounceClick = debounce(function() {console.log('Button clicked!');// 提交表单等操作...
}, 1000, true);button.addEventListener('click', immediateDebounceClick);
节流(Throttle)详解
1. 基本节流函数实现
有两种常见的方式实现节流函数:时间戳法和定时器法。
时间戳法(第一次会立即执行)
/*** 时间戳实现的节流函数* @param {Function} func - 需要节流的函数* @param {number} wait - 等待时间,单位毫秒* @returns {Function} - 节流处理后的函数*/
function throttle(func, wait) {let previous = 0; // 上一次执行的时间戳return function() {const now = Date.now(); // 当前时间戳const context = this;const args = arguments;// 如果当前时间与上一次执行时间差大于等待时间if (now - previous > wait) {func.apply(context, args);previous = now;}};
}
定时器法(第一次会延迟执行)