前端性能优化篇:防抖和节流

参考:JS问题:项目中如何区分使用防抖或节流?

面试官:什么是防抖和节流?有什么区别?如何实现?

1 为什么要用到防抖和节流

  • 当函数绑定一些持续触发的事件如:浏览器的resize、scroll、keypress、mousemove ,键盘输入,多次快速click等等。
  • 如果每次触发都要执行一次函数,会带来性能下降,资源请求太频繁等问题。
  • 为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce)节流(throttle) 的方式来减少调用频率。

防抖(Debounce)节流(Throttle) 是前端开发中最常用的优化处理方式,本质上是 优化高频率执行代码 的一种手段。

2 防抖和节流两者定义的区分

2.1 简单定义区分

  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时,即一段时间内,多次执行变为只执行 最后一次

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效,即一段时间内,多次执行变为只执行 第一次

2.2 详细定义区分

  • 防抖(debounce):是指在一段时间内多次触发相同事件时,只执行最后一次触发的事件。这意味着,在一系列触发事件中,如果在指定的时间间隔内,发生了新的触发事件,那么前面的触发事件将被忽略,只有最后一次触发事件会被执行。
  • 节流(throttle):是指在一段时间内多次触发相同事件时,只执行一次事件。这意味着,无论触发事件发生多少次,在指定的时间间隔内只会执行第一次事件。

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。

假设电梯有两种运行策略 debounce 和 throttle,超时设定为15秒,不考虑容量限制。

电梯第一个人进来后,15秒后准时运送一次,这是节流。

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖。

3 防抖和节流两者应用场景的区分

真实的项目中,在不同的场景下灵活切换使用防抖或节流,才会更加减少不必要的资源消耗,更加提高前端应用的性能和响应性。

3.1 防抖(Debounce)的应用场景

  • 搜索框输入: 当用户在搜索框中输入内容时,可以使用防抖来延迟触发搜索请求。只有在用户停止输入一段时间后才会发送请求,避免频繁的请求发送。
  • 窗口调整resize: 当窗口大小调整时,可以使用防抖来优化执行某些操作的频率,例如重新计算布局或重新渲染页面。
  • 按钮点击: 当用户点击一个按钮时,可以使用防抖来确保只有在用户停止点击一段时间后才会执行相应的操作,避免误操作或重复执行。
  • 手机号、邮箱验证输入检测

3.2 节流(Throttle)的应用场景

  • 页面滚动: 当用户滚动页面时,可以使用节流来限制触发滚动事件的频率。例如,在滚动过程中只执行某些操作的固定时间间隔,以减少事件处理的次数。
  • 鼠标移动: 当用户移动鼠标时,可以使用节流来控制触发鼠标移动事件的频率。例如,在一定时间内只执行一次鼠标移动的处理逻辑,避免过多的计算和渲染操作。
  • 实时通信: 在实时通信应用中,如聊天应用或在线协作工具,可以使用节流来限制发送消息的频率,以避免发送过多的请求或数据。
  • 高频点击提交,表单重复提交

4 防抖

Vue2:

/**
 * @param {Function} func
 * @param {number} wait
 * @param {boolean} immediate
 * @return {*}
 */
export function debounce(func, wait, immediate) {
  let timeout, args, context, timestamp, result

  const later = function() {
    // 据上一次触发时间间隔
    const last = +new Date() - timestamp

    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last)
    } else {
      timeout = null
      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
      if (!immediate) {
        result = func.apply(context, args)
        if (!timeout) context = args = null
      }
    }
  }

  return function(...args) {  // 返回一个包装后的函数
    context = this  // 保存函数执行上下文对象
    timestamp = +new Date()
    const callNow = immediate && !timeout  // 是否立即调用函数的条件
    // 如果延时不存在,重新设定延时
    if (!timeout) timeout = setTimeout(later, wait)
    if (callNow) {  // 如果满足立即调用条件,则立即执行函数
      result = func.apply(context, args)
      context = args = null
    }

    return result
  }
}

Vue3:

export const useDebounce = (func: Function, delay: number, immediate: boolean = false) => {
  // let timer: ReturnType<typeof setTimeout>;
  let timer: NodeJS.Timeout | null = null;

  return (...args: any[]) => {
    if(timer) clearTimeout(timer);
    timer = setTimeout(() => {
      func(...args);
    }, delay)
  }

}

手写防抖函数:

// 手写防抖函数

/**
 * 执行频率:在事件停止触发的延迟时间内执行一次
 * 
 * 防抖函数应用场景:
 * 1、频繁的点击按钮,触发某个事件
 * 2、监听浏览器滚动事件,完成某些特定操作
 * 3、用户缩放浏览器的resize事件等等
 * 
 * 目的:延迟执行,确保只在停止操作后执行一次
 * */

/**
 * @param {function} func - 目标对象
 * @param {number} wait - 防抖间隔时间
 * @param {boolean} immediate - 开启初次立即执行
 * @param {function} resultCallback - 目标对象返回值的回调函数
 * @return {function} - 防抖函数
 * @author m
 * @example
 *
 * ```js
 * const inputChange = function (event) {
 *   console.log(`发送了第${++counter}次网络请求`, this, event)
 *  // 返回值
 *  return "test"
 * }
 * 
 * const debounceChange = debounce(inputChange, 2000, false, (res) => {
 *  console.log("拿到真正执行函数的返回值:", res)
 * })
 * 
 * // 取消功能
 * const cancelBtn = document.querySelector("#cancel") // 获取取消按钮
 * cancelBtn.onclick = function() {
 *   debounceChange.cancel()
 * }
 * ```
 * 
 */

