前端节流防抖技术的学习

说明

今天被问到了节流防抖技术,由于以前在项目中没有用到过,所以一脸蒙蔽;不百度都不知道是什么;

思路

其实问题的本质,就是多次触发后如何一次性调用的问题。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    输入框 : <input id="ipt" type="text">
</body>
<script>
    var ipt = document.getElementById("ipt");

    function test() {
        console.log("dd")
    }
    
    ipt.addEventListener("keyup", function () {
        var time = null;
        if (time !== null) {
            clearTimeout(time);
        }
        setTimeout(() => {
            test();
        }, 500);
    })
</script>

</html>

这边呢,当时临时百度了下,就把答案写了上去,
在这里插入图片描述
晚上测试了下,发现其实还是调用两次,只不过是延时调用了;当时心里也是蛮抵触这种写法的,因为本身就比较讨厌settimeout这个回调函数。
现在有时间想了,大概的思路有以下几种:
首先呢,需求就是期望不要每次键盘抬起的时候都去调用这个函数,而是期望在某段时间,去调用,就是把结果一次性传递给后台。所以呢,像我这样些,还是有点问题的。这种场景其实用到的地方还是蛮多的,也还是值得研究的。可以优化掉多次请求的问题。

解决办法

在解决这个问题之前,我们需要弄清楚setTimeout,setInterval,clearInterval等的作用;

let time1 = setTimeout(() => { 
    }, 0);  //time1 == 1
let time2 = setInterval(() => {
    }, 20); //time2 == 2
clearTimeout(time2);
//我把这个代码放这里的作用呢,其实就是想告诉大家,返回值是公用的从1开始到无穷的数值,就是有几个定时器,就会返回几,而清除可以随便混用,都有效的。

1 通过规定的时间,去触发函数

 var ipt = document.getElementById("ipt");
    function test() {
        console.log("dd")
    }
    var time=null;
    ipt.addEventListener("keyup", function (e) {
        if(time!==null){
            clearTimeout(time);
        }
       time = setTimeout(() => {
        test();
        }, 500); 
    })
    这里就比较简单了,当我们在键盘上输入1的时候,这时time是null,所以会执行定时函数,500ms后调用test();然后我们在按下2的时候,time==1,所以会清除掉前面的定时函数,time就被重新定义了,为2;下面的截图和我预想的一样,而且为了不过多的占用内存,我们最好初始化time;

在这里插入图片描述
最终的写法

  var ipt = document.getElementById("ipt");
    function test() {
        console.log("dd")
    }
    var time=null;
    ipt.addEventListener("keyup", function (e) {
        if(time !== null){
            clearTimeout(time);
        }
        time = setTimeout(() => {
        test();
        time = null;
        }, 2000); 
    })

但是这里呢,需要写一个函数,封装成通用的,那这里就考虑写成如下:
思考的过程:
首先,我们已经面向过程的实现了;那么,我们要做的就是把已经实现的封装成一个函数,做成通用的

考虑到通用性,我们肯定是用函数声明的方式,不能用匿名函数,当然,函数表达式也可以
首先,我们要有一个变量time,相当于两层函数,第一层函数,声明一个变量time,第二个函数,做事件的处理
######首先的想法#########
function debounced() {
        var time = null;
        function child() {
            if (time !== null) {
                clearTimeout(time);
            }
            time = setTimeout(() => {
                test();
                time = null;
            }, 2000);
        }
    }
######修改一些形参等#########
function debounced(fn,wait) {
        var time = null;
        function child() {
            if (time !== null) {
                clearTimeout(time);
            }
            time = setTimeout(() => {
                fn();
                time = null;
            }, wait);
        }
    }
######函数内部函数调用#########
function debounced(fn,wait) {
        var time = null;
        function child() {
            if (time !== null) {
                clearTimeout(time);
            }
            time = setTimeout(() => {
                fn();
                time = null;
            }, wait);
        }
        child();
    }

测试下:

 var ipt = document.getElementById("ipt");
    function test() {
        console.log("dd")
    }

    ipt.addEventListener("keyup", function () {
        debounced(test,2000)
    })

    function debounced(fn,wait) {
        var time = null;
        function child() {
            if (time !== null) {
                clearTimeout(time);
            }
            time = setTimeout(() => {
                fn();
                time = null;
            }, wait);
        }
        child();
    }

在这里插入图片描述发现还是有问题,然后查看下代码
然后按照代码的逻辑,我们走一遍;
逻辑:首先,在键盘连续输入了12345,会触发键盘抬起的监听,当1抬起时,触发debounced函数,在函数内部,声明time=null,声明child函数,调用,此时child为null,调用时间延迟函数,调用fn,time=null;当第二次调用debounced时,time仍然为null,所以又触发了时间延迟函数,,原来问题出在这里,就是我的time一直为null,这里如果我总是先声明一个time,好像始终逃不脱Null的嫌疑,所以这里我不声明,然后去查了很多资料,也算是整理出了一个很完整的例子,但是有几点我认为还是必须要弄明白的:
1 addEventListener使用匿名函数和有名称的函数的区别
2 settimeout使用匿名函数和有名称函数的区别
3 通过1,2 ,我们需要弄清楚函数声明的方式
4 闭包的作用域链的问题
5 函数this指向的问题
突然我发现防抖这个试题,考察的点真的很多,需要相当的基本功啊。
那么我们一个问题一个问题的来解决:

