有关js函数防抖、节流的一些简单认知


用户在浏览网页的时候,总会不断与浏览器页面产生交互,产生各种与UI相关的事件,比如点击、按键、滚动,甚至包括动画以及DOM变动,等等。在产生这些交互的时候,必然会占用浏览器的一些资源,倘若某些事件触发过多,必然会对页面性能产生一定影响,因此我们就需要限制这些事件出发的次数。这不,防抖与节流就来了~

防抖

是什么?

首先,我们来思考个问题,不知你在浏览网页的时候是否有这样的体验:在页面中点击某个按钮,手一抖,就不慎点了2次,与按钮对应的事件也就触发了2次😏

这种情况常发生在用户提交表单时手抖多点了几下,或是用户网络不太好点了一次又点了多次,或是别有用心的用户疯狂点击提交按钮(比如直接执行元素的.click()函数),这些情况造成了表单被疯狂提交,服务器就收到了一堆垃圾请求(重复数据),此时网络就被拥堵了。

都怪你的手,为什么要抖🌚

因此,为了防止你的手抖,而造成这种情况发生,我们就要对这个按钮的事件处理函数进行一些处理,也就是这里的防抖。

原理?

防抖,实际上就是在触发一个事件以后,在之后的一段时间里,事件处理函数只能被执行一次。
嗯,首先我们应该设置一个超时的时间范围 —— 在触发一个事件以后,开始计时,在超时时间未达到之前,若再次触发同一事件,则重新计算超时时间,再次触发则再次重新计算,一直延后(只要超时时间不到,就不会触发事件处理函数);超时时间到达时,如果同一事件没有再次触发,则将执行事件处理函数。

试试?

<body>
    <h1 id="h1">点我有惊喜</h1>
    <script>
        function clickHandler(e) {
            console.log(e)
        }
        h1.addEventListener('click', clickHandler)
    </script>
</body>

这是一个简单的代码片段,用户在点击h1元素的时候,会触发点击事件的处理函数。此处没有做任何处理,用户无论在多短时间内点击了多少次,都会执行处理函数。因此,我们需要尝试优化。

第一步,加定时器。

let timer = null
function clickHandler(e) {
    timer = setTimeout(() => {
        console.log(e)
    }, 800)
}
h1.addEventListener('click', clickHandler)

此处没什么卵用,只是使得点击后的函数能够延迟800ms执行,但最终还是点多少次执行多少次

第二步,在某个地方重新计时。

let timer = null
function clickHandler(e) {
    // 清除定时器
    if (timer) {
        clearTimeout(timer)
        timer = null
    }
    timer = setTimeout(() => {
        console.log(e)
    }, 800)
}
h1.addEventListener('click', clickHandler)

重点来了,我们在设置新定时器之前,先看看之前是否有定时器,如果有了,我们就把之前的定时器清除。经过测试,在800ms内不断点击h1后,事件处理函数将不再会点几次执行多少次,我们的预期目标已经达成。

好了,理论来来说,节流大概就是这么个东西吧,在我看来,它最大的作用是解决了人的手抖问题~

与业务分离

但我们有木有可能对上面这个函数来进行改造,写一个通用函数,使得我们能够更加得心应手地对任意函数也进行节流呢?

既然是对一个函数进行处理,因此在编写这个通用函数的时候我们需要接收一个要处理的函数fn作为参数,并返回一个处理过后的函数;同时,我们可以在参数中传入一个数值timeout,用来设置防抖函数的超时时间。

function debounce(fn, wait){ 
    // ...
    return function(){ ... }
}

将我们处理节流的部分逻辑拿过来;

function debounce(fn, timeout) {
    let timer = null
    return function(){
        // 事件处理函数函数执行前,检测定时器是否存在
        if (timer) {
            // 如果定时器存在,则意味着函数在一定时间内被多次执行了,则清除定时器
            clearTimeout(timer)
            timer = null
        }
        // 然后设置新的定时器
        timer = setTimeout(() => {
            fn()
        }, timeout)
    }
}

这样,这个函数就可以返回一个被节流处理过的新函数。在timeout毫秒这段时间内,由于超时时间未达到,如果新函数被不断执行,真正的事件处理函数将不会执行,但新函数的执行会不断覆盖定时器,直到最后一个定时器超时后才执行真正的事件处理函数。

但目前封装的这个函数还是略有一些问题的:

  1. 传参问题。假设这里的fn是点击事件的处理函数,一般来说用到点击事件的时候,一般都会判断点中的是什么元素,因此fn会接收一个MouseEvent对象,其中包含本次点击的相关信息(笔者喜欢把这个参数写为e)。但被防抖处理后,这个函数就没法接受参数了。可以看到,上方的fn是不带参数,直接被执行的。
  2. this指向问题。(其实笔者还没遇到这个问题,但网上找到的代码有这一处理,待笔者研究研究🤪)

因此,我们需要再进一步处理:

