深入理解防抖与节流:从原理到实战(附可直接复用代码)

75 阅读7分钟

深入理解防抖与节流:从原理到实战(附可直接复用代码)

在前端开发中,我们经常会遇到频繁触发事件的场景——比如输入框实时搜索、滚动条滚动监听、按钮重复点击、窗口大小调整等。如果不对这些事件进行处理,频繁执行回调函数会严重消耗浏览器性能,导致页面卡顿、响应变慢。

防抖(Debounce)节流(Throttle) 就是解决这类问题的两大核心方案。本文将从原理出发,拆解经典的防抖节流实现代码,再结合实战场景说明如何使用个高频,帮你彻底。

一、先搞懂核心概念:防抖 vs 节流

很多人会混淆防抖和节流,其实核心区别很简单:

  • 防抖(Debounce) :触发事件后,等待一段时间再执行回调;如果在等待期间再次触发事件,则重新计时。核心是“触发后延迟执行,重复触发则重置计时”。
  • 节流(Throttle) :触发事件后,立即执行回调;在接下来的一段时间内,无论事件如何触发,都不再执行。核心是“固定时间内只执行一次”。

举个通俗的例子:

  • 防抖:电梯关门后,只要有人再按按钮,就重新开门并等待3秒再关门(重复触发重置计时);

  • 节流:电梯每3秒只能关一次门,即使期间有多人按按钮,也只能等3秒后再关门(固定时间内只执行一次)。

二、经典实现代码拆解

下面分别解析开头给出的防抖和节流核心代码,逐行搞懂背后的逻辑,帮你从“会用”到“懂原理”。

1. 防抖(Debounce)实现

先看完整实现代码,再逐行拆解:

function debounce(fn, delay) {
  let timer = null; // 定时器变量
  return function(...args) {
    if (timer) clearTimeout(timer); // 清除之前的定时器
    timer = setTimeout(() => {
      fn.apply(this, args); // 延迟执行函数,保持 this 和参数
    }, delay);
  };
}
解析
  1. function debounce(fn, delay) { ... }:定义防抖函数,接收两个参数——fn(需要防抖的目标函数)、delay(延迟时间,单位ms)。
  2. let timer = null;:声明一个定时器变量timer,用于存储 setTimeout 的返回值。这里利用了闭包的特性——timer 不会随着 debounce 函数执行结束而销毁,而是被返回的匿名函数引用,从而持续追踪定时器状态。
  3. return function(...args) { ... }:返回一个匿名函数,作为最终被事件触发的回调函数。...args 是 ES6 剩余参数,用于接收事件触发时传递的参数(比如事件对象 event)。
  4. if (timer) clearTimeout(timer);:核心逻辑——如果定时器存在(说明之前触发过事件且还在等待期),则清除之前的定时器,重置计时。这就是“重复触发重置计时”的关键。
  5. timer = setTimeout(() => { fn.apply(this, args); }, delay);:重新设置定时器,延迟 delay 毫秒后执行目标函数 fn。这里用 apply(this, args) 绑定 this 指向(确保 fn 内部的 this 是事件触发的元素,而非 window),并传递接收的参数 args

2. 节流(Throttle)实现

完整实现代码如下,同样逐行拆解:

function throttle(fn, delay) {
  let lastTime = 0; // 记录上次执行时间
  return function(...args) {
    const now = Date.now(); // 获取当前时间戳
    if (now - lastTime >= delay) { // 判断当前时间与上次执行时间差是否大于等于延迟时间
      lastTime = now; // 更新上次执行时间为当前时间
      fn.apply(this, args); // 执行目标函数,保持 this 和参数
    }
  };
}
解析
  1. function throttle(fn, delay) { ... }:定义节流函数,参数与防抖一致——fn(目标函数)、delay(时间间隔,单位ms)。
  2. let lastTime = 0;:声明 lastTime 变量,用于记录目标函数上次执行的时间戳(初始值为0,确保第一次触发时能直接执行)。同样利用闭包特性,持续追踪上次执行时间。
  3. return function(...args) { ... }:返回匿名函数作为事件回调,...args 接收事件参数。
  4. const now = Date.now();:获取当前时间戳(毫秒级),用于计算与上次执行时间的差值。
  5. if (now - lastTime >= delay) { ... }:核心逻辑——判断当前时间与上次执行时间的差值是否大于等于 delay。如果是,说明已经过了“冷却期”,可以执行目标函数;如果不是,则忽略本次触发。
  6. lastTime = now;:执行函数前,更新 lastTime 为当前时间戳,确保接下来的 delay 时间内不会再次执行。
  7. fn.apply(this, args);:绑定 this 指向并传递参数,执行目标函数。

