防抖和节流--情景图片理解及手撕源码

94 阅读6分钟

一、关于防抖的理解

1.1 首先看看官方定义:

防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

官方永远都是这种让人感觉读天书的感觉...... 大概看看就好了,一会我们再继续聊这个啦。

1.2 举个小栗子

这里我们对防抖举个小栗子,看过比较好的解释就是坐电梯的,规则是这样的,一群人乘坐电梯,当电梯门打开后就会停留大概1分钟,如果1分钟内有人进来电梯里了,那么电梯会重新等待1分钟,如果1分钟内没有人进来,那么电梯就会启动

那么根据这个例子,我们来看防抖的定义: ”当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时“ 翻译成这个例子,那就是,电梯不时有人进来,如果超过1分钟没有人进电梯,电梯启动,如果在1分钟内有人进来了,那么电梯重新计时。

1.3 用代码来演示一下过程

这里用学习做例子,其中假设系统学习一种技能的时间是一样的,一次只学习一类技术,学习的过程用进度条表示

代码大概的意思就是,点击按钮机会开始学习一门新技术,进度条向前,如果再次点击,如果上一次的学习还没完成,就不再继续,结束当前学习,而只进行当次的学习。

<!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>
    <style>
        #btn {
            padding: 3px 10px;
            background-color: lightseagreen;
            color: white;
            border: 0;
            border-radius: 5px;
            margin-bottom: 20px;
            cursor: pointer;
        }
    
        .border {
            width: 400px;
            height: 40px;
            border: 1px solid black;
            border-radius: 20px;
            overflow: hidden;
        }
    
        #son {
            width: 0;
            height: 100%;
            text-align: center;
            line-height: 40px;
            background-color: goldenrod;
        }
    </style>
</head>
<body>
    <button id="btn">开始学习新内容</button><br/>
    <span>学习进度:</span>
    <div class="border">
        <div id="son">
            <span id="rate">0</span><span>%</span>
        </div>
    </div>
    <div id="done">目前系统学习次数(有效次数):0</div>
    <div id="undone">三分钟热度学习次数(点击次数-1):0</div>
</body>
<script>
    let son = document.getElementById('son');
    let btn = document.getElementById('btn');
    let span = document.querySelector('#rate');
    let num = 0;
    let timer = null;
    let doneNum = 0;
    let undoneNum = 0;
    btn.onclick =  function () {
        //如果上一次定时器还未停止,则清除定时器,进度条归0
        if (timer !== null) {
            clearInterval(timer);
            num = 0;
            son.style.width = '0px';
            span.innerHTML = 0;
            die();
        }
        timer = setInterval(function () {
            num += 10
            //给宽进行递增
            son.style.width = num + 'px'
            //给百分比进行递增  使用Math.ceil() 是为了防止百分比有小数
            span.innerHTML = Math.ceil(num / 400 * 100)
            if (num == 400) {
                clearInterval(timer);
                overOne();
            }
        }, 10)
    }
    function die() {
        undoneNum++;
        document.querySelector('#undone').innerHTML = '目前三分钟热度学习次数(点击次数-1): ' + undoneNum;
    }
    function overOne() {
        doneNum++;
        document.querySelector('#done').innerHTML = '目前系统学习次数(有效次数):' + doneNum;
    }


</script>
</html>

演示结果:

QQ录屏20230227120412.gif 可以看到,当你多次快速点击按钮的时候,已系统完成的次数并不会增加,只有放开,或者两次点击的次数相隔比较久的时候才会增加,而从防抖的角度来看,快速点击按钮对应的就是多次触发事件,已完成系统的次数增加,那就是事件触发。因此,从这里我们可以想到防抖函数的实现思路就是:当事件触发后,先开始计时,如果在你确定的时间内事件没有再次被触发,那么事件执行,如果在这段时间内事件被再次触发了,那么重新计时,直到事件被触发(应该没有用户无聊到一直触发事件吧?)。

1.4 手撕代码

根据上面的思路,我们就是事件触发的时候触发一个定时器,定时器时间一到就会执行函数,如果在时间未到时又触发函数,则清除上一个定时器,开启新的定时器:

//防抖函数,其中fn是要进行防抖的函数,delay是延迟时间
    function debounce(fn,delay){
        //标记上一次定时器的返回值
        let timeout = null;
        return function(){
            //清除上一次的定时器
            clearTimeout(timeout);
            //同步当前定时器返回值
            timeout = setTimeout(()=>{
                //修正this指向
                fn.call(this);
            },delay)
        }
    }

简单测试一下吧:

//html 
<button id="debounceTest">测试</button>
//js
document.querySelector("#debounceTest").onclick = throttle(function(){
    console.log("函数被触发了"+ Date.now());
},500)

演示结果:

QQ录屏20230227153511.gif

只有当不再点击按钮一段时间后才会触发,对于开发而言,如果进行ajax请求时便可以使用节流,比如在写博客时工具的自动保存便用到节流技术,当正在修改,不断输入的时候并不会触发,只有不输入一段时间才会触发自动保存,使用节流技术的原因很明显就是为了降低函数的调用次数,从而减少不必要的资源损耗。

二、来谈谈节流

2.1 老规矩,先看看官方解释:

节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

2.2举个小栗子

在节流的定义上就相对人性化很多啦,不过我们还是结合例子一起来理解,这会更加的更好理解,这里举的例子是打车(假设只有一名司机,一次只载一名乘客),打车的时候,如果司机的车上没有乘客,那么中途他就可以接到订单,但是如果他正在为一名乘客服务,那么接到的其他订单就会被系统自动回绝,直到到达目的地,才能收到下一个订单。那么在这里当持续触发事件时,保证一定时间段内只调用一次事件处理函数。 便可以理解为当系统不断接到客户订单时,在订单执行的时间内,司机只为该乘客服务,而其他乘客如果在这段时间内想要该司机为其服务则不生效,只能等到司机为上一位乘客服务完成(可能用不接受提前预约的私人定制的例子会比较恰当)。 简单而言就是函数触发后的一段时间内,再次触发函数均是无效操作,直至函数执行。

2.3 手撕代码

通过对比上一次函数触发时间和现在时间的间隔是否超过设置的延迟时间进行节流。

//节流函数
    function throttle(fn, delay) {
        //记录上一次函数触发的时间
        let lastTime = 0;
        return function () {
            // 记录当前函数触发的时间
            let nowTime = Date.now();
            if (nowTime - lastTime > delay) {
                // 修改this指向
                fn.call(this);
                // 同步时间
                lastTime = nowTime;
            }
        }
    }

测试小代码:

//需要设置高度超过窗体
document.onscroll = throttle(function(){
        console.log("scroll事件触发了"+Date.now())
    },1000)

结果:

QQ录屏20230227162059.gif 可以看到当滑动快速的滑到底部时由于时间并没有超过1秒,所以只触发一次,当慢慢滑动时,每一秒触发一次。节流和防抖一样都是减少函数的调用次数,但是应用场景不同,比如对于滚动条时间,不适合用防抖,因为用户非常快的滑动滚动条,但是由于仅触发最后一次滚动事件,那么滚动事件的触发就不合理了,这个时候就需要节流了,虽然用户很快滑动滚动条,多次触发滚动事件,但是这么短的时间内由于节流函数,就可减少很多次不必要的的滚动事件触发。