监听与节流

688 阅读3分钟

防抖和节流

其实好多地方都有接触到,一直没有时间做个小总结。关键是在于一定时间内的延迟事件的触发。之前项目中的轮询和长链接其实大都是运用到了类似的方法。

窗口resize,鼠标touchmove事件,scroll,以及类似keypress这样的密集监听事件的变动,都会产生大量的性能消耗。

  • 常规的防抖节流方案debounce

  • 窗口事件,按压,touch移动等相关的事件都可以用来进行优化,防止密集触发导致页面卡死。其中关于context和arguments的介绍可以参考一下JS中的arguments参数,我觉得这样理解比较好.

  • 作用是在短时间内多次触发同一个函数,只执行最后一次,或者只在开始时执行。

    // debounce 函数接受一个函数和延迟执行的时间作为参数
    /*
    *@fn 实际调用的回调函数体
    *@delay 触发回调事件的延迟时间
    */
    function debounce(fn, delay){
        let timer = null;
        return function() {
            // 获取函数的作用域和变量
            let context = this; // 函数的隐式参数一般有两个,一个是this,未来调用函数的实际上下文作用域,还有一个就是封装实参的对象arguments。
            let args = arguments;
            clearTimeout(timer);
            timer = setTimeout(function(){
                fn.apply(context, args);
            }, delay)
        }
    }
    
    • 优化后的debounce,优化添加一个立即执行函数参数
    function debounce(func, delay, immediate){
        var timer = null;
        return function(){
            var context = this;
            var args = arguments;
            if(timer) clearTimeout(timer);
            if(immediate){
                var doNow = !timer;
                timer = setTimeout(function(){
                    timer = null;
                },delay);
                if(doNow){
                    func.apply(context,args);
                }
            }else{
                timer = setTimeout(function(){
                    func.apply(context,args);
                },delay);
            }
        }
    }
    

    内部运行原理

    1. 第一次进入监听函数体,给闭包一个初始化timer为null,内部作为一个个闭包返回体函数,其内部变量会进行内存驻留,而外部的变量无法影响到当前的闭包作用下的timer,也就是每次触发监听时,tiemr永远都是上次返回的值。此时doNow为true,回调函数第一次进入就会执行。
    2. setTimeout函数会生产一个计数器ID返回,每次内部clearTimeout清除之后,就相当于返回timer为整形的参数,上次的回调函数事件也会被阻止。
    3. 在频繁触发监听事件时并且时间间隔没有超过delay,闭包函数体第三行的timer会被clear,并且此时timer为返回的是整形,再次触发immediate参数时,doNow=!计数Id 返回false,所以回调函数并不会执行
    4. 如果没有传immediate参数,直接走延时函数,和上面的方案一致。
  • 节流:类似于防抖,节流是在一段时间内只允许函数执行一次。 input监听,输入框智能提示以及像高德地图的POI智能检索地区,密码输入提示等的相关案例都是这么实现的。

时间戳实现:

var throttle = function(func, delay){
    var prev = Date.now();
    return function(){
        var context = this;
        var args = arguments;
        var now = Date.now();
        if(now-prev>=delay){
            func.apply(context,args);
            prev = Date.now();
        }
    }
}

当监听密集时间间隔小于自己设置delay时间,由于每次的触发监听时,prev时间是进入函数时,初始化的时间。闭包内的函数now-prev < delay不会执行,这样就可以做到限流。

定时器实现:

var throttle = function(func, delay){
    var timer = null;

    return function(){
        var context = this;
        var args = arguments;
        if(!timer){
            timer = setTimeout(function(){
                func.apply(context, args);
                timer = null;
            },delay);
        }
    }
}

两次实现的原理基本类似,时间戳节流可以做到第一次先执行,定时器的方式是等延迟结束后执行。具体怎么使用,还是看具体的业务。其中和核心思想,还是在于闭包。

闭包

通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。可以看这一篇js中的闭包