函数节流和函数防抖

Clloz · · 258次浏览 ·

前言

函数节流 throttle 和函数防抖 debounce 都是非常常用的小技巧,特别是在处理频繁触发的事件的时候。比如我们在懒加载的时候不会等到页面滚动到最底部的时候在触发事件,一般是给一个范围,当进入这个范围的时候我们再向后台请求数据并渲染到页面上。这个时候如果我们不进行一定的处理就会发现事件在一些情况下会触发很多次,比如用户在进入到事件触发的范围内进行非常缓慢的滚动,请求就会多次发送,我们的代码也会多次执行。这样不仅页面的逻辑出了问题,并且性能也会受到影响。所以我们需要利用函数节流或者函数防抖来应对这种情况。

函数节流 throttle

节流我们可以理解为节约流量,而这个流量就是回调函数执行的量,对于会频繁触发的事件的回调函数我们规定单位时间内只执行一次,这样就能够保证页面的逻辑正确也不会执行无意义的回调函数,达到节流的目的。经常被用来比喻的是当一秒钟连续播放 24 帧或以上的画面,人脑会处理成连续的画面,所以以前的电影都是 24 帧的,之所以不用 100 帧是因为 24 帧刚刚好能够满足需求,没有必要去追求 100 帧。不过现在不管是游戏还是电影都会追求更高的帧数,实际情况是帧数的高低对于观感的影响还是不同的,如果你一直玩 30 帧的游戏你并不会觉得又卡顿,但是如果你是一直玩 60 帧或者更高帧数的游戏,突然让你回到 30 帧你会感觉到画面的卡顿。

扯远了,我们来想象一下需要函数节流的场景,比如典型的监听页面滚动触发回调函数,我们写一段测试代码,然后来看看多次触发的情况:

<style>
    div {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    .wrap {
        position: absolute;
        top: 50%;
        left: 50%;
        height: 200px;
        width: 200px;
        padding: 0 10px;
        transform: translate(-50%, -50%);
        border: 1px solid black;
        overflow: auto;
    }

    .content {
        width: 100%;
        height: 500px;
        background: lightblue;
    }
</style>
<div class="wrap">
    <div class="content"></div>
</div>
<script>
    var wrap = document.querySelector('.wrap')
    var content = document.querySelector('.content')
    wrap.addEventListener('scroll', function () {
        console.log('dispatch')
    })
</script>

我们为 wrap 绑定了一个滚动事件的监听,这种需求是经常用到的,比如我们需要根据用户的滚动显示不同的内容,但是很明显的是我们并不需要如此频繁地执行回调函数,我们需要让这个监听的回调函数执行的次数少一点,这时候就需要用到函数节流。

scroll-event

浏览器的监控肯定是固定频率持续发生的,这是我们无法控制的,我们能做的就是在执行流进入到回调函数的时候进行判断,如果不符合条件我们就不执行,这就是函数节流的原理。如何设计这个逻辑呢,我们可以利用 JavaScript 的定时器配合一个表示状态的锁来达到目的,代码如下:

wrap.addEventListener('scroll', throttle(function () {
    console.log('dispatch: ' + new Date().getTime());
}, 300))

function throttle (fn, interval) {
    var executing = false;
    return function () {
        if (executing) return;
        executing = true;
        setTimeout(() => {
            fn.call(this, arguments);
            executing = false;
        }, interval)
    }
}

现在我们的回调函数在 executingtrue 的时候会直接返回,而我们的功能代码也就是 fn 每隔 300ms 才会执行一次,效果如下:

throttle-scroll

需要注意的是 setTimeout 的回调函数中的 this 默认指向 window,因为 setTimeoutwindow 对象上的一个方法,调用 setTimeout 实际上是 window.setTimeout,而我们的 fn 中的 this 需要指向绑定事件的 DOM 元素,所以需要 bind 或者箭头函数,同时 throttle 函数中的 this 指向的是 window,作为 throttle 的一个参数,fn 指向的也是 window 所以这里我们需要用 apply 或者 call 来指定 this,具体的细节这里就不展开了,留到关于 this 的文章中讨论。

函数防抖 debounce

函数防抖这个名字有点让人摸不着头脑,虽然 bounce 确实有晃动的意思。我的理解就是对于某个事件我们在单位时间内只希望它触发一次,但是某些特殊情况(比如回调函数执行比较久,或者事件触发频率太高)那么我们需要函数防抖来处理。生活中的例子,我们坐电梯的时候,电梯有个逻辑,当电梯门关上 3s 后如果没有人按电梯就启动(相当于回调函数的执行),但是如果有人在这三秒内按了电梯按钮,电梯重新开门并重新计时,函数防抖的逻辑和这个类似。

我们还用元素的滚动来说明。函数节流的滚动是当我们触发滚动事件的时候就执行回调函数,但是有时候我们会遇到这样的逻辑,当用户滚动到底部的时候我们执行回调函数。并且为了加载的效果更好,我们往往不会真的等到用户滚动到最底部的时候才执行,而是给定一个范围,比如当用户滚动到距离底部还有 50px 的时候就执行回调函数。但是这样会造成一个问题,当用户滚动的很慢,这个回调函数会多次触发,这样我们只需要执行一次的逻辑可能会多次执行,代码如下( HTMLCSS 还和上面相同):

//debounce normal
wrap.addEventListener('scroll', function () {
    if (parseFloat(window.getComputedStyle(wrap).getPropertyValue('height')) + wrap.scrollTop + 30 > parseFloat(window.getComputedStyle(content).getPropertyValue('height'))) {
        console.log('dispatch')
    }
})

效果如下

throttle

我们可以发现我们的回调函数发生了很多没有必要的执行。解决办法就是我们给我们的逻辑代码加一层 setTimeout 如果在单位时间内事件又出发了,就把这个定时器 clearTimeout 取消掉并给一个新的定时器,那么只有当单位时间内没有触发新的事件,这个定时器才会执行。

wrap.addEventListener('scroll', debounce(function () {
    console.log('dispatch')
}, 300))

function debounce(fn, interval) {
    let timeout = null;
    let isLoad = false;
    return function () {
        if (parseFloat(window.getComputedStyle(wrap).getPropertyValue('height')) + wrap.scrollTop + 50 > parseFloat(window.getComputedStyle(content).getPropertyValue('height'))) {
            if (isLoad) return;
            clearTimeout(timeout)
            console.log('clear')
            timeout = setTimeout(() => {
                fn.apply(this. arguments);
                isLoad = true;
            }, interval)
        }
    }
}

debounce

这里需要注意的是由于我们滚动到底部的逻辑一般只要触发一次,所以要加个状态锁,当回调函数执行过后不管用户如何滚动都不再触发该功能了。函数防抖还有很多应用场景,比如当用户停止输入再检测输入内容等。

总结

函数节流和函数防抖都是非常有用的,函数节流可以简单地理解为我们希望减少事件回调函数的执行频率,而函数防抖则可以理解为我们希望回调函数能够尽量在最有效率的时候执行,单位时间内只执行一次。

想要查看文章中的示例请点击

参考文章

  1. 函数节流与函数防抖
  2. 轻松理解JS函数节流和函数防抖

Clloz

人生をやり直す

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*

EA PLAYER &

历史记录 [ 注意:部分数据仅限于当前浏览器 ]清空

      00:00/00:00