防抖和节流

159 阅读4分钟

写在前面

9月23日昨天请假一天

上午有学术讨论,对于我这个学术垃圾而言,确实是煎熬,但是马上也要面临着开题,这也是早晚都要跨过去的一步!

下午去新校区办了点儿事,晚上参加了华为的笔试。嗯~只能说自己太菜了。。。

OK,回到正题。今天要说的就是防抖和节流

问题描述

防抖和节流大家都不陌生,只要涉及某些事件频繁触发的场景,就会有他们的存在。

想象一个场景,假如你要去登录一个网站,就像下面这样

假设这个网站并没有去做一些其他的处理,那么当你点击 login 按钮之后,虽然网站后台会进行一系列的操作,但是你看到的前端页面是没有任何反应的。

你觉得是不是刚才没有点上,于是你又点了几次,然后网站后台又重新做了几次同样的操作。这样会浪费很多后台资源。

其他一些典型场景包括浏览器的 resize() 事件、滚动条事件等

于是你需要一些方法能够控制函数执行的频率,这就是防抖和节流要解决的问题

防抖

防抖:将函数的多次执行变为最后一次执行

**目标:**当用户多次点击 login 按钮,我们仅让用户的最后一次点击生效

还是以上登录的场景,假设我们有以下代码:

function login () {
  // do something...
  console.log('登录成功');
};

const loginBtn = document.querySelector("#login");
loginBtn.addEventListener('click', login);

以上代码的意思是获取到 login 按钮,并绑定 login() 函数,其中 login() 函数省略其它步骤,只保留打印登录成功的信息

第一步我们定义 debounce() 防抖函数,将 login() 作为参数传入

function debounce(fn) {
  // do Something...
}
loginBtn.addEventListener('click', debounce(login));

既然是让最后一次生效,那么就需要对之前的点击做一下延时操作,想到延时,第一反应想到的就是 setTimeout 函数,于是我们的函数雏形为:

function debounce (fn) {
  setTimeout(function () {
    fn();
  }, 1000);
}

这样能做到让传入的函数在 1s 后执行,但是这么做的问题就是这个函数会在 1s 后立即执行

所以我们应该在 debounce() 函数内返回一个函数,这样就能做到用户点击后函数才执行

function debounce (fn) {
  return function () {
    setTimeout(function () {
      fn();
    }, 2000);
  }
}

我们改正了函数立即执行的问题,但是我们目前实现的这个函数还是只能将执行时间延迟,并不能让函数只响应用户最后一次点击

我们现在需要做的是限制计时器,这里我们利用到的是 setTimeout 函数的返回值。当用户多次点击时,如果返回值不为空(函数还未执行),就清除计时器。这样就能保证函数只响应用户的最后一次操作

function debounce (fn) {
  let timer = 0;
  return function () {
    if (timer) clearTimeout(timer)
    timer = setTimeout(function () {
      fn();
    }, 2000);
  }
}

以上代码就能基本满足我们上面的问题,但是真实的场景中,还涉及到函数的参数和函数内部的 this 指向。

要解决这个问题最关键的是要想明白,(以上场景中 addEventListener)真正执行的函数是 debounce 内部 return 后的匿名函数

不管是传参还是 this 的绑定都是针对 return 后的匿名函数

所以最终我们的代码是:

function debounce (fn) {
  let timer = 0;
  return function (...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, 1000);
  }
} 

那么,你知道为什么 setTimeout 里面的函数要用箭头函数的形式吗?

还有,在以上用户多次点击登录的场景中,如果让用户多次点击的最后一次生效对用户而言是很不好的体验,那能不能让用户第一次点击就生效,让第一次以后的多次连续点击失效呢?

以下是仅让第一次点击生效的代码:

function debounce (fn) {
  let timer = 0;
  return function (...args) {
    if (timer) clearTimeout(timer);
    let firstClick = !timer;
    if (firstClick) {
      fn.apply(this, args);
    }
    timer = setTimeout(() => {
      timer = 0;
    }, 1000);
  }
} 

以上的代码你看懂了吗?

节流

节流:将多次操作变成每隔一段时间执行

还是以上的场景,我们想让用户连续多次点击,在固定时间间隔内执行一次。代码实现如下:

function throttle (fn, delay = 3000) {
  let begin = 0;
  return function (...args) {
    let cur = new Date().getTime();
    if (cur - begin > delay) {
      fn.apply(this, args);
      begin = cur;
    }
  }
}

这就是节流的代码实现,是不是很简单?