深入 JS 之分析防抖与节流

发布于 2022-10-03 22:26:34 字数 5826 浏览 106 评论 0

防抖

防抖是什么?

对于一定时间段的连续的函数调用,只让其执行一次。

常用于 DOM 事件的监听回调中:核心是维护一个定时器,事件被触发 n 秒后再执行回调,如果 n 秒内又被触发,则重新计时。

什么时候应用防抖

前端开发中会遇到一些频繁的事件触发,如:

  1. window 的 resize、scroll
  2. mousedown、mousemove
  3. keyup、keydown

这些事件触发后都会执行一个回调函数,如果短时间内有很多次事件触发,则浏览器需要短时间内执行很多次回调,如果次数过于频繁,浏览器会反应不过来,就会出现 卡顿 现象,此时就需要防抖/节流。

举个例子:

// 模拟一段ajax请求
    function ajax(context) {
      console.log('ajax request' + context);
    }

    let inputa = document.getElementById('unDebounce');

    inputa.addEventListener('keyup', function(e) {
      ajax(e.target.value);
    })

此处也频繁执行了 keyup 的回调,因为这个例子很简单,所以浏览器完全反应的过来,但例子复杂的话,浏览器就会有卡顿了。此时我们可以通过 防抖节流 来解决。

跟着 underscore 的防抖代码学习

核心是维护一个定时器,事件被触发n秒后再执行回调,如果n秒内又被触发,则重新计时。

    // 模拟一段ajax请求
    function ajax(context) {
      console.log('ajax request' + context.target.value);
    }
    
    function debounce(fn, wait) {
      var timeout = null;

      return function() {
        var context = this; // this指向DOM元素
        var args = arguments; // JavaScript 在事件处理函数中会提供事件对象 event 作为参数。

        clearTimeout(timeout);
        timeout = setTimeout(function() {
          fn.apply(context, args)
        }, wait)
      }
    }

    let inputb = document.getElementById('Debounce');

    inputb.addEventListener('keyup', debounce(ajax, 1000))

可以看到,我们加入了防抖以后,当你在频繁的输入(事件频繁被触发)时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。

完善防抖函数 debounce

立即执行

上面的 debounce 函数已经可以满足很多场景了,但如果有这样一个需求:

我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

我们可以加个 immediate 参数判断是否是立刻执行,如下:

function debounce(fn, wait, immediate) { // immediate为立即执行一次的开关
      var timeout = null;
      return function() {
        // 一开始立即执行一次fn,若在wait时间内再次触发事件执行回调,callNow 会为 false,不会调用到fn。若在wait时间后再触发事件则会再次立即执行。
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
          var callNow = !timeout;
          timeout = setTimeout(function() {
            timeout = null;
          }, wait);
          if (callNow) fn.apply(context, args);
        } else {
          timeout = setTimeout(function() {
            fn.apply(context, args);
          }, wait)
        }
      }
    }

返回值

getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。

function debounce(fn, wait, immediate) {
      var timeout = null;
      var result;
      return function() {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
          var callNow = !timeout;
          timeout = setTimeout(function() {
            timeout = null;
          }, wait);
          if (callNow) result = fn.apply(context, args);
        } else {
          timeout = setTimeout(function() {
            fn.apply(context, args);
          }, wait)
        }
        return result;
      }
    }

手动取消防抖

希望能取消 debounce 函数,比如说 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行了。

其实很简单,我们在函数上挂个 cancel 方法就行了:

function debounce(func, wait, immediate) {

    var timeout = null, result;

    var debounced = function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    };

    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced;
}

比如,需要用一个按钮,点击一下就可以取消,就可以这样使用:

var xxx = debounce(fn, 10000, true);
inputb.addEventListener('keyup', xxx));

document.getElementById("button").addEventListener('click', function(){
    xxx.cancel();
})

至此我们就实现了一个 underscore 中的 debounce 函数。

节流

节流是什么?

让一个函数不要执行得太频繁,减少一些过快的调用来节流

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

也常用于在DOM事件的监听回调中:如果你持续触发事件,每隔一段时间,只执行一次事件。

什么时候应用节流

函数节流有哪些应用场景?哪些时候我们需要间隔一定时间触发回调来控制函数调用频率?

  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次。

按上面的例子:

function throttle(fun, delay) {
        let last, deferTimer
        return function (args) {
            let that = this
            let _args = arguments
            let now = +new Date()
            if (last && now < last + delay) { // 在设置的间隔时间内执行。这段代码保证在间隔时间内停止触发时,delay秒后还会执行一次。
                clearTimeout(deferTimer)
                deferTimer = setTimeout(function () {
                    last = now
                    fun.apply(that, _args)
                }, delay)
            }else { // 一开始立即执行
                last = now
                fun.apply(that,_args)
            }
        }
    }

小结

函数节流和函数去抖的核心其实就是限制某一个方法被频繁触发,而一个方法之所以会被频繁触发,大多数情况下是因为 DOM 事件的监听回调,而这也是函数节流以及去抖多数情况下的应用场景。

按照DOM事件的监听回调来说两者区别的话:防抖是只有等事件停止触发后 n 秒才执行函数,节流是持续触发的时候,每 n 秒执行一次函数。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

苏辞

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

胡图图

文章 0 评论 0

zt006

文章 0 评论 0

z祗昰~

文章 0 评论 0

冰葑

文章 0 评论 0

野の

文章 0 评论 0

天空

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文