阅读 430

JS中防抖在思考(避免连续点击查询)

前几天在项目中看见防止查询多次点击的一种写法

   /**
    * 类似于一种简单的防抖思想的实现
    */
    search = () => {
        clearTimeout(this.timer);
        this.timer = setTimeout(() => {
            fetch('url').then((res) => {
                //todo
            }).catch(() => {
                //todo
            })
        } ,1000);
    }
复制代码

但是这样会存在一种问题:

第一种情况(默认setTimeOut的时间为1秒时)

1.当用户点击一次时,其实所用的时间为接口请求时间加上1秒,当接口响应时间只有几十毫秒时,这种做法就有点鸡肋。

2.当用户连续点击10次时,其实前9次请求都被清除掉了(因为我们连续点击之间的时间间隔肯定会小于1秒),只有最后一次点击被执行,但是查询的时间变为了1秒加上接口请求时间

第二种情况(尽量的缩短setTimeOut的时间,比如50ms)

1.当用户点击一次,其实所用的时间为接口请求时间加上50毫秒

2.当用户点击10次时,假如说接口请求时间为30毫秒,而用户点击的时间间隔为200毫秒,也就是说当setTimeOut的时间加上接口请求时间小于用户点击的时间间隔时,并没有限制住,其实相当于执行10次。

我想听一下你对这种写法的理解,也不知道我对这种写法理解对不对 ?

“其实在项目开发中较常用的一种写法时,因为我们大部分的查询都添加了loading效果,而且loading效果的显示是通过this.state.isLoading来进行判断的,那么这样就好判断了,点击查询时先判断this.state.isLoading是否为false,当为false时,下发请求,为true时,给出正在努力查询中的提示。”

再回顾一下防抖概念:

防抖(debounce)

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

我们先来看一个袖珍版的防抖理解一下防抖的实现(类似项目中防止查询多次点击):

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
   // 缓存一个定时器id
   let timer = 0
   // 这里返回的函数是每次用户实际调用的防抖函数
   // 如果已经设定过定时器了就清空上一次的定时器
   // 开始一个新的定时器,延迟执行用户传入的方法
   return function(...args) {
     if (timer) clearTimeout(timer)
     timer = setTimeout(() => {
       func.apply(this, args)
     }, wait)
   }
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数

复制代码

其实我的理解就是防抖这种用法用户防止用户多次点击查询好像不太特别的合适。其实它更多的场景是用于:搜索引擎搜索问题、用户放缩窗口大小等场景。

  • 上面是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用: 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  • 例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
也就是把防抖函数改成可以支持防止点击多次查询的情况:

实现一个带有立即执行选项的防抖函数

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}
复制代码

上面函数的实现思路:

  1. 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为null,就可以再次点击了。
  2. 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数

其实在项目开发中个人更喜欢通过loading这种效果,来做到防止用户连续点击查询。