防抖 Debounce
什么是防抖
如果有一个按钮,每点击一次就发送一个请求,
有时候说不好误触还是无聊,会一直点那个按钮,但是无意义的请求会占用很多资源,我们当然不希望这样。
所谓点击事件的防抖, 就是防止手抖连续点击, 就是指在连续点击中,只执行最后一次点击。
当然也可以不是点击事件,
也可以执行第一次。
官方一点说就是:
防抖,只要函数在一段时间内持续被调用,就不会真的执行这个函数。
该函数将在它停止被调用 wait(wait的值我们可以自己制定) 毫秒后被执行。
如果是立即执行,则函数只在第一次调用之后执行一次。
当然,对于概念性的东西我也说不太好,大家可以多看看别人的解释。
只在最后执行一次的防抖
立即执行版本的防抖
纯享版防抖代码
function debounce(func, wait, immediate) {
var timeout;
return function() {
// 定义变量、定义函数
var context = this, args = arguments;
var callNow = immediate && !timeout;
// 执行函数
clearTimeout(timeout);
timeout = setTimeout(function() {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (callNow) func.apply(context, args);
};
};
function onMouseMove(){
console.log('111');
}
var debouncedMouseMove = debounce(onMouseMove, 1000);
window.addEventListener('mousemove', debouncedMouseMove);
史无前例详细注释解释版本
/*
func:需要被防抖的函数
可以是一个点击事件,也可以是连续请求事件
wait:间隔的时间
函数两次连续触发的间隔大于这个间隔时间,函数才会被执行
immediate:是否立即执行
如果是true,那么在连续触发函数中,执行第一次和最后一次
如果是false,那么只执行最后一次
*/
function debounce(func, wait, immediate) {
/*
声明一个名为 `timeout` 的变量,我们稍后将使用它来存储 `setTimeout` 函数返回的timeoutID。
setTimeout 的返回值`timeoutID`是一个正整数,表示定时器的编号。
这个值可以传递给`clearTimeout()`来取消该定时器。
在 setTimeout 的第二个参数中传递的指定毫秒数满足之前 调用 `clearTimeout` 时才会阻止 setTimeout 第一个参数里的函数被执行。
*/
var timeout;
/*
通过闭包过程返回一个匿名函数,在这个函数里面可以调用 `debounce` 方法的 `func` 参数。
*/
return function() {
/*
保存 上下文 和 参数
定义不定义都没什么大碍,可能这里只是为了方便看this到底指向谁
*/
var context = this,
args = arguments;
// 现在应该调用该函数吗? 如果 immediate 为 true 且 !timeout,则答案是:是
/*
什么情况下会 `!timeout === true` 呢 即`!!timeout == false`?
当定时器id为空的时候,在连续调用函数中的第一次的时候,定时器为空
callNow 是为了控制第一次是否立即执行
callNow的值:
true:我们传参的时候,`immediate` 又是 `true`;
在定义 `callNow` 的时候,如果是第一次调用函数,那么 timeout 是undefined,!timeout 就是 true。
false:immediate 传参为 false;
或者 在连续调用函数中,不是第一次调用,timeout 保存有只
*/
var callNow = immediate && !timeout;
/*
只要我们的 `debounce` 方法绑定的事件在 `wait` 周期内仍在触发,
就从 JavaScript 的执行队列中删除 timeoutID(即令`timeout = null`)。
这可以防止调用 `setTimeout` 函数中传递的函数。
请记住,`debounce` 方法旨在用于快速触发的事件,即:调整窗口大小或滚动或者连续点击事件。
事件第一次触发的时候,`timeout` 变量已被声明,但没有分配任何值 - 此时 `timeout === undefined`。
因此,不会从 JavaScript 的执行队列中删除任何内容,因为队列中没有放置任何内容 - 没有任何内容需要清除。
如果,`timeout` 变量保存有 `setTimeout`函数返回的 timeoutID。
只要在 `wait` 时间内连续触发这个函数,
`timeout` 就会被清除,从而导致在 `setTimeout` 函数中传递的函数从执行队列中删除。
一旦两次连续触发的时间间隔大于 `wait`,`setTimeout`函数中传递的函数就会执行。
*/
clearTimeout(timeout);
// 设置新的计时器
timeout = setTimeout(function() {
/*
删除定时器ID。
注意:通过 setTimeout 执行 later 时,
`setTimeout` 函数将返回一个 timeoutID 给 `timeout` 变量。
通过将 `null` 分配给 `timeout` 来删除该 timeoutID。
我们可以看到后面调用later的地方是这样调用的:
`timeout = setTimeout(later, wait);`
当两次连续调用在wait时间间隔之内,那么 变量`timeout`必然有值,在later函数里面重置`timeout`的值就是对时间间隔重新计时。
*/
timeout = null;
/*
如果`immediate` 是假的(false),那意思就是只执行最后一次,不是立即执行,
那我们就在定时器里面调用函数,这样定时器时间间隔到了,函数才会执行。
*/
if (!immediate) {
// 使用 apply 调用原始函数
// apply 允许您定义“this”对象以及参数(均在 setTimeout 之前捕获)
func.apply(context, args);
}
}, wait);
// 立即模式,没有等待计时器? 执行功能..
/* 如果是调用之后立即执行,那么就在这里立即执行函数。 */
/*
一般来说我们希望在连续事件的最后,满足wait时间之后执行函数,
但是如果希望在 函数连续反复触发 中的第一次 就执行,那就让`immediate = true`
如果`immediate = true`,
则 `callNow` 变量的值将仅在我们的 `debounce` 方法第一次发生事件后才为 `true`。
第一次触发事件后,`timeout` 变量将包含一个false。
因此,分配给 `callNow` 变量的表达式的结果是 `true`,并且在我们的 `debounce` 方法的 `func` 参数中传递的函数在下面的代码行中执行。
每次我们的 `debounce` 方法在 `wait` 周期内触发的事件,
`timeout` 变量保存在前一个事件触发时分配给它的 `setTimout` 函数返回的 timeoutID ,并且`debounce` 方法被执行。
这意味着对于 `wait` 期间内的所有后续事件,`timeout` 变量保持一个真值,
并且分配给 `callNow` 变量的表达式的结果是 `false`。
因此,我们的 `debounce` 方法的 `func` 参数中传递的函数将不会被执行。
最后,当 `wait` 周期满足并且 `setTimeout` 函数中传递的 `later` 函数执行时,
结果是它只是将 `null` 分配给 `timeout` 变量。
在我们的 `debounce` 方法中传递的 `func` 参数将不会被执行,
因为 `later` 函数中的 `if` 条件失败。
*/
if (callNow) func.apply(context, args);
}
}
/////////////////////////////////
// DEMO:
function onMouseMove(){
console.log('111');
}
// 定义防抖函数
var debouncedMouseMove = debounce(onMouseMove, 50);
// 在每次鼠标移动时调用 debounced 函数
window.addEventListener('mousemove', debouncedMouseMove);
节流 Throttle
什么是节流
时间戳版本写节流【立即执行】
纯享版
function throttle(func, timeFrame) {
var lastTime = 0;
return function (...args) {
var now = new Date();
if (now - lastTime >= timeFrame) {
func(...args);
lastTime = now;
}
};
}
function onMouseMove(){
console.log('111');
}
var throttleMouseMove = throttle(onMouseMove, 1000);
window.addEventListener('mousemove', throttleMouseMove);
注释版本
function throttle(func, timeFrame) {
// 定义一个时间,代表最后一次(上一次)触发函数的时间
var lastTime = 0;
return function (...args) {
// 拿到这一次触发函数的时间
var now = new Date();
// 如果上一次的时间和这一次的时间间隔 大于或者等于 我们所定义的时间间隔
// 那么就执行函数
// 否则就什么也不做
if (now - lastTime >= timeFrame) {
func(...args);
// 更新最后一次触发时间 为 这一次的 触发时间
lastTime = now;
}
};
}
定时器版本写节流【wait时间后再执行(非立即执行】
纯享版
function throttle (fn, delay) {
var timeout;
return args => {
if (timeout) return
timeout = setTimeout(() => {
fn.call(this, args)
clearTimeout(timeout)
timeout = null
}, delay)
}
}
function onMouseMove(){
console.log('111');
}
var throttleMouseMove = throttle(onMouseMove, 1000);
window.addEventListener('mousemove', throttleMouseMove);
注释版本
function throttle (fn, delay) {
return args => {
// 如果 timeout 保存有定时器id,说明不是在这个时间间隔内触发过函数,那么不执行直接返回
if (timeout) return
// 如果 timeout 没有保存定时器id,说明在这一段时间里面,这是第一次触发函数的,那么进入 定时器, 同时这个定时器的id 保存在 timeout
timeout = setTimeout(() => {
/*
执行函数fn
*/
fn.call(this, args)
// 清除定时器。
/*
在事件循环中,定时器里面的函数放在任务队列,
等到调用栈为空的时候才会执行settimeout里面的函数,
如果执行调用栈里面的东西的时间大于settimeout里面的时间间隔,
就会导致有多个定时器函数积压在任务队列,
等到调用栈为空,执行任务队列的时候,
就会执行多个积压在任务队列里面的定时器,这样效果很差。
所以我们把前面的定时器都清除了,只执行最后一个定时器
*/
clearTimeout(timeout)
// 清除定时器id----其实并不知道为什么清楚定时器id,是为了垃圾回收吗?
timeout = null
}, delay)
}
}
参考文章
Can someone explain the "debounce" function in Javascript