JS专题之去抖函数

2018-12-08 admin

一、前言

为什么会有去抖和节流这类工具函数?

在用户和前端页面的交互过程中,很多操作的触发频率非常高,比如鼠标移动 mousemove 事件, 滚动条滑动 scroll 事件, 输入框 input 事件, 键盘 keyup 事件,浏览器窗口 resize 事件。

在以上事件上绑定回调函数,如果回调函数是一些需要大量计算、消耗内存、HTTP 请求、DOM 操作等,那么应用的性能和体验就会非常的差。

去抖和节流函数的根据思想就是,减少高频率事件处理函数 handler 的执行频率(注意是事件处理函数,不是事件回调函数),将多次事件的回调合并成一个回调来执行,从而优化性能。

二、简单版去抖(debounce)

去抖(debounce),也叫防抖,那抖动指的是什么呢?抖动意味着操作的不稳定性,你可以理解成躁动症,安静不下来~防抖的含义便是为了防止抖动造成的结果不准确,等到稳定的时候再处理结果。

比如在输入事件,鼠标移动,滚动条滑动,键盘敲击事件中,等到停止事件触发,频率稳定为零后,才开始执行回调函数,也就是所谓的没有抖动后处理。

个人总结:去抖,就是事件触发频率稳定后,才开始执行回调函数, 一连串的事件触发,但只进行一次事件处理。

频率就是单位时间触发的次数,如果单位时间内,事件触发超过一次,就只执行最后一次,如果单位时间内没有触发超过一次,那就正常执行。去抖分为延迟执行和立即执行两种思路。

看一个简单版的去抖函数延迟执行实现:

<div>
    输入框: <input type="text" id="exampleInput">
</div>
<script>
window.onload = function() {
    var inputEl = document.getElementById("exampleInput");

    inputEl.oninput = debounce(ajax); // debouce 函数执行了,返回一个函数,该函数为事件的回调函数

    // 事件真正的处理函数(handler),参数是回调函数传递过来的。
    // 常见场景就是边输入查询关键字,边请求查询数据,比如百度的首页搜索
    function ajax(event) {
        console.log("HTTP 异步请求:", event.target.value);
        // $.ajax() 请求数据 ...
    }

    function debounce(func, delay) {  // 参数为传入的事件处理函数和间隔时间
        var interval = delay || 1000;
        var timer = null;  // 闭包保存的 timer 变量,会常驻内存

        return function(args) { // 返回的匿名函数是事件的回调函数,在事件触发时执行,参数为 DOM 事件对象(event)

            var context = this; // 事件的回调函数中,this 指向事件的绑定的 DOM 元素对象(HTMLElement)

            console.log(timer);
            clearTimeout(timer); // 如果事件回调函数中存在定时器,则清空上次定时器,重新计时。如果间隔时间到后,处理函数自然就被执行了。
            timer = setTimeout(function() {
                func.call(context, args); // 定时器时间到后,执行事件真正的处理函数 handler
                // 执行的事件处理函数(handler),需要把调用对象 this 和事件对象 传递过去,就像没被debounce处理过一样
            }, interval)
        }
    }
}
</script>

上面代码中我的注释已经能够说明整个去抖的过程,再来啰嗦几句话~

  1. debounce 函数在主线程顺序执行时已经被调用,传入的参数一个是真正想在事件触发执行的事件处理函数
  2. 另一个参数是事件触发的间隔时间,间隔时间内再次触发事件,则重新计时,类似于罚你 5 分钟内不准说话,时间到后就可以开始说话,如果 5 分钟内说话了,则再罚你 5 分钟内不准说话,以此类推~
  3. debounce 函数有一个 timer 内部变量,timer 在返回的执行函数中被访问,形成了闭包,有关闭包的内容,可以翻看我之前的文章《JavaScript之闭包》
  4. bebounce 函数返回的匿名函数才是 input 事件的回调函数,所以该匿名函数有一个默认参数 event 对象。
  5. 同第 4 点,匿名函数是 dom 元素注册事件的回调函数,所以匿名函数(回调函数)的 this 指向 HTMLInput 元素。
  6. 同第 2 点,触发函数后,如果发现闭包中保存着 timer 变量, timer 变量初始值为 null, 之后触发定时器后,timer 为当次定时器的 id,id 是一个数字。去抖的过程在于,如果在定时器的间隔时间内触发了函数,它会把上一次事件触发时定义的定时器清除,又重新定义一个定时器。如果本次定时器没有被清除,时间到后就会自然执行事件处理函数。对这个过程有困惑的同学,可以把 timer 变量在 clearTimeout 之前打印出来就明白了。
  7. 延时执行了事件处理函数(handler),需要传递调用对象和事件对象过去,此处 call 可以和 apply 互换,如果用 apply, 传递 arguments 类数组即可。这样保证了参数的一致性,就像没被 debounce 处理过一样。

