美文网首页lodash
在 Vue 中使用lodash对事件进行防抖和节流

在 Vue 中使用lodash对事件进行防抖和节流

作者: 刘其瑞 | 来源:发表于2020-06-23 18:41 被阅读0次

    有些浏览器事件可以在短时间内快速触发多次,比如调整窗口大小或向下滚动页面。例如,监听页面窗口滚动事件,并且用户持续快速地向下滚动页面,那么滚动事件可能在 3 秒内触发数千次,这可能会导致一些严重的性能问题。

    如果在面试中讨论构建应用程序,出现滚动、窗口大小调整或按下键等事件请务必提及 防抖(Debouncing) 和 函数节流(Throttling)来提升页面速度和性能。这两兄弟的本质都是以闭包的形式存在。通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

    Throttle:第一个人说了算
    throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

    先给大家讲个小故事:现在有一个旅客刚下了飞机,需要用车,于是打电话叫了该机场唯一的一辆机场大巴来接。司机开到机场,心想来都来了,多接几个人一起走吧,这样这趟才跑得值——我等个十分钟看看。于是司机一边打开了计时器,一边招呼后面的客人陆陆续续上车。在这十分钟内,后面下飞机的乘客都只能乘这一辆大巴,十分钟过去后,不管后面还有多少没挤上车的乘客,这班车都必须发走。

    在这个故事里,“司机” 就是我们的节流阀,他控制发车的时机;“乘客”就是因为我们频繁操作事件而不断涌入的回调任务,它需要接受“司机”的安排;而“计时器”,就是我们上文提到的以自由变量形式存在的时间信息,它是“司机”决定发车的依据;最后“发车”这个动作,就对应到回调函数的执行。

    总结下来,所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。只要一位客人叫了车,司机就会为他开启计时器,一定的时间内,后面需要乘车的客人都得排队上这一辆车,谁也无法叫到更多的车。

    对应到实际的交互上是一样一样的:每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“一辆车的乘客”——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。

    现在一起实现一个 throttle:

    // fn是我们需要包装的事件回调, interval是时间间隔的阈值
    function throttle(fn, interval) {
      // last为上一次触发回调的时间
      let last = 0
     
      // 将throttle处理结果当作函数返回
      return function () {
          // 保留调用时的this上下文
          let context = this
          // 保留调用时传入的参数
          let args = arguments
          // 记录本次触发回调的时间
          let now = +new Date()
     
          // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
          if (now - last >= interval) {
          // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
              last = now;
              fn.apply(context, args);
          }
        }
    }
     
    // 用throttle来包装scroll的回调
    const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
    

    Debounce:最后一个人说了算
    防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

    继续讲司机开车的故事。这次的司机比较有耐心。第一个乘客上车后,司机开始计时(比如说十分钟)。十分钟之内,如果又上来了一个乘客,司机会把计时器清零,重新开始等另一个十分钟(延迟了等待)。直到有这么一位乘客,从他上车开始,后续十分钟都没有新乘客上车,司机会认为确实没有人需要搭这趟车了,才会把车开走。

    我们对比 throttle 来理解 debounce:在throttle的逻辑里,“第一个人说了算”,它只为第一个乘客计时,时间到了就执行回调。而 debounce 认为,“最后一个人说了算”,debounce 会为每一个新乘客设定新的定时器。

    现在一起实现一个 debounce:

    // fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
    function debounce(fn, delay) {
      // 定时器
      let timer = null
     
      // 将debounce处理结果当作函数返回
      return function () {
        // 保留调用时的this上下文
        let context = this
        // 保留调用时传入的参数
        let args = arguments
     
        // 每次事件被触发时,都去清除之前的旧定时器
        if(timer) {
            clearTimeout(timer)
        }
        // 设立新定时器
        timer = setTimeout(function () {
          fn.apply(context, args)
        }, delay)
      }
    }
     
    // 用debounce来包装scroll的回调
    const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)
    

    用 Throttle 来优化 Debounce
    debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

    为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:

    // fn是我们需要包装的事件回调, delay是时间间隔的阈值
    function throttle(fn, delay) {
      // last为上一次触发回调的时间, timer是定时器
      let last = 0, timer = null
      // 将throttle处理结果当作函数返回
     
      return function () { 
        // 保留调用时的this上下文
        let context = this
        // 保留调用时传入的参数
        let args = arguments
        // 记录本次触发回调的时间
        let now = +new Date()
     
        // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
        if (now - last < delay) {
        // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
           clearTimeout(timer)
           timer = setTimeout(function () {
              last = now
              fn.apply(context, args)
            }, delay)
        } else {
            // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
            last = now
            fn.apply(context, args)
        }
      }
    }
     
    // 用新的throttle包装scroll的回调
    const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
     
    document.addEventListener('scroll', better_scroll)
    

    重点来了:在 Vue 里使用 lodash 中的 Debouncing 和 Throttling

    事件节流和防抖是提高性能或降低网络开销的好方法。虽然 Vue 1曾经支持对事件的节流和防抖,但是在Vue 2中为了保持核心的简单性,删除对事件的节流和防抖的支持。因此,在Vue 2对对事件进行防抖和节流我们可以使用 lodash 来做。

    可以通过 yarn 或 npm 安装 lodash。

    <script src="lodash.js"></script>
    # Yarn
    $ yarn add lodash
    # NPM
    $ npm install lodash --save
    

    注意:如果我们不想导入lodash的所有内容,而只导入所需的部分,则可以通过一些Webpack构建自定义来解决问题。还可以使用lodash.throttle和lodash.debounce等软件包分别安装和导入lodash的各个部分。

    throttle 方法:第一个人说了算
    throttle API:
    _.throttle(func, [wait=0], [options={}])
    func (Function): 要节流的函数。
    [wait=0] (number): 需要节流的毫秒数。
    [options={}] (Object): 选项对象。
    [options.leading=true] (boolean): 指定调用在节流开始前,默认true。
    [options.trailing=true] (boolean): 指定调用在节流结束后,默认true。

    要对事件进行节流处理方法非常简单,只需将要调用的函数包装在lodash的_.throttle函数中即可。

    <template>
      <button @click="throttledMethod()">Click me as fast as you can!</button>
    </template>
     
    <script>
    import _ from 'lodash'
     
    export default {
      methods: {
        throttledMethod: _.throttle(function() {
        //注意这里如果使用箭头函数的话, this为 undefined  https://github.com/vue-styleguidist/vue-docgen-api/issues/23
          console.log("throttle");
        }, 5000, {
          leading: true,
          trailing: false
      })
    }
    </script>
    

    testThrottle方法被绑定在一个按钮上,demo最终的效果是 :
    1、按钮点击后控制台立马打印了throttle——19:39:00;
    2、5秒内点击多次按钮,最终只打印一次throttle——19:39:05前;
    3、5秒后再点击一次,会重新打印throttle——19:39:05后;
    PS:lodash默认trailing为true,那么最终的效果是在点击时会立即打印throttle,且5秒后又会再打印一次,即节流之前和之后都会执行该节流函数。

    debounce 方法:最后一个人说了算
    尽管节流在某些情况下很有用,但一般情况我们经常使用的是防抖。防抖实质上将我们的事件分组在一起,并防止它们被频繁触发。要在Vue组件中使用节流,只需将要调用的函数包装在lodash的_.debounce函数中。

    debounce API:
    _.debounce(func, [wait=0], [options={}])
    func (Function): 要防抖动的函数。
    [wait=0] (number): 需要延迟的毫秒数。
    [options={}] (Object): 选项对象。
    [options.leading=false] (boolean): 指定在延迟开始前调用,默认false。
    [options.maxWait] (number): 设置 func 允许被延迟的最大值。
    [options.trailing=true] (boolean): 指定在延迟结束后调用,默认true。

    <template>
      <button @click="throttledMethod()">Click me as fast as you can!</button>
    </template>
     
    <script>
    import _ from 'lodash'
     
    export default {
      methods: {
        testDebounce: _.debounce(function() {
        //注意这里如果使用箭头函数的话, this为 undefined  https://github.com/vue-styleguidist/vue-docgen-api/issues/23
          console.log("debounce");
        }, 2000, {
          leading: true,
          trailing: false
        })
      }
    }
    </script>
    

    testDebounce方法被绑定在一个按钮上,demo最终的效果是 :

    1、按钮点击后控制台立马打印了debounce——19:39:00;

    2、5秒内点击多次按钮,最终只打印一次debounce——19:39:05前,假设19:39:04完成了最后一次点击;

    3、相对于最后一次点击的5秒后再点击一次,会重新打印debounce——19:39:09后;

    PS:lodash默认leading为false、trailing为true,那么最终的效果是在点击后等待5秒才会打印debounce,即延迟之前不执行函数,而是在延迟之后执行。

    相关文章

      网友评论

        本文标题:在 Vue 中使用lodash对事件进行防抖和节流

        本文链接:https://www.haomeiwen.com/subject/guohfktx.html