002-手写源码之节流防抖

154 阅读4分钟

002-手写源码之节流防抖

一、防抖

1. 什么是防抖

一句话来说,就是防止用户手抖,多点了几次按钮,这种情况下我们应该只处理最后一次的点击。即在多次操作中,只有在最后一次操作后,等待一段时间不再有新的操作了,才执行。

一般在提交表单或者向后端查询字段是否重复时会使用到,避免多次向服务器发起同样的请求。

<body>
<input type="text">
<script src="./debounce.js"></script>
<script>
  const input = document.querySelector('input')
  function handleInput(event) {
   console.log('发送网络请求:',event.target.value)
  }
  input.oninput = handleInput
</script>
</body>

在上面的示例中,我们每输入一个字符,就会发起一次网络请求,用户想要模糊查询test,而我们在te的时候就向后端发起查询请求,这显然是不合理的。一般来说,我们在输完一个关键词之后,会略微停顿一下,也就是说:在一段时间内,用户在完成最后一次输入后就会停顿一下,这个时候我们拿到的大概率是一个完整的关键词,这时候才查询是比较合理的。这就比较适合防抖。

2. 怎么实现防抖

首先,我们来分析一下,“在多次操作中,只有在最后一次操作后,等待一段时间不再有新的操作了,才执行”,也就是说,我们的函数要接受一个时间参数,然后在这段时间内,每一次调用,我们都要取消上一次的执行,并且等待一段时间看是否有新的操作。我们可以考虑使用定时器来实现

const debounce = (fn, delay=1000) => {
  let timer // timer作为flag
  return () => { // 利用闭包来存储timer,这样多次调用本函数也还是同一个timer
    if (timer) {
      clearTimeout(timer) // 之前已经在等待执行了,那就清除之前的等待,重新开始等待
    }
    timer = setTimeout(fn, delay)
  }
}

这样我们就实现了一个简单版的防抖函数。但是很多时候我们在处理事件的时候都是需要传递参数的,这就需要我们再小小地改造一下

const debounce = (fn, delay=1000) => {
  let timer // timer作为flag
  return (...args) => { // 利用闭包来存储timer,这样多次调用本函数也还是同一个timer
    if (timer) {
      clearTimeout(timer) // 之前已经在等待执行了,那就清除之前的等待,重新开始等待
    }
    timer = setTimeout(()=> {
      fn(args)
    }, delay)
  }
}

有时候我们希望用户第一次点击的时候就处理了,后面一段时间内都不再处理。

const debounce = (fn, delay=1000, immediate) => {
  let timer // timer作为flag
  let isStarted = false // 利用闭包全局维护是否立即执行过的flag
  return (...args) => { // 利用闭包来存储timer,这样多次调用本函数也还是同一个timer
    if (immediate && !isStarted) {
      fn(args)
      isStarted = true // 标记为已经执行了
    } else {
      if (timer) {
        clearTimeout(timer) // 之前已经在等待执行了,那就清除之前的等待,重新开始等待
      }
      timer = setTimeout(()=> {
        fn(args) // 不过不想最后再执行一次,去掉这里的调用即可
        isStarted = false // 等待时间已到,把状态改为下次可以立即执行
      }, delay)
    }
  }
}

二、节流

1. 什么是节流

有些操作本身就是要频繁进行的,但是我们又不希望他过于频繁,例如,lol当中,我们可能1秒钟点了10下攻击键,但是我们受攻速限制,只会真正攻击2下。这就是节流--控制水流流动速度。

2. 怎么实现节流

有上面的例子,我们就比较容易推断出来了,我们需要判断本次触发与上次执行的时间差,是否大于我们设置的时间间隔,大于则本次可以执行,小于则不能执行。

const throttle = (fn, interval) => {
  let lastTime = 0 // 用闭包来维护,避免每次都生成一个新的lastTime
  return (...args) => {
    const nowTime = Date.now()
    const canExecute = interval <= nowTime - lastTime
    if (canExecute) {
      fn(args)
      lastTime = nowTime
    }
  }
}

上面这种写法,默认会在第一次调用的时候立即执行(即,首执行),如果我们想在第一次调用的时候不立即执行的话,需要改造一下

const throttle = (fn, interval, options={ leading: true }) => {
  const { leading } = options
  let lastTime = 0 // 用闭包来维护,避免每次都生成一个新的lastTime
  return (...args) => {
    const nowTime = Date.now()
    
    if (!lastTime && !leading) {
      // 第一次执行,且设置了首次不执行,把上次时间设置当前时间,待会儿判断执行间隔差的时候就是0,就能实现首次不执行了
      lastTime = nowTime
    }
    
    const canExecute = interval <= nowTime - lastTime
    
    if (canExecute) {
      fn(args)
      lastTime = nowTime
    }
  }
}

上面的实现,如果用户的倒数第二次和最后一次点击相差时间小于设定的间隔差,那么最后一次点击就不会生效,如果我们希望最后一次生效(尾执行)的话,还需要稍加改造

const throttle = (fn, interval, options={ leading: true, trailing:true }) => {
  const { leading, trailing } = options
  let lastTime = 0 
  let timer = null // 跟防抖一样,用于判断是否是最后一次
  return (...args) => {
    const nowTime = Date.now()
    
    if (!lastTime && !leading) {
      lastTime = nowTime
    }
    
    const canExecute = interval <= nowTime - lastTime
    
    if (canExecute) {
      fn(args)
      lastTime = nowTime
    }
    
    // 尾执行的主要逻辑
    
    if (!canExecute && trailing) {
      // 两次时间差小于设定值且开启了尾执行
      if (timer) {
        // 之前那次不是最后一次
        clearTimeout(timer)
        timer = null
      }
      // 因为不知道这到底是不是最后一次,所以定时器等待一下
      timer = setTimeout(() => {
        fn(args)
        lastTime = trailing ? 0: Date.now() // 尾执行则重置变量
      }, interval - (nowTime - lastTime)) // 等待时间是离设定值剩余的时间
    }
    
  }
}

三、总结

  • 防抖:如果事件被频繁触发,防抖能保证只后最有一次触发生效,前面N多次的触发都会忽略
  • 节流:如果事件被频繁触发,节流能够减少事件触发频率,因此,节流是有选择地执行一部分事件