防抖和节流的实现

880 阅读4分钟


注:为防止误会,我下面文字中说的第一次触犯函数,都是指的,在wait时间以外的触发。并不是真正意义上的第一次触发。而是将延时器以外的触发叫做第一次触发。有同学智商感人,无奈声明一下。 # 一、lodash.js插件 现在工作中一般都用插件了,lodash提供的功能很全面。具体使用方法,有官方文档。

二、自己写防抖和节流

   1、防抖

立即执行版本:

我用防抖函数,习惯于点击后立即执行,然后setTimeout锁住。

function debounce(func, wait) {
    let timer
    return function() {
      let context = this
      let args = arguments
      if (timer) clearTimeout(timer);
      let callNow = !timer
      timer = setTimeout(() => {
        timer = null;
      }, wait)
      if (callNow) func.apply(context, args);
    }
}

上面代码很好理解,第一次点击时候还没有timer,所以callNow为true,立即执行函数。此时的setTimeout回调异步函数进入微任务队列。func调用后,开始执行微任务,wait时间后,timer再次变成null。
这里的闭包,保证你在wait时间内重复点击时候,每次都会重新开始执行timer。

延迟执行版:

也可以采用wait时间后,再执行的。不过,我从来不用这种。

function debounce(func, wait) {
    let timer
    return function() {
      let context = this
      let args = arguments
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        func.apply(this, args)
      }, wait)
    }
}

上面这种也很好理解。
初次执行函数时候,没有timer。会进入异步回调,等待wait时间后再执行func。
在wait时间内,你不停的触发函数,会导致清空定时器,然后重新定时,不会执行func。这就是闭包的功劳啊,哈哈。其实闭包就是提供了一个块级作用域。

当然,也可以写成手动选择是否需要立即执行的。

function debounce(func, wait, immediate) {
    let timer
    return function() {
      let context = this
      let args = arguments
      if (timer) clearTimeout(timer)
      if (immediate) {
        let callNow = !timer
        timer = setTimeout(() => {
          timer = null
        }, wait)
        if (callNow) func.apply(context, args)
      } else {
        timer  = setTimeout(() => {
          func.apply
        }, wait)
      }
    }
}

2、节流

延时器版:

    节流相比于防抖的唯一不同之处,在于,节流是在规定时间内触发一次。而防抖是,当处在wait时间内时候,触发不起作用。

function throttle(func, wait) {
    let timeout
    return function() {
      let context = this
      let args = arguments
      if (!timeout) {
        timeout = setTimeout(() => {
          timeout = null
          func.apply(context, args)
        }, wait)
      }
    }
}


现在来看代码,第一次触发,没有timeout,会进入异步队列,wait时间后调用func。
在wait时间内触发,此时有timeout,不做任何操作,延时器不会重新计时,这保证了这是“节流”而不是“防抖”。
我们也可以发现,这个节流,相比较我的第一个防抖,就多了一个是否处于timeout的判断。在防抖中,因为没有这个判断,timer会重新计时。

时间戳版:

还有一种防抖的写法,是记录前后两次触发的时间间隔,当大于等于wait时间,会进行第二次触发。其实原理和延时器是一样的。

function throttle(func, wait) {
    let previous = 0
    return function() {
      let now = Date.now()
      let context = this
      let args = arguments
      if (now - previous > wait) {
        func.apply(context, args)
        previous = now
      }
    }
}

三、多防抖函数解析

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>小黄人</title>
</head>
<body>
  <button id="btn">按钮</button>
<script>
function debounce(func, wait) {
  console.log('绑定监听函数')
  let timer = null
  return function() {
    let context = this
    let args = arguments
    if (timer) clearTimeout(timer)
    console.log('查看callNow值')
    let callNow = !timer
    timer = setTimeout(() => {
      console.log('定时器异步')
      timer = null;
    }, wait)
    console.log('执行表单函数')
    if (callNow) func.apply(context, args)
  }
}
var btn = document.getElementById("btn");
function submitForm() {
  console.log('提交表单')
}
btn.onclick = debounce(submitForm, 2000)
</script>
</body>
</html>


很多人看不懂防抖函数的调用过程(节流是一样的)。下面说的仔细一点,一步步来。
我们先来模拟一个场景——提交表单。使用防抖来防止重复触发。为了方便查看效果,定义防抖时间为3000ms
①首先,js主执行栈执行到31行时候,执行了防抖函数,此时,会定义一个timer在防抖函数作用域中(控制天输出“绑定监听函数”)。并且返回了一个不具名函数。接着会监听click事件是否触发。
click触发后:
②我们是将提交表单的函数作为参数传递给了防抖函数debounce,也就是里面的形参func。防抖函数接收了参数func和wait后,会执行之前返回的不具名函数。
③第一次触发不具名函数时候,callNow为true,执行函数submitForm(输出“执行表单函数”)。此时的延迟回调会进入异步队列等待执行。
④wait时间后,回调执行,timer再次变为null。(输出“定时器异步”)