节流(throttle)指的是:事件频繁触发,但是一个时间段内只响应一次。
实际的运用场景是:我觉得和防抖差不多,都是高频事件的场景。
节流的过程:设置好小周期,这个小周期内如果有事件触发,不管触发几次,周期末尾响应一次就好。
和防抖的比较:防抖是这一波高频事件的最后才响应一次;节流则是,如果高频事件持续较久,持续过程中也会响应一次或几次。

这篇文章的时间线图片可以帮助理解:js防抖和节流

一、setInterval实现
一开始我比较死心眼。既然要根据固定的周期来,那就得全局设置setIntervel了:

<script>
    document.addEventListener('DOMContentLoaded', function () {
        var timer = null;
        var flag = false;

        document.addEventListener('scroll', function () {
            flag = true;
        });

        timer = setInterval(function () {
            if (flag == true) {
                flag = false;
                console.log('you have scrolled during last period');
            } else {
                //console.log('no scrolling during last period');
            }
        }, 1000);
    });
</script>

全局设置setInterval之后,每隔1000ms就做一次flag检测:
如果flag为true,说明前面1000ms的周期内有滚动一次或多次,可以执行动作(输出“you have scrolled during last period”)。
如果flag为false,不用执行动作,这里为了演示就放了个else分支并输出“no scrolling during last period”。
(如果你的浏览器不支持监听document的scoll,试试改成window或document.body;如果周期1000ms效果感受不强,试试改成500、100)

这里的flag,一般叫节流阀。写到这里想起之前已经做过轮播图的节流阀了,还是没能马上把思路、代码实现转移应用到这种普遍的节流场景。

二、setTimeout实现并封装
为什么说刚刚死心眼了呢?因为我看了上面那篇文章的图示之后,认为必须要有连续的周期。实际上没有必要,周期之间可以有间隔,不一定要连续。
也就是说,可以使用setTimeout代替setInterval。设想如下过程:
(1) 第一波高频事件的第一次触发,给予响应(设定timeout,启动周期)。
(2) timeout到了,周期也就到了末尾,执行动作。
(3-1) 之后如果第一波高频事件还在持续触发,会马上响应(设定第二个timeout,启动第二个周期);这种情况下两个周期之间没有间隔,是连续的周期。
(3-2) 如果第一波高频事件在第一个周期末尾前停止了,就不会马上设定第二个timeout;直到第二波高频事件的第一次触发,才会设定第二个timeout,启动第二个周期;这种情况下两个周期之间会有间隔。

改用setTimeout并做封装:

<script>
    document.addEventListener('DOMContentLoaded', function () {
        function throttle(handler, period) {
            var flag = true;
            var timer = null;
            return function () {
                if (flag == true) {
                    flag = false;
                    timer = setTimeout(function () {
                        handler();
                        flag = true;
                    }, period);
                } else {
                    //console.log('nothing happened during last period');
                }
            };
        }

        document.addEventListener('scroll', throttle(function(){
            console.log('you have scrolled during last period');
        }, 1000));
    });
</script>

完成。可以对比一下防抖和节流的代码实现,有些类似,区别在于:
(1) 防抖要清除timeout再设定timeout,不会产生重复timeout。
(2) 节流则是利用节流阀变量来阻挡重复timeout,等timeout到时自动完成、清除。

三、更多:拆解封装的函数以及闭包问题
对我来说返回一个函数还是比较新奇的,接触还不多。另外,throttle()函数中声明了一个flag,是否每次scroll之后都会调用throttle()函数并声明了新的flag变量?如果同时存在很多flag那就乱套了,没法正确判断该不该执行响应动作了。

所以我试着拆解封装的函数,发现必须要将flag、timer的声明放在addEventListener()外面,变成全局变量才可以。拆解如下:

<script>
    document.addEventListener('DOMContentLoaded', function () {
        var flag = true;
        var timer = null;
        document.addEventListener('scroll', function () {
            if (flag == true) {
                flag = false;
                timer = setTimeout(function () {
                    console.log('you have scrolled during last period');
                    flag = true;
                }, 1000);
            } else {
                //console.log('nothing happened during last period');
            }
        });
    });
</script>

说明什么问题:
(1) throttle()函数处理scroll原本的handler,相当于修改了原来的handler,一次性提供了“增强版”的handler;这种返回函数的形式不存在重复调用,只调用了一次throttle(),后续scroll重复调用的是“增强版”handler。(这里给我一种认识:函数声明本身不是对象,而是一组语句,对参数进行加工;构造函数new对象、Function对象才是对象)
(2) 拆解后flag、timer必须写到addEventListener()外面,是因为写里面就是写在handler里面,handler是会被重复调用的,就会重复声明flag、timer。
(3) 封装的throttle()返回了一个包裹原本handler的函数,这里又必须把flag、timer写到return外面,但不必写到throttle()外面作为全局变量;不能写到return里面道理如(2),可以写到throttle()里面是因为retutn的函数引用了flag、timer,构成了闭包;throttle()没有被多次调用,所以没有声明新的flag、timer,加上闭包变量不会被回收,所以这样是OK的。


BENCJL
15 声望0 粉丝

问天问大地