三、实战场景:防抖节流怎么用?

理解原理后,更重要的是知道在什么场景下用哪种方案。下面结合3个高频场景,给出具体的使用示例。

场景1:输入框实时搜索(防抖)

场景说明:用户在输入框输入关键词时,需要触发接口请求获取搜索建议。如果不做处理,每输入一个字符就会发一次请求,性能开销大。用防抖可以实现“用户停止输入n毫秒后,再发送请求”。

效果:用户快速输入时,不会频繁触发请求;只有当用户停止输入500ms后,才会执行搜索请求。

场景2:滚动条滚动监听(节流)

场景说明:监听页面滚动事件,实现“滚动加载更多”或“滚动时更新导航栏状态”。滚动事件触发频率极高,用节流可以限制为“每200ms只执行一次回调”。

// 模拟滚动加载更多逻辑
function loadMoreData() {
  console.log('滚动加载更多数据');
  // 实际项目中这里是请求下一页数据的逻辑
}

// 生成节流函数,每200ms执行一次
const throttledLoadMore = throttle(loadMoreData, 200);
// 绑定滚动事件
window.addEventListener('scroll', throttledLoadMore);

效果:无论用户怎么快速滚动页面,loadMoreData 都只会每200ms执行一次,避免频繁请求接口。

场景3:按钮防重复点击(防抖/节流均可)

场景说明:用户可能会因为网络延迟等原因,快速点击提交按钮,导致重复提交表单。可以用防抖(点击后延迟n毫秒执行,期间重复点击无效)或节流(n毫秒内只能点击一次)解决。

注意:按钮防重复点击更推荐用防抖,因为节流无法避免“用户在300ms内点击两次,第一次执行,第二次在300ms后再次执行”的问题;而防抖只要在300ms内重复点击,就会重置计时,只有最后一次点击会触发执行。

四、防抖与节流的核心区别(表格总结)

为了方便大家快速区分和选择,整理了以下对比表格:

特性防抖(Debounce)节流(Throttle)
核心逻辑重复触发则重置计时,延迟执行固定时间内只执行一次,立即执行
执行时机事件触发后,等待 delay 毫秒执行事件触发后立即执行,之后 delay 毫秒内不执行
适用场景输入框搜索、按钮防重复点击、窗口 resize 监听滚动加载、高频点击事件、鼠标移动监听
依赖变量定时器(setTimeout)时间戳(Date.now())

五、注意事项(避坑指南)

  1. this 指向问题:如果直接将目标函数绑定到事件上,this 会指向触发事件的元素;但如果不使用 apply(this, args),在防抖/节流函数内部,fn 的 this 会指向 window(非严格模式下)。所以一定要用 apply/call 绑定 this。
  2. 参数传递问题:事件触发时可能会传递参数(比如 event 对象),需要用 ...args 接收并传递给目标函数,否则 fn 无法获取到这些参数。
  3. 延迟时间选择:延迟时间(delay)需要根据场景调整——输入框搜索建议推荐 300-500ms,滚动监听推荐 100-200ms,按钮防重复点击推荐 300ms 左右。
  4. 防抖的“立即执行”需求:上面的防抖实现是“延迟执行”,如果需要“第一次触发立即执行,之后重复触发延迟执行”,可以给 debounce 函数加一个参数(比如 immediate: boolean),判断是否需要立即执行。

六、总结

防抖和节流的核心目标都是“优化高频事件的执行频率,提升页面性能”,区别在于执行逻辑和适用场景:

  • 当需要“等待用户停止操作后再执行”时,用防抖(比如输入框搜索);

  • 当需要“固定时间内稳定执行一次”时,用节流(比如滚动加载)。