面试官直击:防抖与节流,你真的搞懂了吗?😱

815 阅读5分钟

前言

最近有一次面试,被狠狠拷打了,尤其是在讨论到页面性能优化时,关于防抖与节流的问题让我措手不及。为了避免再次遇到同样的尴尬,我决定系统地总结这方面的知识,力求简洁明了地分享出来。

一、认识防抖和节流

40.jpg

防抖(Debouncing)

防抖技术的核心思想是在一个指定的时间间隔内,如果事件被频繁触发,则只有最后一次触发才会执行相应的回调函数。换句话说,它确保了在连续快速的操作中,只执行一次回调。这对于避免不必要的计算或请求特别有用,比如用户在搜索框输入时,我们不希望每次按键都发送请求,而是等待用户停止输入一段时间后再发送。

节流(Throttling)

不同于防抖,节流限制了一个函数在一定时间内的调用次数。即无论事件被触发多少次,在规定的时间间隔内只会执行一次回调。例如,在处理滚动事件时,我们可以设置每秒只响应一次,从而减少浏览器的负担。

这两种技术的区别

  • 防抖:就像游戏里面的回城操作,如果在回城时间内再次点击回城(回城被打断),则会重新计算回城时间。也就是说,防抖会在你停止操作后的一段时间才执行。
  • 节流:类似于游戏里英雄的普通攻击,在一定时间内点击多次,也只能攻击一次。这意味着在设定的时间间隔内,即使事件被多次触发,也只会执行一次回调。

使用场景分析

  • 防抖

    • 搜索框自动完成建议:用户输入完毕后延迟一段时间再发送请求,避免每次按键都发送请求。
    • 表单验证:在用户停止输入后进行表单验证,而不是每次按键都验证。
    • 窗口大小调整:在用户停止调整窗口大小后执行相应的布局调整代码。
  • 节流

    • 页面滚动加载更多内容:当用户滚动页面时,每隔一定时间检查是否需要加载更多数据。
    • 鼠标移动事件:在游戏中跟踪玩家的鼠标移动,但不需要对每一次微小的移动都做出反应。
    • 触摸屏滑动:在触摸屏设备上,监听用户的滑动操作,但不需要对每一个细微的触摸点变化都做出响应。

二、实现防抖

防抖的实现原理非常简单,就是通过对要执行的函数进行延迟处理,以此来控制函数执行的次数。

具体流程如下:

  1. 定义debounce函数:接受两个参数,一个是需要执行的函数func,另一个是等待的时间wait
  2. 内部逻辑
    • 使用let timeout;来存储定时器ID。
    • 返回一个新的函数,每当触发事件时调用此函数。
    • 在每次调用返回的函数时,首先清除之前设置的定时器(如果有)。
    • 设置一个新的定时器,在指定的wait时间后执行传入的func函数,并使用apply方法确保正确的上下文(this指向)和参数传递。
// 防抖
function debounce(func, wait) {
    let timeout;
    return function(...args) {
        const context = this;
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(context, args), wait);
    };
}

三、实现节流

节流技术的实现原理也非常简单,就是通过设置一个固定时间间隔,在这个时间间隔内只能执行一次相应的回调函数。

简单实现流程如下:

  1. 定义throttle函数:接受两个参数,一个是需要执行的函数func,另一个是限制的时间间隔limit
  2. 内部逻辑
    • 使用let lastExecutionTime = 0;来记录上一次执行的时间戳。
    • 返回一个新的函数,每当触发事件时调用此函数。
    • 在每次调用返回的函数时,获取当前时间now
    • 如果当前时间减去上次执行的时间大于或等于设定的limit,则执行传入的func函数,并更新lastExecutionTime为当前时间。
// 节流
function throttle(func, limit) {
    let lastExecutionTime = 0;
    return function(...args) {
        const now = Date.now();
        if (now - lastExecutionTime >= limit) {
            func.apply(this, args);
            lastExecutionTime = now;
        }
    };
}

高级实现:

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

  return function (...args) {
    const context = this;
    const now = Date.now();

    // 如果设置了不立即执行,并且是第一次触发,则跳过
    if (!previous && options.leading === false) {
      previous = now;
    }

    const remaining = wait - (now - previous);

    // 如果剩余时间小于等于 0,或者剩余时间大于等待时间(系统时间被调整)
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(context, args);
    } else if (!timeout && options.trailing !== false) {
      // 如果没有计时器,并且设置了尾部执行
      timeout = setTimeout(() => {
        previous = options.leading === false ? 0 : Date.now();
        timeout = null;
        func.apply(context, args);
      }, remaining);
    }
  };
}

参数说明:

  • func:需要节流的函数。
  • wait:时间间隔(毫秒)。
  • options:配置对象,包含以下属性:
    • leading:是否在节流开始时立即执行(默认 true)。
    • trailing:是否在节流结束后执行(默认 true)。

示例:

window.addEventListener('scroll', throttle(() => {
  console.log('Scrolling...');
}, 200));

四. 高级用法

(1)结合 requestAnimationFrame

对于动画相关的场景,可以使用 requestAnimationFrame 实现更平滑的节流。

function throttleWithRAF(func) {
  let isThrottled = false;

  return function (...args) {
    const context = this;

    if (!isThrottled) {
      requestAnimationFrame(() => {
        func.apply(context, args);
        isThrottled = false;
      });
      isThrottled = true;
    }
  };
}

(2)动态调整等待时间

可以根据事件的触发频率动态调整防抖或节流的等待时间。

function dynamicDebounce(func, minWait, maxWait) {
  let timeout = null;
  let lastCallTime = 0;

  return function (...args) {
    const context = this;
    const now = Date.now();
    const elapsed = now - lastCallTime;

    if (elapsed < minWait) {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        lastCallTime = now;
        func.apply(context, args);
      }, maxWait);
    } else {
      lastCallTime = now;
      func.apply(context, args);
    }
  };
}

(3)取消功能

为防抖和节流函数添加取消功能。

function debounce(func, wait) {
  let timeout = null;

  function debounced(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  }

  debounced.cancel = () => {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
}

结语

掌握这些技巧不仅能够让你的网页运行得更加流畅,还能在面试中展现出你的专业水平和技术深度。希望本文能为你的前端开发之路提供一些有价值的参考!

15.jpg