JavaScript防抖与节流函数完整实现指南

作者:青柠

时间:2026-10-25

浏览:526

本文详细解析JavaScript中防抖与节流函数的实现原理。通过从基础到完整的代码演进,涵盖this指向、参数传递、立即执行模式及取消功能。对比时间戳与定时器两种节流方案,并展示结合两者的最终实现,提供可直接复用的代码示例。

文章目录

最近阅读《红楼梦》时,诗词鉴赏增多,偶尔也会产生一些随想……

近日研读underscore.js库中关于防抖和节流的章节,借此机会复习相关概念,重新梳理并整理以下知识点。

前端开发中常遇到鼠标移动、窗口滚动、键盘输入等高频操作。防抖与节流技术旨在优化此类事件,通过降低触发频率来提升页面性能。

如图所示,短短几秒内事件触发的次数非常惊人。

防抖机制

防抖机制

防抖的核心逻辑是:无论事件触发多少次,仅在最后一次触发后的 n 秒才执行回调。若在等待期间再次触发,则重置计时器,以新触发的时间为准重新计算。

接下来探讨如何构建一个基础的防抖函数。

基础版本实现

依据防抖原理,可设置一个定时器。每次触发事件但未到达指定时间时,清除上一个定时器并重新设置新的定时器。

 const debounce = function(func, wait) {
   let timeout
   return function() {
     // 再次触发事件则删除上一个定时器,重新设置
     clearTimeout(timeout)
     timeout = setTimeout(func, wait);
   }
 }

上述代码实现了最基础的防抖功能,触发频率已显著降低。

防抖机制

上下文(this)与参数(arguments)处理

基础版本存在缺陷,例如在setTimeout中,this的指向无法正确获取,导致setTimeout中的this指向了全局Window对象而非预期的调用者。

为解决此问题,可在执行定时器前保存并重置this:

 const debounce = function(func, wait) {
   let timeout
   return function() {
     // 保存this
     let context = this // 新增
 
     clearTimeout(timeout)
     timeout = setTimeout(function() {
       func.apply(context) // 新增
     }, wait);
   }
 }

此外,若需在自定义函数func中传递event对象,基础实现无法正确获取参数。需进一步修改代码以支持参数传递:

 const debounce = function(func, wait) {
   let timeout
   return function() {
     let context = this // 新增
     // 保存参数
     let args = arguments // 新增
 
     clearTimeout(timeout)
     timeout = setTimeout(function() {
       func.apply(context, args) // 修改
     }, wait);
   }
 }

至此,一个功能相对完善的防抖函数已实现。

立即执行模式

若希望事件触发时立即执行函数,随后在停止触发 n 秒后再重新允许执行,需增加此功能。

可通过传入参数immediate来控制是否立即执行。若传递immediate为真,则立即执行一次函数,并设置定时器;时间截止后将定时器状态置为null。下次进入函数时,先判断定时器状态是否为null,再决定是否执行。

 const debounce = function(func, wait, immediate) {
   let res, timeout, context, args;
 
   const debounced = function() {
     context = this
     args = arguments
     // 如果已经设置了setTimeout,则重新进行设置
     if(timeout) clearTimeout(timeout)
     // 判断是否为立即执行
     if(immediate) {
       let runNow = !timeout
       // 设置定时器,指定时间后设置为null
       timeout = setTimeout(function() {
         timeout = null
       }, wait)
       // 如果timeout已经为null(已到期),则执行函数
       // 保存执行结果,用于函数返回
       if(runNow) res = func.apply(context, args)
     } else {
       // 如果没有设置立即执行,则设置定时器
       timeout = setTimeout(function() {
         func.apply(context, args)
       }, wait)
     }
     return res
   }
 
   return debounced
 }

上述逻辑包含两种不同的触发方式,具体流程如下:

图中黑色箭头代表触发动作,红色箭头代表执行动作。

防抖机制

非立即执行模式

防抖机制

立即执行模式

当immediate为true时,执行流程如下:

