时间:2022-07-03 09:31:35 | 栏目:JavaScript代码 | 点击:次
前言:
防抖(Debounce) 和 节流(Throttle) 技术用于限制函数执行的次数。通常,一个函数将被执行多少次或何时执行由开发人员决定。但在某些情况下,开发人员会将这种能力赋予用户,由用户决定执行该功能的时间和次数。
例如,添加到click
、scroll
、resize
等事件上的函数,允许用户决定何时执行它们以及执行多少次。有时,用户可能会比所需更频繁地执行这些操作。这可能不利于网站的性能,特别是如果附加到这些事件的函数执行一些繁重的计算。在这种情况下,用户可以控制函数的执行,开发人员必须设计一些技术来限制用户可以执行函数的次数。
举个例子,假设我们为滚动事件scroll
添加了一个函数,该函数中会执行修改DOM元素的操作。我们知道,修改DOM元素大小开销很大,会引起浏览器的回流(Reflow)和重排(Repaint),以及重新渲染整个或部分页面。如果用户频繁滚动,导致该函数频繁被调用,可能会影响网页性能或导致页面卡顿等。
此外,有些事件回调函数中包含ajax等异步操作的时候,多次触发会导致返回的内容结果顺序不一致,而导致得到的结果非最后一次触发事件对应的结果
所以,为了优化网页的性能,控制函数被调用的频率是很有必要的,防抖(Debounce) 和 节流(Throttle) 是通过控制函数被调用的频率来优化脚本性能的两种方法
防抖:无论用户触发多少次事件,对应的回调函数只会在事件停止触发指定事件后执行。(即:回调函数在事件停止触发指定时间后被调用)
例如,假设用户在 100 毫秒内点击了 5 次按钮。防抖技术不会让这些点击中的任何一个执行对应的回调函数。一旦用户停止点击,如果去抖时间为 100 毫秒,则回调函数将在 100 毫秒后执行。因此,肉眼看来,防抖就像将多个事件组合成一个事件一样。
触发事件后函数不会立即执行,而是在停止事件触发后 n 毫秒后执行,如果在 n 毫秒内又触发了事件,则会重新计时
/** * @desc 函数防抖 * @param func 回调函数 * @param delay 延迟执行毫秒数 */ function debounce(func, delay) { let timer; // 定时器 return function () { let context = this; // 记录 this 值,防止在回调函数中丢失 let args = arguments; // 函数参数 //如果定时器存在,则清除定时器(如果没有,也没必要进行处理) timer ? clearTimeout(timer) : null; timer = setTimeout(() => { // 防止 this 值变为 window func.apply(context, args) }, delay); } }
触发事件后立即执行回调函数,但是触发后n毫秒内不会再执行回调函数,如果 n 毫秒内触发了事件,也会重新计时。
/** * @desc 函数防抖 * @param func 回调函数 * @param delay 延迟执行毫秒数 */ function _debounce(func, delay) { let timer; // 定时器 return function () { let context = this; // 记录 this 值,防止在回调函数中丢失 let args = arguments; // 函数参数 // 标识是否立即执行 let isImmediately = !timer; //如果定时器存在,则清除定时器(如果没有,也没必要进行处理) timer ? clearTimeout(timer) : null; timer = setTimeout(() => { timer = null; }, delay); // isImmediately 为 true 则 执行函数(即首次触发事件) isImmediately ? func.apply(context, args) : null; } }
举个例子来对比一下两个版本的区别:
document.body.onclick= debounce(function () { console.log('hello') },1000)
如上代码中,我们给body添加了一个点击事件监听器。
hello
,要等 1000ms 后才会打印。在这 1000s 内如果还点击了 body,那么就会重新计时。即最后一次点击 body 过1000ms后控制台才会打印hello
hello
,但是在此之后的 1000ms 内点击 body ,控制台不会有任何反应。在这 1000s 内如果还点击了 body,那么就会重新计时。必须等计时结束后再点击body,控制台才会再次打印 hello
。通常,搜索框会提供下拉菜单,为用户当前的输入提供自动完成选项。但有时建议项是通过请求后端得到的。可以在实现提示文本时应用防抖,在等待用户停止输入一段时间后再显示建议文本。因此,在每次击键时,都会等待几秒钟,然后再给出建议。
window 触发 resize 的时候,不断的调整浏览器窗口大小会不断触发这个事件,用防抖让其只触发一次
例如掘金一类的网站,都会内嵌文本编辑器,在编辑过程中会自动保存文本,防止数据丢失。每次保存都会与后端进行数据交互,所以可以应用防抖,在用户停止输入后一段时间内再自动保存。
通常对于一些特殊格式的输入项,我们通常会检查格式。我们可以应用防抖在用户停止输入后一段时间再进行格式检测,而不是输入框中内容发生改变就检测。
节流:无论用户触发事件多少次,附加的函数在给定的时间间隔内只会执行一次。(即:回调函数在规定时间内最多执行一次)
例如,当用户单击一个按钮时,会执行一个在控制台上打印Hello, world
的函数。现在,假设对这个函数应用 1000 毫秒的限制时,无论用户点击按钮多少次,Hello, world
在 1000 毫秒内都只会打印一次。节流可确保函数定期执行。
/** * @desc 函数节流 * @param func 回调函数 * @param limit 时间限制 */ const throttle = (func, limit) => { let inThrottle; // 是否处于节流限制时间内 return function() { const context = this; const args = arguments; // 跳出时间限制 if (!inThrottle) { func.apply(context, args); // 执行回调 inThrottle = true; // 开启定时器计时 setTimeout(() => inThrottle = false, limit); } } }
/** * @desc 函数节流 * @param func 回调函数 * @param limit 时间限制 */ function throttle(func, limit) { //上次执行时间 let previous = 0; return function() { //当前时间 let now = Date.now(); let context = this; let args = arguments; // 若当前时间-上次执行时间大于时间限制 if (now - previous > limit) { func.apply(context, args); previous = now; } } }
很多现有的库中已经实现了防抖函数和节流函数
拿王者荣耀为例,通常都有攻速一说。如果攻速低,即使 n 毫秒内点击平A按钮多次,也只会执行一次平A。其实这里就类似于节流的思想,可以通过设置节流的时间间隔限制,来改变攻速。
如果滚动事件被触发得太频繁,可能会影响性能,因为它包含大量视频和图像。因此滚动事件必须使用节流
如何选择防抖和节流:
关于防抖函数和节流函数的选择,一篇博客中是这样建议的:
A debounce is utilized when you only care about the final state. A throttle is best used when you want to handle all intermediate states but at a controlled rate.
即:如果只关心最终状态,建议使用防抖。如果是想要函数以可控的速率执行,那么建议使用节流。
延时多久合理: