JavaScript中的防抖与节流
描述
防抖(debounce)和节流(throttle)是两种用于控制函数执行频率的技术,常用于优化高频触发的事件(如滚动、输入、窗口调整等)。防抖的核心是"延迟执行",在事件被频繁触发时,只有等到触发停止一段时间后才会执行函数。节流的核心是"稀释执行",确保函数在一定时间间隔内最多执行一次。
解题过程
-
理解问题场景
想象一个搜索框,用户在输入时会实时向服务器发送请求。如果不加控制,每输入一个字符就发送一次请求,会造成巨大的性能浪费。我们需要限制请求的频率。 -
防抖(Debounce)的实现思路
-
核心思想:事件被触发后,等待一个设定的延迟时间(如500毫秒)。如果在这个延迟时间内,事件又被触发,则取消上一次的等待,并重新开始计时。直到某次触发后,在延迟时间内没有再被触发,函数才会最终执行。
-
生活比喻:就像电梯的自动关门。电梯门开后,如果不断有人进入(频繁触发),电梯会一直重新计算关门等待时间。直到最后一个人进入后,一段时间内没人再进,门才会关上(函数执行)。
-
实现步骤:
a. 创建一个高阶函数,它接收两个参数:需要被防抖化的原始函数func和延迟时间wait。
b. 在高阶函数内部定义一个变量(例如timeoutId)来存储定时器的标识。
c. 返回一个新的函数(例如闭包)。
d. 在新函数内部,每次调用时,先使用clearTimeout(timeoutId)清除之前存在的定时器。
e. 然后设置一个新的定时器,延迟wait毫秒后执行原始函数func。
f. 将新定时器的标识赋值给timeoutId。 -
基础代码实现:
function debounce(func, wait) { let timeoutId; // 用于存储定时器ID // 返回防抖化后的新函数 return function (...args) { // 清除上一次的定时器 clearTimeout(timeoutId); // 设置新的定时器 timeoutId = setTimeout(() => { func.apply(this, args); // 使用apply确保func的this上下文和参数正确 }, wait); }; } -
使用示例:
// 一个模拟的搜索函数 function search(query) { console.log(`正在搜索: ${query}`); } // 对search函数进行防抖化,延迟500毫秒 const debouncedSearch = debounce(search, 500); // 模拟快速输入 debouncedSearch('a'); // 被取消 debouncedSearch('ap'); // 被取消 debouncedSearch('app'); // 被取消 // 等待500毫秒后,最终只会打印一次:正在搜索: app
-
-
节流(Throttle)的实现思路
-
核心思想:确保函数在一个固定的时间间隔内只执行一次。无论事件触发有多频繁,函数都会按照设定的间隔规律地执行。
-
生活比喻:就像水龙头,你即使把阀门拧到最大(频繁触发),水流也是匀速流出的(函数规律执行)。
-
实现步骤(使用时间戳):
a. 创建一个高阶函数,接收func和间隔时间wait。
b. 定义两个变量:lastExecTime(上次执行的时间戳)和timeoutId(定时器ID,用于尾调用)。
c. 返回一个新的函数。
d. 在新函数内部,获取当前时间currentTime。
e. 计算距离下次可执行的时间remaining = wait - (currentTime - lastExecTime)。
f. 如果remaining <= 0,说明已经过了间隔时间,可以立即执行函数,并更新lastExecTime为currentTime。
g. 如果没有定时器在等待,并且remaining > 0,则设置一个定时器,在remaining时间后执行函数(确保最后一次触发也能被执行)。 -
代码实现:
function throttle(func, wait) { let lastExecTime = 0; // 上次执行时间 let timeoutId; // 定时器ID return function (...args) { const currentTime = Date.now(); const remaining = wait - (currentTime - lastExecTime); // 如果已过间隔时间,或者系统时间被调整(currentTime < lastExecTime) if (remaining <= 0 || currentTime < lastExecTime) { // 清除可能存在的尾调用定时器 clearTimeout(timeoutId); timeoutId = null; lastExecTime = currentTime; func.apply(this, args); } // 如果不在间隔时间内,且还没有设置尾调用定时器 else if (!timeoutId) { timeoutId = setTimeout(() => { lastExecTime = Date.now(); // 使用当前时间更新最后一次执行时间 timeoutId = null; func.apply(this, args); }, remaining); } }; } -
使用示例:
// 一个处理滚动事件的函数 function handleScroll() { console.log(`窗口滚动了,当前位置: ${window.scrollY}`); } // 对handleScroll函数进行节流化,每200毫秒最多执行一次 const throttledScrollHandler = throttle(handleScroll, 200); // 当用户快速滚动页面时,handleScroll函数会以最大每200毫秒一次的频率被调用,而不是每次滚动都触发。 window.addEventListener('scroll', throttledScrollHandler);
-
-
防抖与节流的对比与选择
- 防抖:适用于连续事件结束后只需要触发一次的场景。
- 例子:搜索框输入联想、窗口
resize事件(等待用户调整完窗口大小后再计算布局)。
- 例子:搜索框输入联想、窗口
- 节流:适用于连续事件中需要保持一定执行频率的场景。
- 例子:页面滚动加载(scroll)、鼠标移动(mousemove)、射击游戏的射速限制。
- 防抖:适用于连续事件结束后只需要触发一次的场景。
总结
防抖和节流是前端性能优化中非常实用的技巧。防抖通过"重置延迟"来保证最终只执行一次,适合处理"结果"。节流通过"固定间隔"来保证执行频率,适合处理"过程"。理解它们的核心思想并掌握其实现原理,对于处理高频事件至关重要。