细谈js-防抖、节流

343 阅读2分钟

应用场景

防抖和节流是针对高频触发而出现的控制触发频率的解决方案(属于性能优化方案)。

如: 如鼠标移动事件onmousemove, 滚动滚动条事件onscroll,窗口大小改变事件onresize,监听输入框oninput事件。。。。

防抖(debounce)

我先来说一个常见的场景:input输入框远程搜索功能,这里则需要监听oninput事件触发后请求接口。 这时候会出现一个问题,用户输入搜索内容是一个字一个输入的,只要input框内容改变就请求一次接口。其实只有最后一次输入的内容才是用户想要搜索的,这导致了服务器资源的浪费

这时候就进行改造,目标为:用户输入过程中不触发请求,停止输入400毫秒后再触发。 这时候就用到了防抖(debounce)

版本1(延迟执行): 周期内有新事件触发,清除旧定时器,重置新定时器。

	
// 策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。
var debounce = (fn, wait) => {
	let timer, // 定时器id
    	timeStamp=0// 最近一次执行时间
	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('重置',now);
			clean();  // 清除定时器
			run();    // 从当前时间重置新的计时器
		}else{
			console.log('set',now);
			run();    // 上次计时器已经执行,设置一个新的计时器
		}
        // 记录最近一次执行时间
		timeStamp=now;
	}
}

// 使用
document.getElementById('inp').addEventListener(
'input', 
  // 执行debounce函数 返回触发执行函数 形成闭包
  debounce(function() {
    console.log(this.value)
  }, 400)
);

看效果

版本2: 周期内有新事件触发,记录最近触发时间,当前周期结束后判断当前周期内是否有触发,如有则设置延时器,以此类推,直到周期结束后判断当前周期内没有触发则执行动作。(此版本不会清除定时器)

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){ // 判断当前周期内是否被触发
				console.log('debounce reset',timerInterval-interval);
				startTimeStamp=now;
				run(wait-interval);  // 重置定时器的剩余时间
			}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);    // 上次计时器已经执行,设置一个新的计时器
		}
		
	}
 
}

版本3(前缘执行): 在版本二基础上添加是否立即执行选项

当事件快速连续不断触发时,动作只会执行一次,前缘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){ // 定时器开始时间被重置,所以interval小于timerInterval
				console.log('debounce reset',timerInterval-interval);
				startTimeStamp=now;
				run(wait-interval);  // 重置定时器的剩余时间
			}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; // 记录最近一次触发时间
 
		if(!timer){
			console.log('debounce set',wait);
            		// 立即执行
			if(immediate) {
				fn.apply(context,args);
			}
			run(wait);    // 上次计时器已经执行,设置一个新的计时器
		}
		
	}
 
}

节流(throttling)

在周期内只执行一次,如有新的事件触发不执行。周期结束后又有事件触发,开始新的周期。节流策略也分前缘和延迟两种。

延迟:throttling

举个栗子:监听浏览器滚动条滚动事件

// 代码如下 触发滚动事件打印滚动条位置
onscroll = function() {
	console.log(this.scrollY)
}

这时会出现个情况 触发频率非常高,我就按了一下下箭头触发了九次。

如果处理逻辑发杂的话就对浏览器性能造成了很大负担,这时候节流就登场了。

// 定时器期间,触发不执行,只在定时器结束后执行
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("周期开始");
			run();
		}else{
			console.log("被节流");
		}
	}
 
}

// 使用 
onscroll = throttling(function() {console.log(this.scrollY)},200)

前缘throttling

// 上个版本基础上添加前缘

var throttling = (fn, wait, immediate=false) => {
	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("周期开始");
            		// 前缘执行
			if(immediate){
				fn.apply(context,args);
			}
			run();
		}else{
			console.log("被节流");
		}
	}
 
}