如何用 JavaScript 实现一个函数防抖 (Debounce) 和函数节流 (Throttle)?

好的,各位靓仔靓女们,今天咱们来聊聊前端面试中经常出现的“防抖”和“节流”这两个小妖精。别怕,它们其实没那么难,今天我就把它们扒个精光,让你们彻底掌握,以后再也不怕面试官问这些问题了!

开场白:为什么要有防抖和节流?

想象一下,你正在做一个搜索框,用户每输入一个字,就向服务器发送一次请求。如果用户输入 "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 的指向,防止在 setTimeoutthis 指向发生改变。
      • clearTimeout(timer):清除之前的定时器,重新计时。
      • setTimeout:设置一个新的定时器,在 delay 毫秒后执行 func
      • func.apply(context, args):执行 func 函数,并将 contextthis)和参数 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:判断是否需要立即执行。只有 immediatetrue 并且 timer 不存在时,才需要立即执行。
      • if (!timer) timer = setTimeout(...):只有当 timer 不存在时,才设置定时器。
      • if (callNow) func.apply(context, args):如果需要立即执行,就立即执行 func
      • if (!immediate) func.apply(context, args):只有当 immediatefalse 时,才在 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)
执行时机 事件停止触发一段时间后执行 单位时间内最多执行一次
应用场景 搜索框输入、窗口大小调整 滚动事件、按钮点击
侧重点 减少执行次数 控制执行频率
形象比喻 迟到的英雄 水龙头
  • 选择:
    • 如果希望在事件停止触发一段时间后才执行,比如搜索框输入,可以使用防抖。
    • 如果希望控制事件的执行频率,比如滚动事件,可以使用节流。

四、总结

防抖和节流都是优化性能的利器,理解它们的原理和适用场景,可以让你在面试中游刃有余,在实际项目中写出更高效的代码。

  • 防抖: 延迟执行,减少执行次数。
  • 节流: 控制频率,保证一定时间内只执行一次。

希望今天的讲解对大家有所帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注