function debounce(func, wait = 200, immediate = false, resultCallback) {
  // 保存每次定时器操作,在下一次规定时间内 重复调用提供取消信息
  let timer = null;
  // 作为判断当前为立即执行 or 防抖模式
  let isInvoke = false;

  const _debounce = function (...args) {
    // 排除第一次调用无timer,其余规定时刻清除上一次定时器
    if (timer) clearTimeout(timer)

    if (immediate && !isInvoke) {
      const result = func.apply(this, args);
      // 将目标对象返回值返回后,能够进行回调执行处理
      if (resultCallback) resultCallback(result);
      // 初次立即执行,后续正常防抖模式
      isInvoke = true;
    } else {
      // 定时,正常防抖模式内容
      timer = setTimeout(() => {
        const result = func.apply(this, args) // 绑定this为元素本身以及event事件
        // 将目标对象返回值返回后,能够进行回调执行处理
        if (resultCallback) resultCallback(result);
        // 防抖模式结束,下次输入内容立即执行
        isInvoke = false;
      }, wait)
    }
  }

  // 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer);
    // 初始化
    timer = null;
    isInvoke = false;
  }

  return _debounce;
}

5 节流

Vue2:


// 节流函数
export function throttle(func, wait) {
  let timeout; // 定义一个计时器变量,用于限制函数调用频率
  return function (...args) { // 返回一个包装后的函数
      const context = this; // 保存函数执行上下文对象
      if (!timeout) { // 如果计时器不存在
          func.apply(context, args); // 执行函数
          timeout = setTimeout(() => {
              timeout = null; // 清空计时器变量
          }, wait); // 创建计时器,在指定时间后重置计时器变量
      }
  };

手写节流函数:

// 手写节流函数

/**
 * 执行频率:在事件频繁触发时,以固定的时间间隔执行
 * 
 * 应用场景:页面滚动、鼠标移动、窗口调整大小等高频事件
 * 
 * 目的:限制执行频率,降低函数调用的次数
 */

/**
 * 创建一个节流函数,在给定的时间间隔内最多执行一次目标函数
 * @param {function} func - 需要节流的目标函数
 * @param {number} intervalTime - 节流的间隔时间(毫秒)
 * @param {Object} [options] - 节流的配置选项
 * @param {boolean} [options.leading = true] - 是否在第一次调用时立即执行
 * @param {boolean} [options.trailing = false] - 是否在冷却时间结束后追加一次执行
 * @param {function} [options.resultCallback] - 每次节流函数执行后的回调函数,用于处理目标函数的返回值
 * @return {function} - 返回包装后的节流函数 
 * @author m
 * @example
 *
 * ```js
 * // 创建一个节流函数,每 1000 毫秒最多执行一次
 * const throttledFn = throttle(() => {
 *   console.log('Throttled function executed');
 * }, 1000);
 * 
 * // 绑定事件
 * document.addEventListener('scroll', throttledFn);
 * 
 * // 调用 cancel 方法
 * throttledFn.cancel();
 * ```
 * 
 */

function throttle(func, intervalTime = 200, options = { leading: true, trailing: false, resultCallback }) {
  // 记录上一次的开始时间
  const { leading, trailing, resultCallback } = options;
  // 每次点击时,重置当前最新时间
  let lastTime = 0;
  // 记录
  let timer = null;

  const _throttle = function (...args) {
    // 获取"第一次"调用时间
    let nowTime = new Date().getTime();
    // 必须第一次执行(lastTime) 且 用户传参调节为首次不立即执行(leading)
    if (!lastTime && !leading) lastTime = nowTime;
    // 获取冷却时间 = 节流间隔时间 - 第一次响应时间 + 发起响应时间
    const remainTime = intervalTime - nowTime + lastTime;

    // 达到冷却,可以执行响应函数
    if (remainTime <= 0) {
      // 如果节流间隔后存在正常响应函数,将"最后一次响应函数"的定时器清除
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      const result = func.apply(this, args);
      // 返回值
      if (resultCallback) resultCallback(result);
      // 记录函数上一次被成功调用的时间戳
      lastTime = nowTime;
      return;
    }

    // 节流最后一次也可以执行的判断逻辑
    if (trailing && !timer) {
      // 返回timer,只有冷却归零后还未存在正常调用,timer尚未清零才能调用,一旦调用则重置timer
      timer = setTimeout(() => {
        timer = setTimeout(() => {
          timer = null;
          lastTime = !leading ? 0 : new Date().getTime();
          const result = func.apply(this, args);
          // 返回值
          if (resultCallback) resultCallback(result);
        })

      }, remainTime);
    }
  }

  // 取消方法
  _throttle.cancel = function () {
    // 有值的情况才进行取消
    if (timer) clearTimeout(timer);
    // 初始化
    timer = null;
    lastTime = 0;
  }

  return _throttle;
}

参考:JS高级-手写防抖节流函数与实现事件总线

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值