防抖和节流

101 阅读5分钟

作者:Sean

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天

前言:防抖和节流是前端开发中重要的两个概念,但是其概念与其名称一样,没有浅显易懂。防抖和节流是两个不同的功能,都用于限制用户的频繁操作和资源请求,减轻服务器的压力,是前端开发中重要的性能优化方法。下面就让我们探究一下防抖和节流吧。

1. 防抖(debounce)

1.1 什么是防抖

让我们用一个例子来认识防抖:

需求
用户在搜索框中输入内容之后,在下拉框中显示输入内容相关的信息

那么,很显然,这需要获取用户输入的字符,然后以该字符向服务器发送请求,服务器返回与该字符相关的信息。

**但是,我们需要在用户每输入一个字符就发送一次请求吗?**这显然是不合理的,如果用户数量大,那么服务器将不堪重负。

我们就需要一点点优化,在用户输入一个字符的未来一段时间内,只要用户持续输入内容,就不发送请求。这时候就需要**“防抖”**,即在这规定的时间内,只有用户持续输入内容,发送请求的行为就会被打断,规定时间需要重新计时。这就像是游戏中的“吟唱读条“,在吟唱过程中如果受到了打断,吟唱则需要重头开始。

1.2 如何实现防抖

那么如何实现防抖呢?刚刚提到了计时的概念,那很显然需要用到定时器。让我们直接看代码:

function debounce(func, delay) {
  let timer = null;
  return function (...args) {
    // 闭包返回函数
    let context = this; // 保存this
    timer && clearTimeout(timer); // 清除定时器
    timer = setTimeout(() => {
      // 设置定时器
      func.call(context, ...args); // 绑定this指向和传参
    }, delay);
  };
}

分析

  1. 首先,我们需要为防抖函数传入要执行的函数并设定一个延迟时间。并且用闭包的形式将里面的被定时器包裹的函数返回。这是因为我们触发事件需要的是这个内部函数,而不是外面的防抖函数。

  2. 设置定时器,并将执行函数放进去。为了达到防抖效果,需要在执行函数前消除定时器,这样在用户每次执行事件时,定时器就会重新计时。

  3. 考虑定时器内部this 指向问题,需要在外部用一个变量保存 this,在定时器内部用 call 绑定。

  4. 考虑执行函数的参数传入问题。使用剩余参数在 call 绑定 this 指向时一并传入。

1.3 第一次执行

以上就是基本的防抖函数了,但是它有一个问题,那就是在用户第一次执行事件时,它就会起到一个“防抖”的效果,这在一些场合是我们不希望看见的。于是,我们需要考虑一下如何让函数在第一次事件中能够立刻执行。

function debouceImme(func, delay) {
  let timer = null;
  return function (...args) {
    let context = this;
    const callNow = !timer;
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
    }, delay);
    callNow && func.call(context, ...args);
  };
}

分析

  • 使用了一个callNow限制了函数的执行,只有当 timer 被清除,callNow才为真,函数才执行。
  • 而定时器的作用缺变成了将自身清除,而不是限制函数的执行。这样与callNow结合后,在计时器计时期间,callNow始终不为真,函数就无法执行,起到了防抖的作用
  • 而第一次执行时,timer 被定义成 null,所以函数可以直接执行。

1.4 封装

将以上两种情况都考虑并封装起来:

function debounce(func, delay, immediate) {
  let timer;
  return function (...args) {
    let context = this;
    timer && clearTimeout(timer);
    if (immediate) {
      let callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, delay);
      callNow && func.call(context, ...args);
    } else {
      timer = setTimeout(() => {
        func.call(context, ...args);
      }, delay);
    }
  };
}

2. 节流(throttle)

2.1 什么是节流

理解了防抖,拿什么是节流呢?还是用一个例子:

需求
用户使用手机验证码登录时,请求一次验证码之后要60s之后才能再次点击请求,考虑到一些需求,不能将按钮直接设置为disabled

同理,我们还是可以使用定时器来解决这个需求,即定时器只要在计时,这个函数就不会进入执行。不管用户在如何点击按钮也不会触发事件。这就像是游戏中技能的“冷却时间”,一个技能在释放完毕之后,要经过一段时间才能再次释放。

2.2 如何实现节流

直接上代码:

function throttle(func, delay){
  let timer;
  return function (...args) {
    let context = this;
    if (timer) return
    timer = setTimeout(()=>{
      func.call(context, ...args)
      timer = null;
    }, delay)
  }
}

分析

  • 相对于上面的防抖,节流就很好理解了。在设置完定时器之后,只要这个定时器还在计时,重复执行这个时间都会触发if(timer) return直接返回。

2.3 第一次执行

同样,上面的节流函数在第一次执行事件就直接进入了计时。为了让第一次就能直接触发事件,我们需要使用新的思路来重新这个函数。

function throttleimme(func, delay) {
  let pre = 0;
  return function (...args) {
    let now = new Date(),
    if(now - pre > delay) {
      func(...args)
      pre = now
    }
  }
}

分析

这里直接将定时器替换成简单的时间减算。

  • 首先第一次执行时,now - pre肯定大于延迟的值,则必定能进入内部执行函数
  • 执行完函数后,将刚刚设置的now时间赋值给了pre,下次再点击时,又重新了设置now,新的now减去旧的now的到的时间就是上一次执行完时间后所过的时间,如果不够设置的间隔时间,是无法再次触发事件的。

2.4 封装

function throttle(func, delay, imme) {
  if (imme) {
    let pre = 0;
  } else {
    let timer;
  }
  return function (...args) {
    if (imme) {
      let now = new Date();
      if (pre - now > delay) {
        func(...args);
        pre = now;
      }
    } else {
      let context = this;
      setTimeout(() => {
        func.call(context, ...args);
      }, delay);
    }
  };
}

注意

  • 当使用防抖函数时,内部的执行函数不能够加( ),否则函数会立刻执行

  • 如果要传入参数,应当使用debounce(func.bind(this, ...args), delay)