函数的防抖与节流

函数的防抖与节流

在前端开发中,我们经常会听到函数的防抖和节流,下面我们来看看这两个重要的技术。

函数防抖与节流的概念很相似,他们都可以限制函数执行的次数,但是使用场景是不同的。

首先,我们知道在前端开发中,会遇到一些频繁的事件触发,比如:

  1. windowresizescroll
  2. mousedownmousemove
  3. keyupkeydown

等等。

我们来看个例子:

HTML

<h1>Number of scroll events </h1>
<a href="#" class="reset">Reset</a>
<div id="counter">0</div>

CSS

body {
  background: #444444;
  color: white;
  margin: 0 auto;
  max-width: 600px;
  padding: 20px;
  min-height: 1000vh;
}

#counter {
  position: fixed;
  top: 100px;
  left: 40%;
  font-size: 50px;
}

.reset {
  color: white;
  text-decoration: none;
  border: 1px solid white;
  padding: 10px 20px;
  background: rgba(0, 0, 0, 0.1);
}

JavaScript

let i = 0;
const counter = document.getElementById('counter');
const reset = document.getElementsByClassName('reset')[0];

window.addEventListener('scroll', getUserAction);

function getUserAction() {
  counter.innerHTML = i;
  i++;
};

reset.onclick = function () {
  counter.innerHTML = '0';
  i = 0;
};

效果如下:

频繁的事件触发

可以看到:当滑动滚动条时,getUserAction 这个方法一直在被调用着。因为这个例子很简单,所以浏览器完全可以反应过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会出现卡顿。

为了解决这个问题,一般有两种解决方案:

  1. debounce 防抖;
  2. throttle 节流。

一、防抖

防抖的原理就是:不管怎么触发事件,但是一定在事件触发 n 秒后才执行,如果一个事件触发的 n 秒内又触发了这个事件,那就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。

根据防抖的原理,我们可以这样来改造代码:

function debounce(func, wait) {
  let timer = null;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(func, wait);
  };
}

使用的时候:

window.addEventListener('scroll', debounce(getUserAction, 1000));

现在不管怎么滑动滚动条,只有在 1000ms 内不再触发,才执行事件;否则将不会执行。

这里还有一个小问题,debounce 函数的 this 指向 Window 对象,我们可以更改一下 this 的指向:

function debounce(func, wait) {
  let timer = null;
  return function () {
    const that = this;
    clearTimeout(timer);
    timer = setTimeout(function () {
      func.apply(that);
    }, wait);
  };
}

二、节流

节流的原理是:一个函数执行一次后,只有大于设定的执行周期,才会执行第二次。也就是说:在规定的时间内,只让函数触发的第一次生效,后面的不生效。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

1、使用时间戳

function throttle(func, wait) {
  let previous = 0;
  return function () {
    const nowTime = Date.now();
    if (nowTime - previous > wait) {
      func();
      previous = nowTime;
    }
  };
}

例子依然使用前面的例子:

window.addEventListener('scroll', throttle(getUserAction, 1000));

运行代码可以看到:当滑动滚动条的时候,事件立刻执行,每过 1s 会执行一次;如果停止触发,以后不会再执行事件。

解决 this 指向问题:

function throttle(func, wait) {
  let previous = 0;
  let that;
  return function () {
    const nowTime = Date.now();
    that = this;
    if (nowTime - previous > wait) {
      func.apply(that);
      previous = nowTime;
    }
  };
}

2、使用定时器

function throttle(func, wait) {
  let timeout;
  return function () {
    const that = this;
    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(that);
      }, wait);
    }
  };
}

相关文章 
1、ES6 —— 模板字符串
2、ES6 —— let 和 const
3、ES6 —— Set 和 Map 的数据结构
4、JavaScript 中的值传递
5、JavaScript 中的类数组对象(array-like objects)
目录