1 函数声明的方式
    //函数声明
    function add(){
        console.log("函数声明")
    }

    //函数表达式
    var test = function(){
        console.log("函数表达式")
    }
 
    //实例化,这种用的很少
    var te = new Function();
2 onclick和addeventlistener的区别以及addeventlistener里面传函数的区别
    //函数声明
    function add(){
        console.log("函数声明")
    }

    //函数表达式
    var test = function(){
        console.log("函数表达式")
    }
    var ipt = document.getElementById("ipt");
    ipt.onclick=function(){
        console.log("aa")
    }
    ipt.onclick=test;
    ipt.onclick=add;

在这里插入图片描述通过上面的对比,我们发现onclick只能绑定一个事件
这里我把onclick直接执行

 //函数声明
    function add(){
     
        console.log("函数")
    }

    //函数表达式
    var test = function(){
        console.log("函数表达式")
    }
    var ipt = document.getElementById("ipt");
    
    ipt.onclick=add();

在这里插入图片描述我发现点多少次都没有反应了,而且是一开始就执行了,这里应该也不难理解,
然后我们来看看addeventlistener

//函数声明
    function add(){
     
        console.log("函数")
    }

    //函数表达式
    var test = function(){
        console.log("函数表达式")
    }
    var ipt = document.getElementById("ipt");
    ipt.addEventListener("click",function(){
        console.log("1")
    })
    ipt.addEventListener("click",test)

在这里插入图片描述显然这里的addeventlistener可以绑定多个

settimeout传入函数的区别
//函数声明
    function add(){
     
        console.log("函数")
    }

    //函数表达式
    var test = function(){
        console.log("函数表达式")
    }
    var ipt = document.getElementById("ipt");
    setTimeout(() => {
        console.log("hehe")
    }, 1000);
    setTimeout(add, 2000);
    setTimeout(test(), 5000);

在这里插入图片描述
基本上也是显而易见的

函数的作用域链及闭包的关系

1 首先作用域
全局作用域就是整个script标签内的
局部作用域就是函数内部的,也叫函数作用域
2 作用域链
函数内部套函数,就产生了作用域链,就近原则
3 闭包
闭包就是通过函数内部嵌套函数的形式,产生可持久化的局部变量,避免全局变量污染,又可以持久化操作变量
4 匿名函数的作用域
匿名函数是可以访问到全局变量的