首次执行:若timeout为null,则runNow设为true,随后设置定时器;指定时间后将timeout置为null,代表执行间隔时间;最后判断runNow以决定是否执行函数。

二次执行:

  • 情况一:超过设置时间。若第二次触发时已超时,此时timeout已被定时器置为null,进入debounced函数后,runNow为true,重新设置定时器并执行函数。
  • 情况二:未超过设置时间。因未超时,timeout未被置为null,故runNow为false。由于timeout的定时器已被清除,此时重置定时器,不执行函数。

当immediate为false时:

此情况与之前设置一致,未超时则重置定时器,定时器到达指定时间后自动执行一次函数。

两者最大区别在于:立即执行模式在首次触发时即执行一次,若下次触发时已到达设置时间,则直接执行;非立即执行模式首次触发仅设置定时器,时间到达后自动执行,若在设置时间内再次触发则仅重置定时器,永不立即执行。

取消功能

若需取消debounce函数,例如debounce的时间间隔为10秒,且immediate为true,则需等待10秒后才能重新触发。若具备取消功能,点击取消后再次触发即可立即执行。

 debounced.cancel = function() {
     // 删除定时器
     clearTimeout(timeout);
     // 设置timeout为null
     timeout = null;
 };

只需清除定时器,将timeout置为null即可。因为若immediate为true会直接执行一次函数,随后重新设置定时器。

完整防抖实现

完整的防抖函数代码如下:

 function debounce(func, wait, immediate) {
   let res, timeout, context, args;
 
   const debounced = function () {
       context = this;
       args = arguments;
 
       if (timeout) clearTimeout(timeout);
       if (immediate) {
           var runNow = !timeout;
           timeout = setTimeout(function(){
               timeout = null;
           }, wait)
           if (runNow) res = func.apply(context, args)
       }
       else {
           timeout = setTimeout(function(){
               func.apply(context, args)
           }, wait);
       }
       return res;
   };
 
   debounced.cancel = function() {
       clearTimeout(timeout);
       timeout = null;
   };
 
   return debounced;
 }

节流机制

节流同样是减少触发执行的手段,但其思路与防抖完全不同。

持续触发事件时,每隔一段时间仅执行一次。即仅按设置的时间作为时间段,到达指定时间后触发函数即执行;未到达指定时间,无论触发多少次均不执行。

即“没到点,无论怎么触发,均不执行”。

目前主要有两种实现方式:使用时间戳和设置定时器。

时间戳实现

触发函数时,使用当前时间戳与上一次保存的时间戳相减,对比设置的时间,决定是否执行函数。

 const throttle = function(func, wait) {
   let previous = 0, context, args;
 
   return function() {
     context = this
     args = arguments
 
     // 获取当前时间戳
     let now = +new Date()
     // 判断当前时间戳与上一次触发的时间差值是否大于等于指定时间
     if((now - previous) >= wait) {
       func.apply(context, args)
       // 更新时间戳
       previous = now
     }
   }
 }

注意:在JS中,元素前使用'+'号可将其转换为Number类型;若转换失败,则得到NaN。

+new Date()将调用Date.prototype上的valueOf()方法。根据MDN文档,Date.prototype.value方法等同于Date.prototype.getTime()。

 console.log(+new Date('2022-08-17'));
 console.log(new Date('2022-08-17').getTime());
 console.log(new Date('2022-08-17').valueOf());
 console.log(new Date('2022-08-17') * 1);
 // 结果都是相同的

定时器实现

定时器思路为:首次触发时设置定时器,指定时间后将变量置为null;下次触发时判断变量是否为null,以此决定执行。

const throttle = function(func, wait) {
  let timeout, context, args;

  return function() {
    context = this
    args = arguments
    // 允许执行
    if(!timeout) {
      // 设置定时器,到达时间后设置timeout为null
      timeout = setTimeout(function() {
        timeout = null
        func.apply(context, args)
      }, wait)
    }
  }
}

上述两种写法均可实现基本节流函数,但存在差异:

  • 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行。
  • 第一种事件停止触发后不再执行,第二种事件停止触发后仍会再执行一次。

