JavaScript节流(throttle)与防抖(debounce)原理及实现

作者:纸飞机

时间:2026-10-24

浏览:241

本文深入解析JavaScript中节流(throttle)与防抖(debounce)函数的核心原理与应用场景。通过对比resize、scroll等高频事件,阐述两者在控制执行频率与空闲等待上的区别,并提供从基础实现到underscore.js源码级的代码示例,帮助开发者优化前端性能,避免频繁DOM操作导致的卡顿。

文章目录

高频事件触发的性能问题

问题的引出

在Web开发中,某些交互场景下事件会被高频触发,导致DOM操作或资源加载过于频繁,进而引发页面UI卡顿甚至浏览器无响应。

针对此类问题,常见的需求通常分为两类:一是停止触发操作后等待指定毫秒数再执行后续逻辑;二是以固定的频率间隔执行处理函数。为了解决这两种不同的业务场景,分别衍生出了debounce(防抖)和throttle(节流)两种技术方案。

1. resize事件

2. mousemove事件

3. touchmove事件

4.scroll事件

节流与防抖的核心概念

throttle 与 debounce

目前主流JavaScript框架及工具库均提供了这两个函数的支持。例如,jQuery中有对应的throttle和debounce插件,而underscore.js和Lodash.js等库也原生内置了这两个函数。

原理:

其核心思路是设置一个时间阈值delay,确保在delay毫秒内,函数最多只执行一次。

关键在于执行时机的选择:是在触发事件后立即执行,然后进入等待期?还是先等待delay毫秒,若期间无新事件则执行函数?

在等待期间触发的事件被忽略,确保只执行一次处理逻辑。

具体策略上,存在两种模式:一种是在触发时立即执行一次,后续事件被忽略(执行在前);另一种是忽略前面的所有触发,仅在操作结束后执行最后一次(执行在后)。

throttle与debounce的区别

区别:

1. throttle

可以将节流形象地理解为拧紧水龙头,使水以水滴形式流出。你会发现每隔一段时间,就会有一滴水落下。

即预先设定一个执行周期,当调用动作的时刻大于等于执行周期时,则执行该动作,随后进入下一个新的周期。

2.debounce

可以将防抖形象地理解为按住弹簧,只要手不松开,弹簧就不会弹起;只有当手松开(停止触发)后,弹簧才会反弹。

即当调用动作n毫秒后,才会执行该动作;若在这n毫秒内再次调用此动作,则重新计算执行时间(重置计时器)。

代码实现与效果演示

简单代码实现及实验结果

下面通过简单的代码示例来演示这两个函数的具体行为:

throttle 函数:


 window.addEventListener("resize", throttle(callback, 300, {leading:false}));
 window.addEventListener("resize", callback2);
 function callback () { console.count("Throttled"); }
 function callback2 () { console.count("Not Throttled"); }
 /**
 * 频率控制函数, fn执行次数不超过 1 次/delay
 * @param fn{Function}   传入的函数
 * @param delay{Number}  时间间隔
 * @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
 *             如果想忽略结束边界上的调用则传入 {trailing:false},
 * @returns {Function}   返回调用函数
 */
 function throttle(fn,delay,options) {
   var wait=false;
   if (!options) options = {};
   return function(){
     var that = this,args=arguments;
     if(!wait){
       if (!(options.leading === false)){
         fn.apply(that,args);
       }
       wait=true;
       setTimeout(function () {
         if (!(options.trailing === false)){
           fn.apply(that,args);
         }
         wait=false;
       },delay);
     }
   }
 }

将上述代码粘贴到浏览器控制台运行,观察控制台输出结果,可得到:

高频事件触发的性能问题

接下来观察debounce函数的运行效果。

debounce 函数:


window.addEventListener("resize", throttle(callback, 300, {leading:false}));
window.addEventListener("resize", callback2);
function callback () { console.count("Throttled"); }
function callback2 () { console.count("Not Throttled"); }
/**
 * 空闲控制函数, fn仅执行一次
 * @param fn{Function}   传入的函数
 * @param delay{Number}  时间间隔
 * @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
 *             如果想忽略结束边界上的调用则传入 {trailing:false},
 * @returns {Function}   返回调用函数
 */
function debounce(fn, delay, options) {
  var timeoutId;
  if (!options) options = {};
  var leadingExc = false;

  return function() {
    var that = this,
      args = arguments;
    if (!leadingExc&&!(options.leading === false)) {
      fn.apply(that, args);
    }
    leadingExc=true;
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(function() {
      if (!(options.trailing === false)) {
        fn.apply(that, args);
      }
      leadingExc=false;
    }, delay);
  }
}

将上述代码粘贴到浏览器中运行,并分三次改变窗口大小。观察发现,每一次改变窗口大小的开始和结束边界,事件处理函数各执行一次:

高频事件触发的性能问题

如果是一次性连续改变窗口大小,会发现开始和结束的边界各执行一次事件处理函数。请注意对比一次性改变窗口大小时throttle的情况:

高频事件触发的性能问题

主流库源码解析

underscore.js 的代码实现

_.throttle函数


 /**
 * 频率控制函数, fn执行次数不超过 1 次/delay
 * @param fn{Function}   传入的函数
 * @param delay{Number}  时间间隔
 * @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
 *             如果想忽略结束边界上的调用则传入 {trailing:false},
 * @returns {Function}   返回调用函数
 */
_.throttle = function(func, wait, options) {
  var context, args, result;
  var timeout = null;
  var previous = 0;
  if (!options) options = {};
  var later = function() {
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    var now = _.now();
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) {
      clearTimeout(timeout);
      timeout = null;
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
};

_.debounce函数


/**
 * 空闲控制函数, fn仅执行一次
 * @param fn{Function}   传入的函数
 * @param delay{Number}  时间间隔
 * @param options{Object} 如果想忽略开始边界上的调用则传入 {leading:false},
 *             如果想忽略结束边界上的调用则传入 {trailing:false},
 * @returns {Function}   返回调用函数
 */
_.debounce = function(func, wait, immediate) {
  var timeout, args, context, timestamp, result;

  var later = function() {
    var last = _.now() - timestamp;
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  };

  return function() {
    context = this;
    args = arguments;
    timestamp = _.now();
    var callNow = immediate && !timeout;
    if (!timeout) timeout = setTimeout(later, wait);
    if (callNow) {
      result = func.apply(context, args);
      context = args = null;
    }
    return result;
  };
}; 

参考的文章

Debounce and Throttle: a visual explanation jQuery throttle / debounce: Sometimes, less is more! underscore.js

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章