# 函数节流与防抖

节流和防抖函数都属于js中的高阶函数,它在我们的实际开发中也经常用到,它们可以用来限制函数的执行次数,从而提高执行性能。它们也是面试中的高频问题,我们这章就来了解一下它们的工作原理。

# 节流(throttle)

节流函数一般用来限制函数在单位时间内的调用次数。它规定在给定的时间内,回调函数只能执行一次,即使触发了多次事件,也只能有一个回调函数执行。举个例子,比我要给页面的滚动事件添加监听函数,函数会做大量的计算,如果不加节流的话,因为不断的触发回调,不断的进行计算会导致页面在滚动的时候就会变得非常卡顿。我们先来看不加节流的情况:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>函数节流</title>
  <style>
    body{
      height: 2000px;
      background: linear-gradient(grey, lightgreen, blue);
    }
  </style>
</head>
<body>
  
</body>
<script>
  function onScroll(e) {
    console.info('window scrolled')
  }
  window.onscroll = onScroll
</script>
</html>

上面的scroll没有加节流,会导致onScroll被重复的触发很多次:

no throttle

如果这里的onScroll函数是一个特别复杂的运算的话,那么页面就会变得卡顿,极大的影响用户体验,所以此时,该介绍我们的节流函数出场了。

function throttle(fn, delay) {
  let first = true, timer = null
  return (...args) => {
    // 如果是第一次调用的话,直接直接即可
    if (first) {
      fn.apply(this, args)
      first = false
    } else {
    
      // 当前有定时器在跑的话,不做任何操作,直接return
      if (timer) return

      // 否则就将回调函数放到定时器里面来
      timer = setTimeout(() => {
        // 在里面,要清除定时器,重置timer,以免影响后续的判断
        clearTimeout(timer)
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}

再改下我们的事件监听代码:

const throtteledScroll = throttle(onScroll, 1000)
window.onscroll = throtteledScroll

这下我们的onScroll函数就会在页面滚动的时候,每秒执行一次:

throttle-ok

其实我们的节流函数不用定时器也是可以的,上面使用定时器的原因,是想让函数可以在脱离事件后也能得到一次执行。

function throttle(fn, delay) {
  let last = null;
  return (...args) => {
    if (!last) {
      fn.apply(this, args)
      last = Date.now()
    } else {
      if (Date.now() - last > delay) {
        fn.apply(this, args)
        last = Date.now()
      }
    }
  }
}

# 防抖(debounce)

其实防抖也很好理解,这里的“抖”发生的前提,就是一件事重复很多次,如果我们让它只执行一次,那么它就“抖”不起来了,这也是防抖函数的原理。我们来看看比较正式一点的解释: 在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。

之前看到一个举的很贴切的例子,我们的电梯,当有人进去按下关门按钮的时候,电梯会等2s再关闭,这期间如果有人进来的话,你不得不重新按下关闭按钮,就得再等2s,直到关门这个间隙没有人进来时,电梯才会关门、运转。

类比上面关于防抖函数的解释,我相信你已经开始明白了,下面我们来看具体的实现:


/***
 * @param fn 回调函数
 * @param delay 延时时间,即函数要过多少毫秒才会被调用
 **/
function debounce(fn, delay) {
  let timer = null
  return () => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn()
    }, delay)
  }
}

结合一个具体的场景,我们来看看它的用处。我们来模拟一个搜索功能,提供一个输入框,当用户输入文字的时候,我们打印出搜索的内容:

<body>
  请输入关键字进行搜索:<input type="text" id="keyword">
</body>
<script>
  const input = document.querySelector('#keyword')
  function onInput(e) {
    console.info('search:', e.target.value)
  }
  input.addEventListener('input', onInput)
</script>

我们来看看没有加上防抖的效果:

我们每输入一个字符,都会触发回调,如果这是正式的环境,那么意味这每一次输入都要向服务器发起请求,这对服务器来说无疑产生来很大的压力。我们希望用户在最终输入完时,或者停顿某一个时间长度时,再发起请求,这样显得更加合理,我们利用防抖函数来改进代码:

<script>
  function debounce(fn, delay) {
    let timer = null
    return () => {
      clearTimeout(timer)
      timer = setTimeout(() => {
        fn()
      }, delay)
    }
  }
  const input = document.querySelector('#keyword')
  function onInput(e) {
    console.info('search:', e.target.value)
  }
  const debounceOnInput = debounce(onInput, 1000);
  input.addEventListener('input', debounceOnInput)

</script>

运行一下,你会发现一秒后报错了:

debounce error

原来我们的debounce函数没有保存源函数的参数,导致我们的onInput方法的参数丢失。我们来修改一下debounce函数:

function debounce(fn, delay) {
  let timer = null
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

再来运行一下:

# 参考资料

上次更新: 5/23/2020, 3:25:52 PM