带你学习深入 js 函数的节流和防抖的原理

117 阅读4分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

前言

在我们日常使用浏览器的事件的时候,有一些事件触发的次数会非常频繁导致浏览器性能消耗过大,比方说一个最常见的 mousemove 鼠标移动事件,在点击过后稍微移动浏览器就会运行非常多次移动的逻辑,如果移动逻辑在复杂一点,这就很容易造成页面的卡顿。

滚动条demo

通过一个简单的滚动条例子来感受一下 window.onscroll 运行起来次数的恐怖。

  • window.onscroll:浏览器滚动条滚动触发事件
let index = 0;
function showTop() {
  let nowDate = new Date().getSeconds();
  let scrollTop =
    document.body.scrollTop || document.documentElement.scrollTop;
  console.log("滚动条位置:" + scrollTop);
  console.log("当前时间:" + nowDate);
  console.log("运行了:" + index + "次");
  index++;
}
window.onscroll = showTop;

image.png

image.png 可以看到,上下两张是我的demo在浏览器运行时接下来的console.log,在短短的一秒钟浏览器运行了 showTop 这个函数60多次,这是在我们这函数逻辑很简单的情况下,但是要是逻辑稍微复杂一点,特别是涉及到页面的dom元素操作,那么将会消耗非常大的浏览器内存,导致页面卡顿。

平时要是写一些拖拽,滚动条等需要频繁触发的事件,使用起来不是特别的流畅,就要考虑是不是逻辑函数执行次数过多而导致了浏览器卡顿导致的。这个时候,我们就需要根据实际需求来在我们的函数上加上节流或者防抖。

防抖(debounce)

函数后执行

根据上面的这种情况,我们就能够想到节流或者防抖,我们先来看一下要怎么实现防抖。

在第一次触发事件时,不立刻执行函数,而是给出一个期限时间,只有在这个时间内没有再次触发事件,才会执行函数

放在例子中就是,无论滚动条滚动了多久,只有当滚动条停下来超过一个时间期限,才会执行函数。

13287748484175583.gif

接下来我们来看一下要怎么实现这样的一个函数:

let timer = {};
const debounce = ({
  method,
  delay = 200
}) => {
  if(timer) clearTimeout(timer);
  timer = setTimeout(() => {
    method();
    clearTimeout(timer);
    timer = undefined;
  }, delay);
};

通过定义一个定时器,并且对我们的函数做一层包装,只有当delay时间过后才会触发method我们传入的方法:

 function showTop() {
  debounce({
    method: () => {
      let nowDate = new Date().getSeconds();
      let scrollTop =
        document.body.scrollTop || document.documentElement.scrollTop;
      console.log("滚动条位置:" + scrollTop);
      console.log("当前时间:" + nowDate);
      console.log("运行了:" + index + "次");
      index++;
    },
    ahead:true
  });
}

只需要将函数内部的逻辑做一层包装就可以实现上面的展示,这里我们没有传入时间所以是默认的0.2秒,利用可选参数的好处就是参数不传入函数也能正常运行。

函数先执行

防抖并非只有上面一种执行方式,通过我们自己的设计还可以变为函数先执行的防抖

在第一次触发事件时,立刻执行函数,并且给出一个期限时间,只有在这个时间内没有再次触发事件,才可以再次执行函数

接下来做一下扩展,现在需要一开始就运行函数,停下来一段时间后才能运行第二次。我们可以加入一个参数ahead给用户自定义,当ahead为true的时候触发优先执行函数

13287749769392098.gif

优化一下函数,传入一个新的参数ahead,如果当ahead为true并且当前的timer没有在运行,则执行我们的method。然后,在timer执行结束之后,如果ahead为true,就不会执行timer里面的method()

let timer = {};
const debounce = ({
  method,
  delay = 300,
  ahead = false
}) => {
  if (ahead && !timer) {
    method();
  }
  clearTimeout(timer);
  timer = setTimeout(() => {
    if (!ahead) {
      method();
    }
    clearTimeout(timer);
    timer = undefined;
  }, delay);
};

这样我们就实现了一个防抖的函数,而且可以支持传入时间和函数的执行前后顺序

节流(throttle)

节流和防抖又有着不一样的地方,节流指的是 在指定的时间间隔内只会执行一次函数,每隔指定时间后会再次执行

13287751439796304.gif

let timer = {};
let canRun = true;
const throttle = ({ method, delay = 2000}) => {
  if (!canRun) {
    return;
  }
  clearTimeout(timer);
  canRun = false;
  timer = setTimeout(() => {
    method();
    canRun = true;
    clearTimeout(timer);
    timer = undefined;
  }, delay);
};

上述就是简单的实现了一个节流函数,但是要注意,我们实现的时候未必只有setTimeout这一种办法,比如说也可以通过当前的时间戳,判断当前时间戳和上一次执行的时间戳是否大于指定的间隔时间来判断是否执行

总结

节流和防抖在前端的应用还是很广泛的,不止是上面的滚动条事件,平时设计的点击事件照道理也应该根据实际情况加上节流或者防抖,特别是当按钮需要发送请求的时候,不做限制的话可能会出现问题,上一个请求结果还没有返回,又多次的发出相同请求,这就是在设计的时候需要注意的了