节流

作者: 姜治宇 | 来源:发表于2020-03-29 12:19 被阅读0次

一、什么是节流?

节流就是一定时间内,一个动作只执行一次。

二、为什么要节流?

js有一些事件触发的频率肥肠高。比如:调整窗口事件(resize)、滚动条事件(scroll)、鼠标悬浮事件(mouseover、mouseout、mouseenter、mouseleave)、键盘事件(keyup、keydown)等等,如果在这些事件的回调函数中有ajax请求、动画和dom的操作,那不仅会增加服务器负担,而且会造成页面抖动、卡死甚至崩溃。

三、如何实现节流?

常见的方案是用setTimeout定时器来实现,先来看一个简易版的。

function throttle(fn,wait){
        var timer = null;
        
        return function(){
            var context = this;
            var args = arguments;
            console.log('timer>>',timer)
            if(!timer){
                timer = setTimeout(function(){
                    fn.apply(context,args);
                    clearTimeout(timer)
                    timer = null

                },wait)
            }
        }
    }

这个版本很好理解,我们利用闭包,在函数内部返回一个匿名函数,让timer变量在内存中不释放,这样下次触发时,内存检测到timer已经赋值了,所以不会再让fn执行。
我们测试一下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>throttle</title>
</head>
<body>

</body>
</html>
<script>
    function throttle(fn,wait){
        var timer = null;

        return function(){
            var context = this;
            var args = arguments;
            console.log('timer>>',timer)
            if(!timer){
                timer = setTimeout(function(){
                    fn.apply(context,args);
                    clearTimeout(timer)
                    timer = null

                },wait)
            }
        }
    }
    function resizeFunc(){
        console.log('resize')

    }
    window.addEventListener('resize',throttle(resizeFunc,500))

</script>

测试结果:

test.jpg
大家应该发现问题了,就是timer的值并未清零。每次setTimeout,系统都会分配一个计数器id,这个id从1开始自增。虽然我们调用了clearTimeout来清除timer,但实际上计数器还是自增,这是否说明定时器仍在内存中未被释放呢?难道是因为闭包的原因?
如果是这样,我们不妨摒弃闭包的方式,换一种思维来实现。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>throttle</title>
</head>
<body>

</body>
</html>
<script>
    var throttleID = null;
    function throttleLow(){
        var isClear = arguments[0],fn;


        if(typeof isClear === 'boolean'){
            fn = arguments[1]
            console.log(throttleID)
            //清除定时器
            clearTimeout(throttleID)
        } else{
            fn = isClear
            var wait = arguments[1]
            arguments.callee(true,fn)
            var that = this
            throttleID = setTimeout(function(){
                fn.call(that)
            },wait)
        }
    }
    function resizeFunc(){
        console.log('resize')

    }
    window.addEventListener('resize',function(){
        throttleLow(resizeFunc,1000)
    })

</script>

throttleID是全局变量,只要确保释放掉它就可以了。但实际经过测试发现,throttleID仍旧是自增的,为啥呢?
我们知道,定时器是异步的,是定时器触发线程来执行的,每新增一个定时任务,序号就会加1。而一旦定时器线程运行完成后,就会将回调推入宏任务队列,等JS引擎执行栈空闲了,就从宏任务队列调入回调函数并执行。
因此,销毁定时器的动作是在JS引擎线程中完成的,而新定时任务的序号分配是定时器线程控制,这两个线程之间彼此独立互不干扰。
但这样实现还有个问题:
throttleID这个变量必须是全局的!
如果throttleID是局部变量,那每次进来都是null,那clearTimeout就没用了。真是让人掉头发啊,如果能保存住throttleID又不用全局变量就好了。有办法吗?
当然有!就是利用fn。因为fn是对象,当它作为引用型变量传入形参后,可以在fn下(函数本身也是对象)新增一个键保存计数器id,下次再进来时,同一个fn下的计数器id仍在。

function throttleSuper(){
        var isClear = arguments[0],fn;
        if(typeof isClear === 'boolean'){
            fn = arguments[1]
            //清除定时器
            fn._throttleID && clearTimeout(fn._throttleID)
        } else{
            fn = isClear
            var wait = arguments[1]
            arguments.callee(true,fn)
            var that = this
            fn._throttleID = setTimeout(function(){
                fn.call(that)
            },wait)
        }
    }

两种实现办法大家择其一便可。

相关文章

网友评论

    本文标题:节流

    本文链接:https://www.haomeiwen.com/subject/ragsuhtx.html