既然执行行为不同,能否将两者结合?

结合实现

结合两者的目标是实现一个既能开始执行一次,又能结束时再执行一次的函数。

思路如下:若触发时未到指定时间,则设置定时器;若已到指定时间,则直接执行。

function throttle(func, wait) {
  let timeout, context, args, previous = 0;

  const later = function() {
      // 定时器执行时更新时间戳
      previous = +new Date();
      timeout = null;
      // 执行函数
      func.apply(context, args)
  };

  const throttled = function() {
      let now = +new Date();
      //下次触发 func 剩余的时间
      let remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果没有剩余的时间了或者更改了系统时间
      if (remaining <= 0 || remaining > wait) {
          // 清空定时器及timeout
          if (timeout) {
              clearTimeout(timeout);
              timeout = null;
          }
          // 更新时间戳变量
          previous = now;
          func.apply(context, args);
      } else if (!timeout) {
          // 处理还没有到达指定时间的触发行为
          // 此处设置定时器时间要设置剩余的时间,与上文中防抖函数中有区别
          timeout = setTimeout(later, remaining);
      }
  };
  return throttled;
}

梳理逻辑如下:

首次触发throttled时,因previous为0,remaining <= 0条件成立,执行func函数,并重置定时器及变量,最后将previous更新为当前时间。

二次触发:

  • 未到达指定时间:remaining为正数,不进入remaining <= 0执行语句,而是设置定时器,不执行函数。
  • 到达指定时间:remaining为负数,执行函数,同首次触发。

定时器执行时,也会更新previous和timeout的值。

核心在于remaining变量的运算。

控制执行时机

若需控制首次和末次是否执行,可传递第三个参数。

具体参数定义如下:

  • leading:false表示禁用第一次执行。
  • trailing: false表示禁用停止触发的回调。
function throttle(func, wait, options = {}) { //修改
  let timeout, context, args, previous = 0;

  const later = function() {
      previous = options.leading === false ? 0 : +new Date(); //修改
      timeout = null;
      func.apply(context, args);
      // 清空作用域及参数变量
      if (!timeout) context = args = null; //修改
  };

  const throttled = function() {
      let now = +new Date();
      // 如果是首次触发,并且设置首次不执行函数。那么将previous与now进行同步
      // now 与 previous 相减不小于0,则不会执行函数
      if (!previous && options.leading === false) previous = now; // 新增
      let remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
          if (timeout) {
              clearTimeout(timeout);
              timeout = null;
          }
          previous = now;
          func.apply(context, args);
          // 清空作用域及参数变量
          if (!timeout) context = args = null; //修改
      } else if (!timeout && options.trailing !== false) { // 修改
          timeout = setTimeout(later, remaining);
      }
  };
  return throttled;
}

需注意实现中的一个问题:

leading:false和trailing: false不能同时设置。若同时设置,即既不开始也不结束时触发,函数将无法正常执行。

核心仍关于时间戳的加减法,仅根据功能需求设置时间戳。

取消功能

与防抖函数的取消功能基本相同,需重置各个作用变量:

throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = null;
}

完整节流实现

function throttle(func, wait, options = {}) {
  let timeout, context, args, previous = 0;

  const later = function() {
      previous = options.leading === false ? 0 : +new Date();
      timeout = null;
      func.apply(context, args);
      if (!timeout) context = args = null; 
  };

  const throttled = function() {
      let now = +new Date();
      if (!previous && options.leading === false) previous = now;
      let remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
          if (timeout) {
              clearTimeout(timeout);
              timeout = null;
          }
          previous = now;
          func.apply(context, args);
          if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
          timeout = setTimeout(later, remaining);
      }

      throttled.cancel = function() {
          clearTimeout(timeout);
          previous = 0;
          timeout = null;
      }
  };
  return throttled;
}

这也是underscore.js中节流的实现方式。

以上为利用JavaScript实现防抖节流函数的详细内容,更多关于JavaScript防抖节流函数的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章