/ javascript

浅析 JavaScript 的函数节流和去抖

现代网页的实现上,会有很多交互上的优化,比如常见的 滚动加载输入联想 等等。他们的实现思路很简单,以滚动加载而言,无非就是去是增加一个滚动的事件监听,每次滚动判断当前的元素是否已经滚动到了用户的可视区,然后根据判断结果来决定是否来加载相关数据。 输入联想也类似,无非就是修改一下监听的事件类型,和判断的相关逻辑。

前端交互的优化上,我们总是需要需要事件监听来知道用户进行了什么操作,从而设定页面给出怎样的反应。但本文主要的内容不是讲如何具体的实现某个交互,而是对交互实现的依赖的事件监听那部分的优化。

滚动加载图片的例子


var onScroll = function() {
  // 这里判断当前的 img 元素是否已经滚动到了用户的可视区
  // 根据判断结果来决定是否来加载相关数据 
  // 注:img 元素当设置 src 属性值后会产生一个 http 的请求,加载图片
};

window.addEventListener('scroll',onScroll ,false);

这样理论上就实现了图片的滚动加载或者说是按需加载,懒加载等等。

这样的实现乍看没有任何问题,但在实际的场景中,问题还是很严重的。

如下:

See the Pen Scroll events counter by Corbacho (@dcorb) on CodePen.

很明显,当你在 codepen 中滚动鼠标的时候,显示的数字蹭蹭蹭的加大, 这个数字就是过程中,浏览器触发 scroll 事件的数字, 可以看出,浏览器触发 scroll 事件是很频繁的。

所以,回到上面提出的问题中,因为浏览器触发 scroll 会很平凡, 也就意味着事件的监听回调函数 onScroll 会被很频繁的执行。这就是问题之所在,且不说我们的 onScroll 里面的逻辑可能会是大量的计算。如此频繁的计算就算可以很轻松的被完成,结果也是没有意义的。所以我们要在这里来优化代码,倒不是优化 onScroll 本省,而是想办法让 onScroll 执行的次数可以减少。

函数节流

在原本的逻辑中,假设在滚动条连续的滚动中,浏览器每 10ms 触发一次scroll 事件,就意味着浏览器每 10ms 就要去执行一次 onScroll,如果想让 onScroll 不那么频繁的被执行,怎么办?

很明显,想去改动 scroll 触发的频率在前端这一层面上肯定是不行,因为他们浏览器自身的设定。那么是否可以在触发 scroll 事件之后,不直接调用 onScroll 函数从而进行优化呢?比如,假设scroll 事件持续触发,我们让 onScroll 函数每 250ms 触发。


var throttle = function() {
  var previous = 0;  // 初始设置上一次调用 onScroll 函数时间点为 0。
  var timeout; 
  var wait = 250; // 该变量表示执行 onScroll 函数的间隔时间。
  // 返回一个函数 ,该函数在每次触发 scroll 事件时,真正调用的函数。
  return function() {
    var now = Date.now(); // 记录下当前触发 scroll 的时间
    var remaining = wait - (now - previous); // 持续的滚动过程中,计算得到调用 onScroll 的甚于时间
    if (remaining <= 0) {
      if (timeout) {
        clearTimeout(timeout)
      }
      previous = now;
      timeout = null;
      onScroll();
    } else if(!timeout) { // 初始或者非持续滚动(滚动动作的时间小于 250ms ),设定一个定时器。
      timeout = setTimeout(onScroll, wait);
    }
  }
};

// 注意这里的调用方式
window.addEventListener('scroll',throttle() ,false);

这样调用是不会有结果的

window.addEventListener('scroll',throttle ,false);

上述的代码旨在说明实现函数节流的思路,著名的 underscore.jslodash 都有节流方法的实现。

输入联想的例子

当你在使用 google 搜索内容的时候,有些关键词输入到一般,谷歌会展示一个可选列表,是根据你当前输入的内容作出的一个猜测联想,如果有中目标的联想,你就可以不需要输入接下来的内容,直接选择自动补全就好。

这种场景下,我们一般是去监听用户的 onKeyRress 或者 onKeyUp 等等键盘事件。在当用户输入一些文字的时候,我们一监听到键盘事件就将用户当前输入的内容去发送一个 ajax 查询请求显然是不行的。那么在节流函数内部去调用 ajax 请求呢?

在这种场景下,其实函数节流也是不合适,因为,函数节流只是减小了函数执行的在事件触发过程中频率,将原来可能调用1000次减少到调用100次。然而对输入联想的场景来说,只要用户还在触发键盘事件,那么发送请求就是没有意义的。

所以,我们现在需要的一种实现是应该是在用户停止键盘事件一段时间后,去发送一个请求。

比如应该这么设计这个过程,用户在输入,用户输入有所停顿(也就是不再触发键盘事件),当这个事件大于 250ms 之后,去触发一个查询请求。 如果用户输入停顿的时间小于 250ms,不去发送查询请求。

电梯模型

输入联想的执行过程,应该和生活中的电梯执行过程是一样的。
比如,当你正在座电梯,当电梯门正要关闭的时候,这是有一个同学又冲过来,按了一下电梯的按钮,这个电梯门会打开然后等待一定的时间,如果在等待的过程中,又有另外一位同学过来按了按钮,之前等待的时间清零,继续等待一定的时间,直到在等待的时间中,不在有别的人去按按钮。门才会关闭,然后上下运行。

函数去抖

了解这个过程之后,来简单的实现下这个过程。

// 如下搜索控件
<input type="search" name="search" />

var sendAjax = function(obj) {
  // 发送请求
};
var debounce = function() {
  var wait = 2500;
  var timeout;
  return function () {
    var now = Date.now();
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(sendAjax, wait);
  }
};
var input = document.querySelector('input[name=search]');

input.addEventLister('keyup', debounce(), false);

总结

函数节流和函数去抖的使用场景略有不同。

函数节流:在事件触发过程中,减小相关回调函数的执行频率。
函数去抖:在事件触发结束后一定时间后去执行事件回调函数,如果在这一定时间内又触发了相关事件,则不去触发事件回调函数,下一次执行事件回调函数时间仍是确定的一定时间。

相关文章和代码