以上就是去抖函数的基本思想, 可以参考示意图

下面这张图是高设 3 里讲的节流函数,其实是这一节所说的去抖函数,高设 3 将 timer 变量用传入的处理函数的属性代替了而已。

三、立即执行

第二节的简单版去抖函数能满足大部分只需要触发一次事件处理的去抖场景:输入数据查询事件,下拉滚动条到窗口底部懒加载数据。

但是有一个问题,假如我想输入框输入内容时,第一个字输完就请数据怎么做? 你可以理解为,你可以马上开始说话,但是说完话后 5 分钟不能说话,如果 5 分钟内说话,则接下来再加 5 分钟不能说话。如果 5 分钟后没说话, 那么接下来,你又可以先说话,然后闭嘴 5 分钟~

所以,引出来了立即执行版的去抖函数。

取消功能实现

<div>
    输入框: <input type="text" id="exampleInput">
</div>
<script>
window.onload = function() {
    var inputEl = document.getElementById("exampleInput");

    inputEl.oninput = debounce(ajax, 1000, true); // debouce 函数执行了,返回一个函数,该函数为事件的回调函数

    // 事件真正的处理函数(handler),参数是回调函数传递过来的。
    function ajax(event) {

        console.log("HTTP 异步请求:", event.target.value);
    }

    function debounce(func, delay, immediate) {
        var interval = delay || 1000;
        var timer = null; // 定时器的初始值为 null, 所以第一次触发事件会立即执行,整个过程中 timer 充当了 flag 的作用,判断能否立即执行(第一次或者上一次立即执行后超过了间隔时间)
        return function(args) {

            var context = this; // 事件的回调函数中,this 指向事件的绑定的 DOM 元素对象(HTMLElement)
            console.log(timer);
            clearTimeout(timer); // 每次有新事件触发,都会清除之前的定时器,如果可以立即执行则执行,如果不可以立即执行则重新创建定时器。
            if (immediate) {
                // 如果上一次的 timer 不为 null, 说明自上一次事件触发并且立即执行处理函数后,间隔时间还未结束。所以 timer 本应为数字 id,不为 null!
                callNow = !timer;
                timer = setTimeout(function() {
                    timer = null; // 每次事件触发,并在定时器时间超过后, 把定时器变量设置 null, 从而可以判断出下一次是否能够立即执行。
                }, interval);

                if (callNow) {
                    func.call(context, args);
                }
            } else {
                timer = setTimeout(function() {
                    func.call(context, args); // 定时器时间到后,执行事件真正的处理函数 handler
                }, interval)
            }

        }
    }
}
</script>

上面代码的注释,可以解释整个流程,下面大致说一下:

  1. 非立即执行版本和前一节内容一样,跳过。
  2. timer 初始值为 null, 第一次触发为立即执行,!timer 为 true, 所以能够立即调用事件处理函数。
  3. 每次事件触发, 都会把 timer 重新赋值,在间隔时间到之前 timer 为数字 id, !timer 为 false, 所以不能立即执行。如果间隔时间到了,会把当次事件触发的定时器 id 置为 null, 下一次事件触发就能立即执行了。
  4. 朋友们可以通过观察 timer 值的变化,思考整个过程,timer 在去抖的过程中充当 flag 的作用,可以用来判断能否立即执行。

看看效果:

取消函数

假如去抖函数的间隔时间为 5 秒钟,我在这 5 秒钟内又想立即执行可以怎么做?于是我们给回调函数加个取消函数属性。

函数也是一个对象,可以像其他一般对象那样添加方法:

<div>
    输入框: <input type="text" id="exampleInput"><button id="cancelBtn">取消</button>
