好的,各位老铁,咱们今天来聊聊前端性能优化里两个“老生常谈”但又“举足轻重”的家伙:Debounce(防抖)和 Throttle(节流)。 别看名字挺唬人,其实原理简单得就像你早上吃的那根油条,只不过要稍微加工一下,让它更符合现代社会“既要快,又要省”的要求。
开场白:为啥需要 Debounce 和 Throttle?
想象一下,你正在开发一个搜索框,用户每输入一个字,就要向服务器发送一次请求,获取搜索结果。如果用户输入速度很快,比如 1 秒钟敲了 5 个字,那就要发送 5 次请求。这不仅浪费服务器资源,还会让用户觉得卡顿。
再比如,你正在监听 window
的 resize
事件,每次窗口大小改变,都要重新计算页面布局。如果用户拖动窗口的速度很快,resize
事件就会被频繁触发,导致页面卡顿。
这些都是典型的高频事件,如果不加以控制,就会严重影响用户体验。Debounce 和 Throttle 就是用来解决这些问题的两把利器。
第一回合:Debounce(防抖)—— “你尽管来,我只认最后一次”
Debounce 的核心思想是:当事件被触发后,延迟一段时间执行回调函数。如果在延迟时间内再次触发事件,则重新计时。只有当延迟时间内没有再次触发事件,才会执行回调函数。
简单来说,Debounce 就像一个“守门员”,只有在一段时间内没有新的请求进来,才会放行最后一个请求。
1. 简单实现:
function debounce(func, delay) {
let timerId;
return function(...args) {
// 清除之前的定时器
clearTimeout(timerId);
// 创建一个新的定时器
timerId = setTimeout(() => {
func.apply(this, args); // 执行回调函数
}, delay);
};
}
代码解释:
debounce(func, delay)
:接受两个参数,func
是要执行的回调函数,delay
是延迟的时间(毫秒)。timerId
:用来保存定时器的 ID。return function(...args)
:返回一个新的函数,这个函数才是真正被调用的函数。...args
是ES6语法,将所有参数收集到一个数组中。clearTimeout(timerId)
:每次调用返回的函数时,都会先清除之前的定时器。setTimeout(() => { ... }, delay)
:创建一个新的定时器,延迟delay
毫秒后执行回调函数。func.apply(this, args)
:执行回调函数,this
指向调用返回的函数的上下文,args
是传递给回调函数的参数。
2. 使用场景:
- 搜索框输入: 在用户停止输入一段时间后,才发送搜索请求。
- 窗口 resize: 在窗口大小调整完毕后,才重新计算页面布局。
- 按钮点击: 防止用户快速点击按钮,导致重复提交。
3. 示例代码:
<input type="text" id="search-input">
<div id="search-result"></div>
<script>
const searchInput = document.getElementById('search-input');
const searchResult = document.getElementById('search-result');
function search(query) {
// 模拟搜索请求
console.log(`Sending search request for: ${query}`);
searchResult.textContent = `Searching for: ${query}...`;
setTimeout(() => {
searchResult.textContent = `Results for: ${query}: [Result 1, Result 2, Result 3]`;
}, 500); // 模拟延迟
}
const debouncedSearch = debounce(search, 300); // 延迟 300 毫秒
searchInput.addEventListener('input', (event) => {
debouncedSearch(event.target.value);
});
</script>
代码解释:
- 当用户在
search-input
中输入时,会触发input
事件。 debouncedSearch
函数会在用户停止输入 300 毫秒后执行。- 如果用户在 300 毫秒内再次输入,
debouncedSearch
函数会重新计时。
4. 带立即执行的 Debounce:
有时候,我们希望在第一次触发事件时立即执行回调函数,然后在延迟时间内忽略后续的触发。
function debounce(func, delay, immediate) {
let timerId;
return function(...args) {
const context = this; // 保存 this 上下文
const callNow = immediate && !timerId; // 是否立即执行
clearTimeout(timerId);
timerId = setTimeout(() => {
timerId = null; // 重置 timerId
if (!immediate) {
func.apply(context, args); // 延迟执行
}
}, delay);
if (callNow) {
func.apply(context, args); // 立即执行
}
};
}
代码解释:
immediate
:一个布尔值,表示是否立即执行回调函数。callNow
:一个布尔值,表示是否应该立即执行回调函数。只有当immediate
为true
且timerId
为null
时,才应该立即执行。timerId = null;
:在定时器执行完毕后,需要将timerId
重置为null
,以便下次可以立即执行。context = this;
保存 this 上下文,避免在setTimeout中this指向window
第二回合:Throttle(节流)—— “你来一次,我就让你歇一会”
Throttle 的核心思想是:在一段时间内,只允许回调函数执行一次。
简单来说,Throttle 就像一个“水龙头”,即使你一直拧着,也只能在一段时间内流出一定量的水。
1. 简单实现:
function throttle(func, delay) {
let lastCalled = 0; // 上次执行回调函数的时间
return function(...args) {
const now = Date.now(); // 当前时间
if (now - lastCalled >= delay) {
func.apply(this, args); // 执行回调函数
lastCalled = now; // 更新上次执行回调函数的时间
}
};
}
代码解释:
throttle(func, delay)
:接受两个参数,func
是要执行的回调函数,delay
是时间间隔(毫秒)。lastCalled
:用来保存上次执行回调函数的时间。return function(...args)
:返回一个新的函数,这个函数才是真正被调用的函数。now - lastCalled >= delay
:判断当前时间与上次执行回调函数的时间间隔是否大于等于delay
。func.apply(this, args)
:执行回调函数。lastCalled = now
:更新上次执行回调函数的时间。
2. 使用场景:
- 滚动事件: 在滚动过程中,每隔一段时间执行一次回调函数。
- 鼠标移动事件: 在鼠标移动过程中,每隔一段时间执行一次回调函数。
- 游戏中的射击频率: 限制玩家的射击频率。
3. 示例代码:
<div id="scrollable-area" style="height: 200px; overflow: auto;">
<div style="height: 1000px;">Scroll me!</div>
</div>
<script>
const scrollableArea = document.getElementById('scrollable-area');
function handleScroll() {
console.log('Scrolling...');
}
const throttledScroll = throttle(handleScroll, 200); // 每 200 毫秒执行一次
scrollableArea.addEventListener('scroll', throttledScroll);
</script>
代码解释:
- 当用户滚动
scrollable-area
时,会触发scroll
事件。 throttledScroll
函数会每 200 毫秒执行一次。- 即使用户滚动速度很快,
handleScroll
函数也不会被频繁执行。
4. 使用定时器的 Throttle:
除了使用时间戳,还可以使用定时器来实现 Throttle。
function throttle(func, delay) {
let timerId = null;
return function(...args) {
const context = this;
if (!timerId) {
timerId = setTimeout(() => {
func.apply(context, args);
timerId = null; // 重置 timerId
}, delay);
}
};
}
代码解释:
timerId
:用来保存定时器的 ID。if (!timerId)
:判断是否已经存在定时器。如果不存在,则创建一个新的定时器。timerId = null;
:在定时器执行完毕后,需要将timerId
重置为null
,以便下次可以创建新的定时器。
Debounce vs Throttle:傻傻分不清楚?
特性 | Debounce (防抖) | Throttle (节流) |
---|---|---|
执行时机 | 在一段时间内没有再次触发事件后执行 | 在一段时间内,只允许执行一次 |
适用场景 | 搜索框输入、窗口 resize、按钮点击 | 滚动事件、鼠标移动事件、游戏中的射击频率 |
目标 | 减少不必要的执行,避免资源浪费 | 限制执行频率,防止页面卡顿 |
形象比喻 | “你尽管来,我只认最后一次” | “你来一次,我就让你歇一会” |
触发事件后 | 延迟执行,如果在延迟时间内再次触发,则重新计时 | 立即执行(或在delay时间之后),后续触发会被忽略直到下一个delay时间段 |
最终结果 | 只执行一次(最后一次) | 在一段时间内最多执行一次 |
总结:
- Debounce: 适用于只需要最后一次结果的场景。
- Throttle: 适用于需要限制执行频率的场景。
进阶:Lodash 的 Debounce 和 Throttle
Lodash 是一个非常流行的 JavaScript 工具库,提供了很多实用的函数,包括 Debounce 和 Throttle。Lodash 的实现更加完善,考虑了更多的边界情况。
// Lodash 的 Debounce
_.debounce(func, [wait=0], [options={}])
// Lodash 的 Throttle
_.throttle(func, [wait=0], [options={}])
使用 Lodash 的 Debounce 和 Throttle 可以省去自己编写代码的麻烦,而且可以保证代码的质量。
结尾:
Debounce 和 Throttle 是前端性能优化中非常重要的两个概念。掌握了它们,可以有效地提高 Web 应用的性能和用户体验。希望通过今天的讲解,大家能够对 Debounce 和 Throttle 有更深入的理解,并在实际开发中灵活运用。
记住,优化永无止境,让我们一起努力,打造更流畅、更高效的 Web 应用!
下次再见!