好的,各位靓仔靓女们,今天咱们来聊聊前端面试中经常出现的“防抖”和“节流”这两个小妖精。别怕,它们其实没那么难,今天我就把它们扒个精光,让你们彻底掌握,以后再也不怕面试官问这些问题了!
开场白:为什么要有防抖和节流?
想象一下,你正在做一个搜索框,用户每输入一个字,就向服务器发送一次请求。如果用户输入 "JavaScript",那就要发送10次请求!服务器:我裂开了!
这就是一个典型的需要优化的场景。频繁触发事件会导致资源浪费,影响性能,甚至让服务器崩溃。防抖和节流就是为了解决这类问题而生的。
一、函数防抖 (Debounce): 迟来的英雄
防抖就像一个迟到的英雄,只有在事件停止触发一段时间后,才会执行。
-
原理: 当事件被触发时,不是立即执行,而是设置一个定时器。如果在定时器结束之前,事件再次被触发,就重新设置定时器。只有当事件停止触发一段时间后,定时器到期,才会执行。
-
生活例子: 你去银行取号,如果前面排队的人一直在变动(取消、插队),银行会重新叫号。只有当一段时间内没有人变动,才会叫到你。
-
代码实现:
function debounce(func, delay) { let timer; // 存储定时器 return function(...args) { const context = this; // 保存this指向 // 如果存在定时器,就清除掉,重新计时 if (timer) { clearTimeout(timer); } timer = setTimeout(() => { func.apply(context, args); // 执行函数,并传递参数和this timer = null; // 清空定时器 }, delay); }; } // 使用例子: function search(keyword) { console.log("正在搜索:" + keyword); } const debouncedSearch = debounce(search, 300); // 300毫秒的防抖 const inputElement = document.getElementById("searchInput"); inputElement.addEventListener("input", function(event) { debouncedSearch(event.target.value); });
- 解释:
debounce(func, delay)
:接受两个参数,func
是要执行的函数,delay
是延迟时间(毫秒)。timer
:存储定时器的 ID。return function(...args)
:返回一个新的函数,这个函数才是真正被事件监听器调用的函数。context = this
:保存this
的指向,防止在setTimeout
中this
指向发生改变。clearTimeout(timer)
:清除之前的定时器,重新计时。setTimeout
:设置一个新的定时器,在delay
毫秒后执行func
。func.apply(context, args)
:执行func
函数,并将context
(this
)和参数args
传递给它。timer = null
:非常重要的一步,在定时器执行完毕后,将timer
设置为null
,表示当前没有定时器在运行。 如果不设置,会导致后续调用无法创建新的定时器,防抖功能失效。
- 解释:
-
带立即执行的防抖:
有时候我们希望在第一次触发事件时就立即执行一次,而不是等待
delay
毫秒。可以修改一下代码:function debounce(func, delay, immediate) { let timer; return function(...args) { const context = this; const callNow = immediate && !timer; // 立即执行的条件 if (!timer) timer = setTimeout(() => { timer = null; if (!immediate) func.apply(context, args); // delay后执行 }, delay); if (callNow) func.apply(context, args); // 立即执行 }; } // 使用例子: const debouncedSearchImmediate = debounce(search, 300, true); // 立即执行的防抖 inputElement.addEventListener("input", function(event) { debouncedSearchImmediate(event.target.value); });
- 解释:
immediate
:新增一个参数,表示是否立即执行。callNow = immediate && !timer
:判断是否需要立即执行。只有immediate
为true
并且timer
不存在时,才需要立即执行。if (!timer) timer = setTimeout(...)
:只有当timer
不存在时,才设置定时器。if (callNow) func.apply(context, args)
:如果需要立即执行,就立即执行func
。if (!immediate) func.apply(context, args)
:只有当immediate
为false
时,才在delay
毫秒后执行func
。
- 解释:
二、函数节流 (Throttle): 水龙头
节流就像一个水龙头,不管你拧得多快,它都只会按照一定的频率滴水。
-
原理: 规定在一个单位时间内,最多只能触发一次函数。如果超过这个时间,也只有第一次触发的函数会被执行。
-
生活例子: 你疯狂点击一个按钮,但实际上这个按钮的功能只能每隔 1 秒执行一次。
-
代码实现 (时间戳版):
function throttle(func, delay) { let previous = 0; // 上次执行的时间 return function(...args) { const now = Date.now(); const context = this; if (now - previous > delay) { func.apply(context, args); previous = now; } }; } // 使用例子: function scrollHandler() { console.log("滚动事件触发:" + Date.now()); } const throttledScrollHandler = throttle(scrollHandler, 500); // 500毫秒的节流 window.addEventListener("scroll", throttledScrollHandler);
- 解释:
previous
:存储上次执行的时间。now
:当前时间。if (now - previous > delay)
:判断当前时间与上次执行时间的间隔是否超过delay
。- 如果超过
delay
,则执行func
,并更新previous
为当前时间。
- 解释:
-
代码实现 (定时器版):
function throttle(func, delay) { let timer = null; return function(...args) { const context = this; if (!timer) { timer = setTimeout(() => { func.apply(context, args); timer = null; }, delay); } }; } // 使用例子: const throttledScrollHandlerTimeout = throttle(scrollHandler, 500); // 500毫秒的节流 window.addEventListener("scroll", throttledScrollHandlerTimeout);
- 解释:
timer
:存储定时器 ID。if (!timer)
:判断当前是否没有定时器在运行。- 如果没有定时器,则设置一个定时器,在
delay
毫秒后执行func
,并将timer
设置为null
。 - 如果已经有定时器在运行,则忽略本次触发。
- 解释:
-
时间戳 + 定时器 混合版:
时间戳版的问题是,第一次会立即执行,停止触发后没有办法再执行一次。 定时器版的问题是,第一次不会立即执行,停止触发后还会再执行一次。 那么结合一下,就能取长补短。
function throttle(func, delay) { let timer = null; let previous = 0; return function(...args) { const context = this; const now = Date.now(); const remaining = delay - (now - previous); if (remaining <= 0 || remaining > delay) { if (timer) { clearTimeout(timer); timer = null; } previous = now; func.apply(context, args); } else if (!timer) { timer = setTimeout(() => { previous = Date.now(); timer = null; func.apply(context, args); }, remaining); } }; } // 使用例子: const throttledScrollHandlerMixed = throttle(scrollHandler, 500); // 500毫秒的节流 window.addEventListener("scroll", throttledScrollHandlerMixed);
- 解释:
remaining = delay - (now - previous)
:计算距离下次执行还需要等待的时间。if (remaining <= 0 || remaining > delay)
:如果remaining
小于等于 0 或者大于delay
,表示可以立即执行。else if (!timer)
:如果remaining
大于 0 并且没有定时器在运行,则设置一个定时器,在remaining
毫秒后执行func
。
- 解释:
三、防抖 vs 节流:谁更胜一筹?
特性 | 防抖 (Debounce) | 节流 (Throttle) |
---|---|---|
执行时机 | 事件停止触发一段时间后执行 | 单位时间内最多执行一次 |
应用场景 | 搜索框输入、窗口大小调整 | 滚动事件、按钮点击 |
侧重点 | 减少执行次数 | 控制执行频率 |
形象比喻 | 迟到的英雄 | 水龙头 |
- 选择:
- 如果希望在事件停止触发一段时间后才执行,比如搜索框输入,可以使用防抖。
- 如果希望控制事件的执行频率,比如滚动事件,可以使用节流。
四、总结
防抖和节流都是优化性能的利器,理解它们的原理和适用场景,可以让你在面试中游刃有余,在实际项目中写出更高效的代码。
- 防抖: 延迟执行,减少执行次数。
- 节流: 控制频率,保证一定时间内只执行一次。
希望今天的讲解对大家有所帮助!下次再见!