目录:
1、什么是防抖和节流
2、防抖和节流的实现
防抖和节流是函数的防抖和函数的节流。在进行窗口的 resize、scroll、输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时,可以采用 debounce (防抖) 和 throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
函数防抖(debounce): 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才执行一次,如果设定的事件到来之前,又一次触发了事件,就重新开始延时。
前端开发过程中的 resize, scroll, mousemove, mousehover 等,会被频繁地触发,不做限制的话,有可能一秒之内执行几十次、几百次,如果在这些函数内部执行了其他函数,尤其是执行了操作 DOM 的函数,那不仅会造成计算机资源的浪费,还会降低程序运行速度,甚至造成浏览器卡死、崩溃。除此之外,重复的 AJAX 调用不仅会造成数据关系的混乱,还会造成网络阻塞,增加服务器压力,显然这个问题也是需要解决的。
“函数防抖” 的关键在于,在一个动作发生的一定时间之后,才执行特定事件。
防抖
下面是事件触发函数频繁执行的情况:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> *{ margin: 0; padding: 0; } #content{ width: 200px; height: 200px; line-height: 200px; background-color: #ccc; margin:0 auto; font-size: 60px; text-align: center; color: #000; cursor: pointer; } </style> </head> <body> <div id="content"></div> <script> let num = 1; let oDiv = document.getElementById('content'); let changeNum = function(){ oDiv.innerHTML = num++; } oDiv.onmousemove = changeNum; </script> </body> </html>
页面效果:
当鼠标在方块内划过时事件被触发,方块内的数字在疯狂地变动。
使用防抖函数去改进它。
防抖函数是在一个动作发生的一定时间之后,才执行特定事件。根据这个特性,自然而然地想起 setTimeout。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> *{ margin: 0; padding: 0; } #content{ width: 200px; height: 200px; line-height: 200px; background-color: #ccc; margin:0 auto; font-size: 60px; text-align: center; color: #000; cursor: pointer; } </style> </head> <body> <div id="content"></div> <script> let num = 1; let oDiv = document.getElementById('content'); let changeNum = function(){ oDiv.innerHTML = num++; } let deBounce = (fn, delay) => { let timer = null; return function(){ if( timer ){ clearTimeout(timer); } timer = setTimeout(function(){ fn(); }, delay) } } oDiv.onmousemove = deBounce(changeNum, 500); </script> </body> </html>
上面,首先定义一个函数 deBounce ,接收 2 个参数,参数 fn 是需要延迟执行的函数,参数 delay 指定一个时间。deBounce 函数返回一个函数,这个函数是 setTimeout 。为防止 setTimeout 累加定义了一个变量 timer,变量的初始值是 null, 它接受 setTimeout 的返回值,在执行 setTimeout 前会用 clearTimeout() 方法取消由 setTimeout() 方法设置的 timeout。当 deBounce 函数定义完成以后,给 oDiv 绑定一个事件,deBounce 接收第一个参数是 changeNum, 第二个参数指定延迟时间为 500 毫秒。
在浏览器查看页面效果时会发现,当在方块频繁地触发事件时,事件不会被频繁地触发,且鼠标移开方块事件只会触发一次,这说明防抖函数基本有效。
上面的防抖函数并不完善,setTimeout 的 this 指向 window,但是却是通过 oDiv 去调用的,所以需要在 fn 执行的时候绑定 this。
可以输出一下 this 确定一下 this 是不是指向window的。在浏览器打开触发事件在控制台可以看到 this 是指向 window 的。
let deBounce = (fn, delay) => {
let timer = null;
return function(){
if( timer ){
clearTimeout(timer);
}
timer = setTimeout(function(){
fn();
console.log( this );
}, delay)
}
}
可以使用 apply 方法绑定 this 作用域。但是有更简单的方法,利用箭头函数的特性去绑定 this 的作用域。
直接将 setTimeout 第一个参数用箭头函数来表示,在浏览器验证一下,可以看到此时 this 指向 div。
timer = setTimeout(()=>{ fn(); console.log( this ); }, delay)
(箭头函数可参考:https://es6.ruanyifeng.com/#docs/function#箭头函数 )
因为箭头函数没有自己的 this 作用域,this 是来自作用域链,同时也没有自己的 arguments。如果是需要绑定 arguments ,可以通过扩展运算符来绑定。
let deBounce = (fn, delay) => { let timer = null; return function(...args){ if( timer ){ clearTimeout(timer); } timer = setTimeout(()=>{ fn(...args); }, delay) } }
至此,一个简单的函数防抖功能就完成了。
节流
函数节流( throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
防抖和节流的作用都是防止函数多次调用。
下面来看一下函数防抖和函数节流的区别:
下面有 2 个button,点击 “ 防抖按钮 ” 调用 deBounce 函数,点击 “ 节流按钮 ”调用 throttle 函数。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .div1{ width: 500px; height: 300px; line-height: 300px; margin: auto; text-align: center; } button{ width: 100px; height:30px; } </style> </head> <body> <div class="div1"> <button>防抖按钮</button> <button>节流按钮</button> </div> <script> let fn = ()=>{ console.log('我被触发了'); } let deBounce = (fn, delay) => { let timer = null; return function( ...args ){ if( timer ){ clearTimeout( timer ); } timer = setTimeout(()=>{ fn( ...args ); }, delay) } } let throttle = (fn, delay) => { let flag = true; return function (...args) { if (!flag) return; flag = false; setTimeout(() => { fn(...args); flag = true; }, delay) } } let deBounceButton = document.getElementsByTagName('button')[0]; let throttleButton = document.getElementsByTagName('button')[1]; deBounceButton.onclick = deBounce(fn, 1000); throttleButton.onclick = throttle(fn, 1000); </script> </body> </html>
在十秒内连续不断点击 “ 防抖按钮 ”,停止点击后发现防抖函数只被触发了一次。在十秒内连续不断点击 “ 节流按钮 ”,停止点击后发现节流函数被触发了 10 次。
可以看出,防抖关注的是一定时间连续触发只在最后一次执行,节流侧重于一段时间内执行一次。
下面来看看怎么实现函数节流:
运行以下代码,打开浏览器查看效果,发现并没有实现节流。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7 </head> 8 <body> 9 <button>点击</button> 10 <script> 11 let oButton = document.getElementsByTagName('button')[0]; 12 let fn = ()=>{ 13 console.log( '我被响应了' ); 14 } 15 let throttle = (fn, delay)=>{ 16 return function(){ 17 setTimeout(()=>{ 18 fn(); 19 }, delay); 20 } 21 } 22 oButton.onclick = throttle(fn, 500); 23 </script> 24 </body> 25 </html>
可以利用开关变量来实现节流功能。
let throttle = (fn, delay) => { let flag = true; return function (...args) { if (!flag) return; flag = false; setTimeout(() => { fn(...args); flag = true; }, delay) } }
至此,实现了节流。也可以通过时间戳来实现防抖和节流。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7 </head> 8 <body> 9 <input type="text"> 10 <script> 11 let deBounce = (fn, delay) => { 12 let timer = null; 13 return function( ...args ){ 14 if( timer ){ 15 clearTimeout( timer ); 16 } 17 timer = setTimeout(()=>{ 18 fn( ...args ); 19 }, delay) 20 } 21 } 22 let oInput = document.getElementsByTagName( 'input' )[0]; 23 let ajax = (content) => { 24 let message = content; 25 let json = { message }; 26 console.log( JSON.stringify( json ) ); 27 } 28 let doAjax = deBounce( ajax, 2000 ); 29 //不使用防抖 30 oInput.addEventListener('keyup', (e)=>{ 31 console.log( e.target.value ); 32 }) 33 </script> 34 </body> 35 </html>
上面的代码执行的结果是:只要按下键盘输入数据,就会触发模拟的 AJAX 请求,这样不仅十分浪费资源,而且在实际的运用中,用户也是输出完整的字符后才会发起请求。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7 </head> 8 <body> 9 <input type="text"> 10 <script> 11 let deBounce = (fn, delay) => { 12 let timer = null; 13 return function( ...args ){ 14 if( timer ){ 15 clearTimeout( timer ); 16 } 17 timer = setTimeout(()=>{ 18 fn( ...args ); 19 }, delay) 20 } 21 } 22 let oInput = document.getElementsByTagName( 'input' )[0]; 23 let ajax = (content) => { 24 let message = content; 25 let json = { message }; 26 console.log( JSON.stringify( json ) ); 27 } 28 let doAjax = deBounce( ajax, 2000 ); 29 //使用防抖 30 oInput.addEventListener('keyup', (e)=>{ 31 doAjax( e.target.value ); 32 }) 33 </script> 34 </body> 35 </html>
上面的代码执行的结果是:当在频繁地输入时不会发起请求,只有当在指定的间隔内没有输入时,才会去执行函数,如果停止输入,但是在指定的间隔内又输入,会重新触发计时。当用户在不断输入值时,用防抖来请求节约资源。