💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 什么是防抖和节流?有什么区别?如何实现? **防抖是控制次数,节流是控制频率** 19回复 1. **防抖** > 触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间 * 思路: > 每次触发事件时都取消之前的延时调用方法 ~~~js function debounce(fn) { let timeout = null; // 创建一个标记用来存放定时器的返回值 return function () { // 每当用户输入的时候把前一个 setTimeout clear 掉 clearTimeout(timeout); timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, //这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话, //就不会执行 fn 函数 fn.apply(this, arguments); //关键在第一个参数,为了确保上下文环境为当前的this,所以不能直接用fn。 }, 500); }; } function sayHi() { console.log('防抖成功'); } var inp = document.getElementById('inp'); inp.addEventListener('input', debounce(sayHi)); // 防抖 ~~~ 2. **节流** > 高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率 * 思路: > 每次触发事件时都判断当前是否有等待执行的延时函数 ~~~js function throttle(fn) { let canRun = true; // 通过闭包保存一个标记 return function () { if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return canRun = false; // 立即设置为false setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中 fn.apply(this, arguments); //关键在第一个参数,为了确保上下文环境为当前的this,所以不能直接用fn。 // 最后在setTimeout执行完毕后再把标记设置为true //(关键)表示可以执行下一次循环了。 //当定时器没有执行的时候标记永远是false,在开头被return掉 canRun = true; }, 500); }; } function sayHi(e) { console.log(e.target.innerWidth, e.target.innerHeight); } window.addEventListener('resize', throttle(sayHi)); ~~~ # start https://www.jianshu.com/p/c8b86b09daf0 在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。 通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。 让我们先来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。 html 文件中代码如下 ~~~xml <div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div> <script> let num = 1; let content = document.getElementById('content'); function count() { content.innerHTML = num++; }; content.onmousemove = count; </script> ~~~ 在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。效果如下 ![](//upload-images.jianshu.io/upload_images/4842858-652a8eb5c73db0c7.gif?imageMogr2/auto-orient/strip|imageView2/2/w/1132/format/webp) 可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。 ###### 防抖(debounce) **所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。** 防抖函数分为非立即执行版和立即执行版。 非立即执行版: ~~~jsx function debounce(func, wait) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { func.apply(context, args) }, wait); } } ~~~ 非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 我们依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数,我们可以这么使用 ~~~swift content.onmousemove = debounce(count,1000); ~~~ 效果如下 ![](//upload-images.jianshu.io/upload_images/4842858-1f6389b9dd9e5ef9.gif?imageMogr2/auto-orient/strip|imageView2/2/w/1134/format/webp) 可以看到,在触发事件后函数 1 秒后才执行,而如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。 上述防抖函数的代码还需要注意的是 this 和 参数的传递 ~~~jsx let context = this; let args = arguments; ~~~ 防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。 立即执行版: ~~~jsx function debounce(func,wait) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); let callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait) if (callNow) func.apply(context, args) } } ~~~ 立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。 使用方法同上,效果如下 ![](//upload-images.jianshu.io/upload_images/4842858-067785c056f182d8.gif?imageMogr2/auto-orient/strip|imageView2/2/w/1140/format/webp) 在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。 双剑合璧版: ~~~jsx /** * @desc 函数防抖 * @param func 函数 * @param wait 延迟执行毫秒数 * @param immediate true 表立即执行,false 表非立即执行 */ function debounce(func,wait,immediate) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait) if (callNow) func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } } ~~~ ###### 节流(throttle) **所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。**节流会稀释函数的执行频率。 对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。 时间戳版: ~~~jsx function throttle(func, wait) { let previous = 0; return function() { let now = Date.now(); let context = this; let args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } } } ~~~ 使用方式如下 ~~~swift content.onmousemove = throttle(count,1000); ~~~ 效果如下 ![](//upload-images.jianshu.io/upload_images/4842858-80423b8898a27732.gif?imageMogr2/auto-orient/strip|imageView2/2/w/1140/format/webp) 可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。 定时器版: ~~~jsx function throttle(func, wait) { let timeout; return function() { let context = this; let args = arguments; if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args) }, wait) } } } ~~~ 使用方式同上,效果如下 ![](//upload-images.jianshu.io/upload_images/4842858-bf2ed4c8ed4f0ec0.gif?imageMogr2/auto-orient/strip|imageView2/2/w/1136/format/webp) 可以看到,在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。 我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。 同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现双剑合璧版的节流函数。 双剑合璧版: ~~~tsx /** * @desc 函数节流 * @param func 函数 * @param wait 延迟执行毫秒数 * @param type 1 表时间戳版,2 表定时器版 */ function throttle(func, wait ,type) { if(type===1){ let previous = 0; }else if(type===2){ let timeout; } return function() { let context = this; let args = arguments; if(type===1){ let now = Date.now(); if (now - previous > wait) { func.apply(context, args); previous = now; } }else if(type===2){ if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args) }, wait) } } } } ~~~ 作者:淘淘笙悦 链接:https://www.jianshu.com/p/c8b86b09daf0 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 ## 参考文章 * [第 3 题:什么是防抖和节流?有什么区别?如何实现?](https://www.muyiy.cn/question/js/3.html) * [JavaScript专题之跟着underscore学防抖](https://github.com/mqyqingfeng/Blog/issues/22) * [JavaScript专题之跟着 underscore 学节流](https://github.com/mqyqingfeng/Blog/issues/26)