</div>
<script>
window.onload = function() {
    var inputEl = document.getElementById("exampleInput");

    var debouncedFunc = debounce(ajax, 5000, true); // 将事件处理函数经过去抖函数处理。
    inputEl.oninput = debouncedFunc; // 绑定去抖后的事件回调函数

    var cancelBtnEL = document.getElementById("cancelBtn");
    cancelBtnEL.onclick = debouncedFunc.cancel; // 绑定回调函数的属性 cancel 方法,点击页面,重置去抖效果

    function ajax(event) {

        console.log("HTTP 异步请求:", event.target.value);
    }

    function debounce(func, delay, immediate) {
        var interval = delay || 5000;
        var timer = null;
        var revokeFunc = function(args) {
            var context = this;
            clearTimeout(timer);
            if (immediate) {

                callNow = !timer;
                timer = setTimeout(function() {
                    timer = null;
                }, interval);

                if (callNow) {
                    func.call(context, args);
                }
            } else {
                timer = setTimeout(function() {
                    func.call(context, args);
                }, interval)
            }

        }

        revokeFunc.cancel = function() {
            clearTimeout(timer); // 清空上一次事件触发的定时器
            timer = null; // 重置 timer 为 null, 从而下一次事件触发就能立即执行。
        }

        return revokeFunc;
    }
}
</script>

看看效果:

总结

去抖函数的意义在于合并多次事件触发为一次事件处理,从而降低事件处理函数可能引发的大量重绘重排,http 请求,内存占用和页面卡顿。

另外,本文有关 this, call, apply,闭包的知识,可以翻看我之前分享的文章。

原文链接:https://segmentfault.com/a/1190000017308358

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-47637.html

文章标题:JS专题之去抖函数

相关文章
从2014年的发展来展望JS的未来将会如何
&lt;font face=&quot;寰�杞�闆呴粦, Arial, sans-serif &quot;&gt;2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
three.js实现围绕某物体旋转
话不多说,请看代码: 可以拖动右上角观察变化 &lt;!DOCTYPE html&gt; &lt;html lang=&quot;en&quot; style=&quot;width: 100%; height:100%;&quot;&gt...
2017-02-17
NodeJS参考手册pdf版
下载地址:Nodejs参考手册PDF版下载 ...
2015-11-12
Node.js学习(1)----HTTP服务器与客户端
Node.js 标准库提供了 http 模块,其中封装了一个高效的 HTTP 服务器和一个简易的HTTP 客户端。http.Server 是一个基于事件的 HTTP 服务器,它的核心由 Node.js 下层 C++部分实现,而接口由 Jav...
2015-11-12
Riot.js:不足1KB的MVP客户端框架
Riot.js是一款MVP(模型-视图-呈现)开源客户端框架,其最大的特点就是体积非常小,不足1KB,虽然体积小,但它可以帮助用户构建大规模的Web应用程序。 Riot.js是由Moot公司开发,目前最新版本为v0.9.2,遵循MIT开源许...
2016-03-11
使用jspdf生成pdf报表
由于前台html已经动态生成报表,而且,前台有一个功能,一个date range组件,当你拖动的时候,报表会在不提交到后台的情况下动态变化。 因此需要用到js生成生报表: 用到的组件: jquery.js jspdf.js canvg.js...
2017-03-25
jQuery中DOM树操作之使用反向插入方法实例分析
本文实例讲述了jQuery中DOM树操作之使用反向插入方法。分享给大家供大家参考。具体分析如下: 使用反向插入方法 这里我们先把创建的内容插人到元素前面,然后再把同一个元素插人到文档 中的另一个位置。通常,当在jQuery中操作元素时,利用...
2015-11-13
AngularJS vs. jQuery,看看谁更胜一筹
很多Web开发新手都会有这样的疑问“我应该使用什么开发框架呢,如何快速学会Web开发呢?”这个问题其实没有一个统一的正确答案,其中讨论最多的就是AngularJS和jQuery的差别。这两者的之间的比较很微妙,有时就像拿苹果和橘子在作比较,...
2015-11-12
如何为高负载网络优化Nginx 和 Node.js?
译者:AlfredCheung 在搭建高吞吐量web应用这个议题上,NginX和Node.js可谓是天生一对。他们都是基于事件驱动模型而设计,可以轻易突破Apache等传统web服务器的C10K瓶颈。预设的配置已经可以获得很高的并发,不过,...
2015-11-12
回到顶部