function debounce(fn, timeout) {
    let timer = null
    return function(){
        // 保存参数与this指向
        let args = arguments;
        let ctx = this;
        if (timer) {
            clearTimeout(timer)
            timer = null
        }
        timer = setTimeout(() => {
            // 用apply执行代码
            fn.apply(ctx,args)
        }, timeout)
    }
}

到此步骤,代码其实已经和网上搜索到的大部分防抖代码类似了。

好了,防抖就先告一段落吧,接下来说说它的好兄dei,传说中的“节流”。

节流

是什么?

这次把目光从鼠标点击事件转向元素的鼠标滚轮事件。滚轮事件有时我们用到的会非常多,例如地图的放大缩小。每当鼠标发生一次滚动,就会触发一次滚轮事件。然而当用户不断滚动的时候,可能会对页面性能产生一定影响。因此我们需要限制滚轮事件的触发频率,而且也应当让滚轮事件能够一直在执行(而不是使用防抖函数 —— 仅执行最后操作的那一次)。这时,节流就很有用了。

原理?

降低事件处理函数触发频率。

来试试?

<body style="margin:0;width:100vw;height:100vh">
    <h1 style="margin:0" id="h1">滚一滚,有惊喜</h1>
    <script>
        function wheelHandler(e) {
            console.log(e)
        }
        document.body.addEventListener('wheel', wheelHandler)
    </script>
</body>

这里代码片段的用于为文档添加鼠标滚轮事件。当鼠标滚轮滚动时,触发wheel事件,并执行回调函数。
尝试运行一下:
image
笔者随便动了几下鼠标滚轮,就看到这一事件触发了多次。那如何限制一下呢?

先不管别的,试试上面写的防抖函数。

        function wheelHandler(e) {
            console.log(e)
        }
        document.body.addEventListener('wheel', debounce(wheelHandler,300))

果然如上文所说,在疯狂触发滚轮事件后,事件处理函数只执行了最后触发的那一次,并没有一直执行。(这说明我们上面写的防抖函数还算成功~)

那如何让事件处理函数在降低执行频率呢?

同样,我们在此对事件处理函数进行修改。

首先,简单地添加一个定时器。

        let timer = null
        function wheelHandler(e) {
            timer = setTimeout(() => {
                console.log(e)
            }, 1000)
        }
        document.body.addEventListener('wheel', wheelHandler)

然而与上方防抖章节一样,此处仅仅是延迟了事件处理函数的执行时机,最终还是事件触发了多少次,事件处理函数执行多少次,并没有使事件处理函数执行频率降低。

接下来,需要继续对这个函数进行优化。

        let timer = null
        function wheelHandler(e) {
            if (!timer) {
                timer = setTimeout(() => {
                    console.log(e)
                    clearTimeout(timer)
                    timer = null
                }, 1000)
            }
        }
        document.body.addEventListener('wheel', wheelHandler)

此处的逻辑是,仅有定时器不存在(为null)的时候,事件处理函数才能够在一段时间超时后执行,且在一次执行过后,定时器将会被清除并置为null,使得的下一次触发执行的事件处理函数能够在时间超时后执行;若触发事件时发现定时器存在,则表示上次的事件处理函数还未执行,此次触发无效。

与业务分离

同样如函数的防抖,我们怎样实现一个类似如下的通用函数,来让其能够对任意函数进行节流?
(此处fn表示要节流的函数-比如某些业务,wait表示超时)

function throttle(fn, wait){ 
    // ...
    return function(){ ... }
}

嗯,首先还是把我们上方的逻辑拿过来,整理一下,得到:

        function throttle(fn, wait) {
            let timer = null
            return function () {
                if (!timer) {
                    timer = setTimeout(() => {
                        fn()
                        clearTimeout(timer)
                        timer = null
                    }, wait)
                }
            }
        }

同样,此处函数也有无法接受参数以及this指向的问题。依葫芦(节流函数)画瓢即可。

        function throttle(fn, wait) {
            let timer = null
            return function () {
                let ctx = this;
                let args = arguments;
                if (!timer) {
                    timer = setTimeout(() => {
                        fn.apply(ctx,args)
                        clearTimeout(timer)
                        timer = null
                    }, wait)
                }
            }
        }

总结一下

想不到今天距离上次写这篇文章都快过了1个月了,都快忘了;今天想起来就总结一下吧。

防抖

也就是将一个函数(函数a)的执行放到定时器中,当包裹的函数被不断执行时,判断定时器是否存在;如果存在,就清除旧定时器,建立新计时器,直到最后一个定时器超时的时候执行该函数(函数a)。因此防抖过后的函数(函数a)无论执行多少次,最终都仅执行最后一次。

节流

同样也是将函数(函数a)的执行放入到一个定时器中,当包裹的函数被不断执行时,判断定时器是否存在;如果不存在,就新建一个定时器,该定时器超时后直接执行函数(函数a);如果存在,则表示上次的函数还未执行,因此不进行任何操作。因此节流过后的函数(函数a)及时一段时间内被连续执行多次,也仅执行每个节流时间段结束后的那一次。


评论