一文读懂函数的防抖和节流
什么是防抖和节流?
「 防抖 」
场景:输入名称的同时去服务器校验名称是否重复,如果代码没做限制,输一次发一次请求;多次点击触发事件
在事件被触发n秒后在执行回调函数,如果在n秒内又触发,则重新计时
假设一个场景:鼠标划过一个div,触发onmousemove事件,它内部的文字会显示当前鼠标的坐标。
防抖:函数防抖,这里的抖动就是执行的意思,而一般的抖动都是持续的,多次的。假设函数持续多次执行,我们希望它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。
分解一下需求:
- 持续触发不执行
- 不触发的一段时间之后在执行
那么怎么实现上述的目标呢?
- 在不触发的时间内在执行,那就需要一个定时器,
- 定时器里面调用我们要执行的函数,将arguments传入,封装一个函数,让持续触发的事件监听我们封装的这个函数,将目标函数作为回调传进去,第二点就实现了,
- 再看第一点:持续触发不执行。我们先思考一下,是什么让我们的函数执行了呢?是上边的setTimeout。
- OK,那么现在的问题就变成了持续触发,不能有 setTimeout,这样直接在持续触发的时候,清掉计时器就好了。
「 节流 」
节流的会用在input,keyup更频繁触发事件中如resize、touchmove。「节流」会强制函数以固定的速率执行。节流的概念可以想象一下水坝,你建了水坝在河道中,不能让水流动不了,你只能让水流的慢一些。换言之,你不能让用户的方法都不执行,如果这样干,就是「防抖」了。
节流的意思是让函数有节制的执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内触发一次。
分解一下需求:
- 持续触发并不会执行多次
- 到一定时间再去执行
那么怎么实现上述的目标呢?
- 持续触发,并不会执行,但是时间到了就会执行。抓取一个关键的点「执行的时机」。
- 要做到控制执行的时机,我们可以通过一个开关,与定时器setTimeout结合完成。
- 函数执行的前提条件是开关打开,持续触发时,持续关闭开关。
- 等到setTimeout到时间了再把开关打开,函数就会执行了。
注:这里的throttle函数执行的结果是其内部return的function的调用,也就是说鼠标经过的事件监听实际上是这个被return的function,不断持续触发的是它。
而 throttle函数只是提供了一个作用域,内部闭包声明一个run的开关变 量。
由于闭包存在run这个变量会一直存在不被销毁,而let run = true只在这个闭包(局部作用域)内只声明了一次,但它不会被持续执行,所以return的函数内部的判断不会被它覆盖掉。
总结:
防抖和节流巧妙地用了setTimeout,来控制函数执行的时机,优点很明显,可以节约性能,不至于多次触发复杂的业务逻辑而造成页面卡顿。
附上完整代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <style> #container { width: 500px; height: 200px; background: #ccc; font-size: 20px; text-align: center; line-height: 200px; } </style> <body> <div id="container"></div> </body> <script> // 防抖 function debounce(func, delay){ let timeout; return function() { clearTimeout(timeout) timeout = setTimeout(()=>{ func.apply(this, arguments) },delay) } } const container = document.querySelector('div'); // 调用方法 container.onmousemove = debounce(function(e){ container.innerHTML = `clientX = ${e.clientX},clientY ${e.clientY}` },1000); // 节流 function throttle (func, delay ) { let run = true; console.log ('触发的时候', run); return function ( ) { console.log ('return的函数内容', run); if(!run) { return false // 如果关闭了开关,就不执行下面的代码 } console.log ('判断开关关闭与否之后的', run); run = false; // 持续触发的话, run一直是false,就会停在上边的判断那里 console.log ('开关关闭后', run); setTimeout(() => { func.apply(this, arguments); run = true; console.log ('开关打开后', run); }, delay) } } const container = document.querySelector('div'); // 调用方法 container.onmousemove = throttle(function(e){ container.innerHTML = `clientX = ${e.clientX},clientY ${e.clientY}` },1000); </script> </html>