我理解的函数防抖和节流

268 阅读4分钟

防抖(debounce)

概念:防抖(在事件被触发n秒之后再执行回调函数,如果在这n秒内又被触发,则重新计时)

我们以一个输入框的keyup事件为例,先看一个没有防抖的例子:

<div>
    <span>没有防抖的input:</span>
    <input type="text" id="inputA">
</div>
<script>
    //模拟一段ajax请求
    function ajax(content){
      console.log('ajax request' + content);
    }
    let inputA = document.querySelector('#inputA');
    inputA.addEventListener('keyup', function(e){
      ajax(e.target.value);
    });
</script>

当我们不停输入时,运行结果如下:

image

按照上面的的概念,我们先试着实现一个简单版本的防抖:

<div>
  <span>防抖后的input:</span>
  <input type="text" id="inputB">
</div>
<script>
    // 防抖初始版本
    function debounceFn1(func, wait) {
      let timer = null;
      return (...args) => {
        timer && clearTimeout(timer);
        timer = setTimeout( () => {
          func.apply(this, args);
        }, wait);
      } 
    }
    let inputB = document.querySelector('#inputB');
    let debounceAjax = debounceFn1(ajax, 500);
    inputB.addEventListener('keyup', function(e){
      debounceAjax(e.target.value);
    })
</script>

当我们不停输入时,运行结果如下:

image

观察打印的结果发现,当我们快速输入时并不会不停发出请求了,嗯,不错不错,基本功能是实现了,可是这种方式并不会马上运行我们的请求代码,如果我们需要马上运行一次再防抖呢,那就加个参数吧,哈哈,上代码:

// 增加立即执行的防抖版本
function debounceFn2(func, wait, immediate) {
  let timer = null;
  return (...args) => {
    timer && clearTimeout(timer);
    //要实现只有首次立即执行的关键是变量timer,它是闭包中引用的一个变量,是不会被马上销毁的
    //所以timer只有在初始化时是null,之后是一直有值的,存的是定时器的ID
    if(immediate && !timer) {
      func.apply(this, args);
    }
    timer = setTimeout( () => {
      func.apply(self, args);
    }, wait);
  } 
}
let debounceAjax = debounceFn2(ajax, 500, true);
inputB.addEventListener('keyup', function(e){
  debounceAjax(e.target.value);
})

当快速输入值时,运行结果如下:

image

好了,一个能应付日常有防抖需求的函数基本上写好了,如果还需要更多功能可以参考underscore是如何实现debounce的,由于我还没用过,有使用心得了下次再总结。

节流(throttle)

概念:在一个单位时间内,只能触发一次函数,如果在这个时间段内触发了多次,只有一次能生效。

节流又分为两种实现方式,分别为有时间戳和计时器,下面先看一个用时间戳实现的栗子:

<div>
    <span>节流后的input:</span>
    <input type="text" id="inputC">
</div>
<script>
    // 函数节流(时间戳版本)
    function throttleFn1(func, wait) {
      // 上一次执行的时间
      let previous = 0;
      return (...args) => {
        // 当次执行的时间
        let now = +new Date();
        if(now - previous > wait) {
          previous = now;
          func.apply(this, args);
        }
      }
    }
    let throttleAjax = throttleFn1(ajax, 1000);
    inputC.addEventListener('keyup', function(e){
      throttleAjax(e.target.value);
    })
</script>

这种方法实现节流的核心是两次函数执行的时间差大于等待时间时才执行回调函数,同时将这个执行的时间设为对比时间,这样就保证了再等待时间间隔内永远只会执行一次回调函数。当快速输入数字时,上面代码执行结果如下:

image

另一种实现节流的方法就是类似于防抖那样利用定时器实现,这个定时器的所定时长就是这个等待值。实现代码如下:

// 节流版本(定时器版本)
function throttleFn2(func, wait) {
  let timer = null;
  return (...args) => {
    if(!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, wait);
    }
  }
}
let throttleAjax = throttleFn2(ajax, 1000);
inputC.addEventListener('keyup', function(e){
  throttleAjax(e.target.value);
})

当快速输入时,运行结果如下:

image

应用场景

  • 关于防抖
    • 典型的应用场景就上上面例子中的搜索关键词联想查询,我们肯定不希望每次输入都去搜索,所以设置一个间隔时间来防抖可以减少一些无效请求。
    • 调整窗口大小触发的resize事件,如果等待时间设置合理并且 调整的快的话可以做到只触发一次回调事件。
  • 关于节流
    • 监听浏览器的滚动条事件时,我们需要周期性地判断滚动条高度但又不想那么频繁就可以用节流

小节

函数的防抖和节流,出现的目的是为了提高代码性能,因为减少了不必要的请求和浏览器资源消耗(比如重绘和回流),算是性能优化的一种方式。

归根到底,防抖和节流是利用了函数的闭包缓存了状态值变量,从而可以对频繁的调用函数根据一定的规则做过滤。以上就是我个人看了一些大佬的文章之后做的总结,代码都是一点一点手打了之后验证过的,感兴趣的童鞋可以复制之后验证一下,当然有理解不足之处欢迎留言指出!

参考