最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • input框中防抖的4种应用场景

    正文概述 掘金(专业逮虾户aaa)   2021-05-02   415

    一、为什么要写这个文章?

    节流/防抖的文章想必大家也看过了很多,大多数都是分为立即执行非立即执行两种版本的。最近在使用的过程的中,发现这两个版本在input框输入的时候得到的效果都不理想。下面就一起来看一下对应的场景,以及优化方式。

    二、使用场景

    1. 非立即执行

    非立即执行的版本就是在连续输入的时候,只要输入的间隔不超过设置的时间间隔,那么这个事件就会只执行一次,也就是最末尾的那一次。

    • 对应的代码为:
    /**
     * 防抖函数非立即执行版
     * @author waldon
     * @date 2021-05-01
     * @returns {Function} - 防抖函数
     */
    const debounce = (function () {
      let timer = 0
      return function (fn, delay = 300) {
        if (timer) {
          clearTimeout(timer)
        }
        // 非立即执行
        timer = setTimeout(() => {
          fn()
        }, delay)
      }
    })()
    
    • 效果展示:

    input框中防抖的4种应用场景

    • 缺陷

    通过上面的动图,可以很清晰的看到,input的值改变后,要等300毫秒,事件才会执行。但是vue的v-model中使用了compositionstartcompositionend帮我们对中文输入进行了优化,一般用户打出第一个词的时候,就想到看到对应的搜索结果了。这种防抖牺牲了用户在速度上的体验。

    2. 立即执行

    立即执行的版本就是在连续输入的时候,只触发第一个关键词改变时的事件。除非后面输入的间隔大于设置的时间间隔,否则不会再次触发

    • 对应的代码为:
    /**
     * 防抖函数立即执行版
     * @author waldon
     * @date 2021-05-01
     * @returns {Function} - 防抖函数
     */
    const debounce = (function () {
      let timer = 0
      return function (fn, delay = 300, immediate = true) {
        if (timer) {
          clearTimeout(timer)
        }
        if (immediate) {
          const callNow = !timer
          timer = setTimeout(() => {
            timer = 0
          }, delay)
          if (callNow) {
            fn()
          }
        } else {
          // 非立即执行
          timer = setTimeout(() => {
            fn()
          }, delay)
        }
      }
    })()
    
    • 效果展示:

    input框中防抖的4种应用场景

    • 缺陷

    这个立即执行的防抖在input框中使用算是有bug的。

    1. 输入快的时候,只会搜索第一个关键词,后面的都会被忽略掉。如果使用在搜索列表中的话,这个搜索结果肯定是不对的
    2. 长按back/delete键删除,频率也是很快的。即使到后面删完了,显示的搜索结果还是删除第一个关键词的那个搜索结果。

    这时候,细心一点的朋友就会想到了。那在输入的头和尾都触发一次不就可以了嘛?我们继续来看第三种。

    3. 立即执行 + 延迟执行

    这个的效果就是前面两种结合。会在输入第一个关键词的时候执行一次逻辑,然后中间连续输入的话不会执行,等到停止输入的时候,再执行最后一次输入的逻辑。

    • 对应的代码为:
    /**
     * 防抖函数重复执行版
     * @author waldon
     * @date 2021-05-01
     * @returns {Function} - 防抖函数
     */
    const debounce = (function () {
      let timer = 0
      return function (fn, delay = 300, immediate = true) {
        if (timer) {
          clearTimeout(timer)
        }
        if (immediate) {
          const callNow = !timer
          timer = setTimeout(() => {
            fn() // 比立即执行的版本多了这一步
            timer = 0
          }, delay)
          if (callNow) {
            fn()
          }
        } else {
          // 非立即执行
          timer = setTimeout(() => {
            fn()
          }, delay)
        }
      }
    })()
    
    • 效果展示:

    input框中防抖的4种应用场景

    • 点评

    这个版本已经能解决 延迟 和 执行结果不准确 这两个问题了。但是更细心的朋友可能就会发现了:“上面的动图中,只输入了一个关键词的时候,也执行了两次。”如果项目里面没有处理重复请求的逻辑的话,那岂不是要发两个重复的请求?那肯定得优化一下了。

    4. 立即执行 + 延迟执行 + cacheKey

    这个和第三个版本的效果是一致的,只是加入了一个cacheKey的字段作为缓存值来判断上一次输入的值是否一致,避免执行重复的逻辑。

    • 对应的代码为:
    /**
     * 防抖函数cacheKey版
     * @author waldon
     * @date 2021-05-01
     * @returns {Function} - 防抖函数
     */
    const debounce = (function () {
      let timer = 0
      let cacheKey = ''
      return function (fn, delay = 300, immediate = true, key = '') {
        if (timer) {
          clearTimeout(timer)
        }
        if (immediate) {
          // 立即执行
          let callNow = !timer
          timer = setTimeout(() => {
            timer = 0
            if (cacheKey !== key) {
              fn()
            }
          }, delay)
          if (callNow) {
            cacheKey = key
            fn()
          }
        } else {
          // 非立即执行
          timer = setTimeout(() => {
            fn()
          }, delay)
        }
      }
    })()
    
    • 效果展示:

    input框中防抖的4种应用场景

    • 点评

    解决了关键词一样还会重复执行的问题。在input事件里面可以把input的value值作为key,pageScroll事件的话可以将scrollTop的值作为key。这个其实是能应付大部分场景的,但是有些比较特殊的场景,他连续触发的时候,没有传改变的值的,那这样肯定不适用了。

    5. 立即执行 + 延迟执行 + lastTimer

    这个版本其实是看了lodash的源码后思考出来的。大致的思路就是,在第一次定时任务定义的时候,把任务池的id也赋值给另一个变量。当连续触发后,timer是会一直变的,而最开始赋值的lastTimer是不会变的。判断这两个值不一致的时候,不触发回调函数,等到连续触发的行为停止之后,再在回调函数里面重置这两个变量。

    • 对应的代码为:
    /**
     * 防抖函数lastTimer版
     * @author waldon
     * @date 2021-05-01
     * @returns {Function} - 防抖函数
     */
    const debounce = (function () {
      let timer = 0
      let lastTimer = 0
      return function (fn, delay = 300, immediate = true) {
        if (timer) {
          clearTimeout(timer)
        }
        if (immediate) {
          // 立即执行
          let callNow = !timer
          timer = setTimeout(() => {
            if (lastTimer !== timer) {
              timer = 0
              lastTimer = 0
              fn()
            }
          }, delay)
          if (callNow) {
            lastTimer = timer
            fn()
          }
        } else {
          // 非立即执行
          timer = setTimeout(() => {
            fn()
            timer = 0
          }, delay)
        }
      }
    })()
    
    • 效果展示:

    这里的效果和第4版是一样的,就不重复贴图了。

    • lodash的debounce实现源码
    function debounce(func, wait, options) {
      let lastArgs,
        lastThis,
        maxWait,
        result,
        timerId,
        lastCallTime
    
      let lastInvokeTime = 0
      let leading = false
      let maxing = false
      let trailing = true
    
      // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
      const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
    
      if (typeof func !== 'function') {
        throw new TypeError('Expected a function')
      }
      wait = +wait || 0
      if (isObject(options)) {
        leading = !!options.leading
        maxing = 'maxWait' in options
        maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
        trailing = 'trailing' in options ? !!options.trailing : trailing
      }
    
      function invokeFunc(time) {
        const args = lastArgs
        const thisArg = lastThis
    
        lastArgs = lastThis = undefined
        lastInvokeTime = time
        result = func.apply(thisArg, args)
        return result
      }
    
      function startTimer(pendingFunc, wait) {
        if (useRAF) {
          root.cancelAnimationFrame(timerId)
          return root.requestAnimationFrame(pendingFunc)
        }
        return setTimeout(pendingFunc, wait)
      }
    
      function cancelTimer(id) {
        if (useRAF) {
          return root.cancelAnimationFrame(id)
        }
        clearTimeout(id)
      }
    
      function leadingEdge(time) {
        // Reset any `maxWait` timer.
        lastInvokeTime = time
        // Start the timer for the trailing edge.
        timerId = startTimer(timerExpired, wait)
        // Invoke the leading edge.
        return leading ? invokeFunc(time) : result
      }
    
      function remainingWait(time) {
        const timeSinceLastCall = time - lastCallTime
        const timeSinceLastInvoke = time - lastInvokeTime
        const timeWaiting = wait - timeSinceLastCall
    
        return maxing
          ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
          : timeWaiting
      }
    
      function shouldInvoke(time) {
        const timeSinceLastCall = time - lastCallTime
        const timeSinceLastInvoke = time - lastInvokeTime
    
        // Either this is the first call, activity has stopped and we're at the
        // trailing edge, the system time has gone backwards and we're treating
        // it as the trailing edge, or we've hit the `maxWait` limit.
        return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
          (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
      }
    
      function timerExpired() {
        const time = Date.now()
        if (shouldInvoke(time)) {
          return trailingEdge(time)
        }
        // Restart the timer.
        timerId = startTimer(timerExpired, remainingWait(time))
      }
    
      function trailingEdge(time) {
        timerId = undefined
    
        // Only invoke if we have `lastArgs` which means `func` has been
        // debounced at least once.
        if (trailing && lastArgs) {
          return invokeFunc(time)
        }
        lastArgs = lastThis = undefined
        return result
      }
    
      function cancel() {
        if (timerId !== undefined) {
          cancelTimer(timerId)
        }
        lastInvokeTime = 0
        lastArgs = lastCallTime = lastThis = timerId = undefined
      }
    
      function flush() {
        return timerId === undefined ? result : trailingEdge(Date.now())
      }
    
      function pending() {
        return timerId !== undefined
      }
    
      function debounced(...args) {
        const time = Date.now()
        const isInvoking = shouldInvoke(time)
    
        lastArgs = args
        lastThis = this
        lastCallTime = time
    
        if (isInvoking) {
          if (timerId === undefined) {
            return leadingEdge(lastCallTime)
          }
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = startTimer(timerExpired, wait)
            return invokeFunc(lastCallTime)
          }
        }
        if (timerId === undefined) {
          timerId = startTimer(timerExpired, wait)
        }
        return result
      }
      debounced.cancel = cancel
      debounced.flush = flush
      debounced.pending = pending
      return debounced
    }
    

    三、总结

    这里的使用场景只是针对用户量特别大或者请求特别耗性能的情况,如果服务器的压力允许的话,使用节流在适当的间隔时间给用户一定的反馈,其实用户体验会更好一些。

    最后,五一快乐~

    四、参考资源

    • github.com/lodash/loda…

    起源地下载网 » input框中防抖的4种应用场景

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元