好了,这几个问题搞明白了,花了我一天的时间,现在我们来正式的完成函数防抖的功能:

 var ipt = document.getElementById("ipt");
    var ipt1 = document.getElementById("ipt");
    function ajax() { console.log("发送请求");console.log(this);console.log(arguments) }

    // ipt.addEventListener("keyup", function () {
    //     y()
    // })
     

    //封装一个通用函数,在一定的时间内,多次请求只触发一次
    //精简版(1)
    function debounced(fn, wait) {
        let timer = null;
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn();
        }, wait);

    }
    /**迭代1,由于(1)中,每次调用这个函数,都会重新生成timer,所以清除
     * 函数并没有把之前的timer清除掉,要想把之前的timer清除掉,必须弄一个
     * 全局变量,但是每次调用该函数,都弄一个全局变量,会造成变量污染和内存溢出
     * 不能这样做,但是我们先看看这样做的危害
     * 危害:
     * 1 产生全局变量污染 timer
     * 2 不具有通用性,当有多个地方调用该函数时,比如我想点击时,延迟300ms发送
     * ajax,在键盘抬起时1s发送请求,那么就会产生循环覆盖
     */
    function debounced1(fn, wait) {
        if (!window.timer) {
            window.timer = null;
        }
        clearTimeout(window.timer);
        window.timer = setTimeout(() => {
            fn();
        }, wait);
    }
    /**
     *  迭代2,那么我们就要考虑抛弃全局变量的做法,由于封装的函数,既要有全局变量
     * 的使用性质,又是希望给每个人单独使用的,那么不可避免的我们想到了闭包,闭包可以
     * 产生局部的可持久化操作的变量,但是在这里有一个难点:
     *  ipt.addEventListener("keyup", function () {m();}) 大家看看我是这样调用的,而且我下面采用的
     * 是函数表达式的情形,还立即执行了?
     * 如果说,我单单这样写:
     * function debounced2(fn, wait) {
        let timer = null;
        return function () {
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn();
            }, wait);
        }

       }
       然后这样去调用:
       ipt.addEventListener("keyup", function () {       debounced2(ajax,500)()      })
       这样做的逻辑流程就是:我每点击一次键盘,就会调用一次debounced2,然后返回function再次调用,相当于
       每次我的timer还是新生成的,知道不;所以,要保证我的闭包函数,只能被调用一次才可以,那么只能像下面这样去写
       也就是声明后,立即执行了去
     */
    var m = function debounced2(fn, wait) {
        let timer = null;
        return function () {
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn();
            }, wait);
        }

    }(ajax,500)
    /**
     * 迭代3 根据上面的情形呢,可以分成两种情况,一种就是,
     * ipt.addEventListener("keyup", function () {  })不变的情形,意思就是,我就想用这种回调的形式去操作;
     * 那么就只能跟迭代2一样,先执行了
     */
    var y= function debounced3(fn, wait) {
        let timer = null;
         return function(){
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn();
            }, wait);
        } 
    }(ajax,500)
     console.log(y)
     /**
     * 迭代4 第二种情形呢,就是我们可以改变下调用的方式
     * ipt.addEventListener("keyup",debounced4(ajax,500)) 
     * 这里可能就要稍微解释下了,为什么这样去做就可以,首先我们看下函数说明
     * addEventListener(event,function,useCapture);我们只看第二个参数哈,funciton,这是什么意思呢,如果
     * 我们传入的是一个匿名函数,相当于就是一个回调函数,只有当事件event触发过后,才会去执行,那么每次执行,相当于
     * 我们每次都要触发debounced4,所以我们的闭包函数每次都被重新触发了,那么在内存中,每次都开辟空间,产生了新的timer,
     * 这样的话,我们的定时器也都是新的;但是,我们如果直接把闭包函数放到那里去,相当于开始就执行了一次闭包函数,在内存
     * 空间只开辟了一个timer,闭包函数运行完,返回的是什么?是function(){clearTimeout(timer....},所以在不同的事件中
     * 我们这样去使用,就相当于每个事件都开辟了自己的内存空间timer,然后返回闭包放到那里,当事件触发后,去执行。
     */
     function debounced4(fn, wait) {
        let timer = null;
         return function(){
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn();
            }, wait);
        } 
    } 
    // ipt1.addEventListener("keyup",debounced4(ajax,500))
    /**
     * 迭代5,我们知道哈,我们的事件接触,在回调函数里啊,是可以拿到一个参数e的,也就是键盘触发事件的所有信息
     * ipt1.addEventListener("keyup",function(e){
            console.log(e)
     })
     那么,我们如果采用了上面的写法,怎么取拿到这个e呢?其实添加监听事件的函数,可以简化成
     function test(callback){
        var e="键盘的所有信息";
        callback(e)
     }
     */
      
     //这里 var callback = function(){}   ====> callback(e) ==== function(){}(e),所以就有了下面的方法
     function debounced5(fn, wait) {
        let timer = null;
         return function(e){            
            clearTimeout(timer);
            timer = setTimeout(() => {
                fn();
                console.log(e)
            }, wait);
        } 
    } 
    // ipt1.addEventListener("keyup",debounced4(ajax,500))
    //这里大家看的可能还是有点不懂。。。那么请私信我。。因为中间删了一些推论过程

    /**
     * 迭代6,至此,防抖的功能基本实现了,而且也拿到了我们的键盘事件。 但是,我们知道,函数的this都是指向window的,
     * 那么当我们返回后,这里事件结束时的this指向肯定变了;本来我们的this,是指向 <input id="ipt" type="text">
     * 但是我们肯定希望ajax在被事件调用完后,只指向调用的dom的
     * 
     */
     function debounced6(fn, wait) {
        let timer = null;
         return function(e){                       
            clearTimeout(timer);
            var that = this;
            timer = setTimeout(function(){         
                fn.call(that);
            }, wait);
        } 
    } 
    //当然,这里也可以用到es6的语法
    function debounced6(fn, wait) {
        let timer = null;
         return function(e){                       
            clearTimeout(timer);
            timer = setTimeout(()=>{       
                fn.call(this);
            }, wait);
        } 
    } 
    // ipt1.addEventListener("keyup",debounced6(ajax, 500))

    /**
     * 迭代7 然后呢,针对迭代5,我们还有更简单的方法,由于在javascript中,执行函数没有设置参数,也是可以被传入参数的,这句话
     * 是什么意思呢,比如
     * function test(){
     * }
     * 我没有设置参数,但是我还是有办法把参数穿进去,比如
     * !function test(){
     * console.log(arguments)
     * }("123")
     * 这里的参数我们就传进来了
     */
     function debounced6(fn, wait) {
        let timer = null;
         return function(){                       
            clearTimeout(timer);
            timer = setTimeout(()=>{   
                console.log(arguments)    
                fn.call(this,arguments);
            }, wait);
        } 
    }
    ipt1.addEventListener("keyup",debounced6(ajax, 500))
总结

里面涉及到了很多知识点,光看个结果是没有意义的;还是要多思考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李卓书

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值