• 性能优化之节流(throttling)与防抖(debounce)


    前言

    首先来举个例子。百度首页的百度输入框,用户输入的时候,每次输入的信息,我们都能看到百度服务器返回给我们的联想关键字。我们每改动一个字,它就换一次联想词,这是我们肉眼能看到的速度,实际上如果不加以处理,可能已经上服务器发起了好几十次的同一个关键字联想请求了,具体速度依赖于不同的pc等机器上的运行速度不同。那么,刚刚也谈到,对于同一个关键字,请求这么多次,也许想给用户呈现的就一次,剩下的请求都是浪费的,并且如果成千上万甚至上亿的用户同时请求,对服务器的负担是巨大的。

    防抖与节流解决的问题

    从上面的例子,我们需要寻求一个解决方案。防抖和节流就是针对响应跟不上触发频率这类问题的两种解决方案。在给DOM绑定事件时,有些事件我们是无法控制触发频率的。 如鼠标移动事件onmousemove, 滚动滚动条事件onscroll,窗口大小改变事件onresize,瞬间的操作都会导致这些事件会被高频触发。 如果事件的回调函数较为复杂,就会导致响应跟不上触发,出现页面卡顿,假死现象。 在实时检查输入时,如果我们绑定onkeyup事件发请求去服务端检查,用户输入过程中,事件的触发频率也会很高,会导致大量的请求发出,响应速度会大大跟不上触发。

    debounce和throttling的策略

    针对此类快速连续触发和不可控的高频触发问题,debounce 和 throttling 给出了两种解决策略:

    1. debounce的策略

    1.1 延迟debounce

    debounce,去抖动。策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。 这是debounce的基本思想。
    延迟debounce,示意图:

    1.2 前缘debounce

    在后期进行优化又扩展了前缘debounce,即执行动作在前,然后设定周期,周期内有事件被触发,不执行动作,且周期重新设定。

    为什么要这样呢?

    试想第一种延迟debounce,我们本来想对用户输入的关键字,发起请求联想的频率降低,但是如果用户在我们设定的时间中,一直输入,导致的就是,用户一直看不到关键字,我们倒不如第一次输入的时候就发起一个请求,服务器返回结果,呈现给用户,然后后续用户的键入结束在继续请求)

    前缘debounce,示意图

    debounce的特点是当事件快速连续不断触发时,动作只会执行一次。 延迟debounce,是在周期结束时执行,前缘debounce,是在周期开始时执行。但当触发有间断,且间断大于我们设定的时间间隔时,动作就会有多次执行。

    1.3 延迟debounce的实现

    var debounce = (fn, wait) => {
    	let timer, timeStamp=0; // timer and time stamp
    	let context, args;
     
    	let run = ()=>{
    		timer= setTimeout(()=>{
    			fn.apply(context,args);
    		},wait);
    	}
    	let clean = () => {
    		clearTimeout(timer);
    	}
     
    	return function(){
    		context=this;
    		args=arguments;
    		let now = (new Date()).getTime();
     
    		if(now-timeStamp < wait){
    			console.log('reset',now);
    			clean();  // clear running timer 
    			run();    // reset new timer from current time
    		}else{
    			console.log('set',now);
    			run();    // last timer alreay executed, set a new timer
    		}
    		timeStamp=now;
     
    	}
    }
    
    

    1.4 前缘debounce的实现

    
    // 优化版: 定时器执行时,判断start time 是否向后推迟了,若是,设置延迟定时器
    var debounce = (fn, wait) => {
    	let timer, startTimeStamp=0;
    	let context, args;
     
    	let run = (timerInterval)=>{
    		timer= setTimeout(()=>{
    			let now = (new Date()).getTime();
    			let interval=now-startTimeStamp
    			if(interval<timerInterval){ // the timer start time has been reset, so the interval is less than timerInterval
    				console.log('debounce reset',timerInterval-interval);
    				startTimeStamp=now;
    				run(timerInterval-interval);  // reset timer for left time 
    			}else{
    				fn.apply(context,args);
    				clearTimeout(timer);
    				timer=null;
    			}
    			
    		},timerInterval);
    	}
     
    	return function(){
    		context=this;
    		args=arguments;
    		let now = (new Date()).getTime();
    		startTimeStamp=now;
     
    		if(!timer){
    			console.log('debounce set',wait);
    			run(wait);    // last timer alreay executed, set a new timer
    		}
    		
    	}
     
    }
    

    1.5 在前缘debounce基础上再添加一个是否立即执行的选项

    // 增加前缘触发功能
    var debounce = (fn, wait, immediate=false) => {
    	let timer, startTimeStamp=0;
    	let context, args;
     
    	let run = (timerInterval)=>{
    		timer= setTimeout(()=>{
    			let now = (new Date()).getTime();
    			let interval=now-startTimeStamp
    			if(interval<timerInterval){ // the timer start time has been reset,so the interval is less than timerInterval
    				console.log('debounce reset',timerInterval-interval);
    				startTimeStamp=now;
    				run(timerInterval-interval);  // reset timer for left time 
    			}else{
    				if(!immediate){
    					fn.apply(context,args);
    				}
    				clearTimeout(timer);
    				timer=null;
    			}
    			
    		},timerInterval);
    	}
     
    	return function(){
    		context=this;
    		args=arguments;
    		let now = (new Date()).getTime();
    		startTimeStamp=now; // set timer start time
     
    		if(!timer){
    			console.log('debounce set',wait);
    			if(immediate) {
    				fn.apply(context,args);
    			}
    			run(wait);    // last timer alreay executed, set a new timer
    		}
    		
    	}
     
    }
    

    2. throttling节流的策略

    throttling,节流的策略是,固定周期内,只执行一次动作,若有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。 节流策略也分前缘和延迟两种。
    与debounce类似,延迟是指 周期结束后执行动作,前缘是指执行动作后再开始周期。

    2.1 延迟throttling示意图:

    2.2 前缘throttling示意图

    2.3 延迟throttling的实现

    // 简单版: 定时器期间,只执行最后一次操作
    var throttling = (fn, wait) => {
    	let timer;
    	let context, args;
     
    	let run = () => {
    		timer=setTimeout(()=>{
    			fn.apply(context,args);
    			clearTimeout(timer);
    			timer=null;
    		},wait);
    	}
     
    	return function () {
    		context=this;
    		args=arguments;
    		if(!timer){
    			console.log("throttle, set");
    			run();
    		}else{
    			console.log("throttle, ignore");
    		}
    	}
     
    }
    
    

    2.4 前缘throttling的实现

    /// 增加前缘
    var throttling = (fn, wait, immediate) => {
    	let timer, timeStamp=0;
    	let context, args;
     
    	let run = () => {
    		timer=setTimeout(()=>{
    			if(!immediate){
    				fn.apply(context,args);
    			}
    			clearTimeout(timer);
    			timer=null;
    		},wait);
    	}
     
    	return function () {
    		context=this;
    		args=arguments;
    		if(!timer){
    			console.log("throttle, set");
    			if(immediate){
    				fn.apply(context,args);
    			}
    			run();
    		}else{
    			console.log("throttle, ignore");
    		}
    	}
     
    }
    
    
    

    总结

    debounce和throttling 各有特点,在不同 的场景要根据需求合理的选择策略。如果事件触发是高频但是有停顿时,可以选择debounce; 在事件连续不断高频触发时,只能选择throttling,因为debounce可能会导致动作只被执行一次,界面出现跳跃。

  • 相关阅读:
    How to get the IIS root path in other application.
    Web.UI.Controls与页面事件的冲突问题。
    分析在服务器上设置计时器的问题。
    首次感觉我的电脑过时了。。。。。。。。郁闷。
    Google Logos
    2005年的最后一天
    TreeView的几个使用小技
    浅淡反射问题
    The restricted headers are:
    在服务器上用Timer遇到的小问题。。。。
  • 原文地址:https://www.cnblogs.com/fe-linjin/p/10890101.html
Copyright © 2020-2023  润新知