大家好,又见面了,我是你们的朋友全栈君。
前端面试,总会被问到这类问题:你知道debounce是什么么?
你知道debounce什么时候用么?
来来来,能给我实现一个debounce么?
了解debounce以及实现方法,不仅会帮助我们面试,也是对我们技术的一次提升。废话不说,来不及了,我们一起学习debounce。
什么是debounce?什么时候使用debounce?
翻看Underscore的文档,它是这么描述debounce的:返回 function 函数的防反跳版本, 将延迟函数的执行(真正的执行)在函数最后一次调用时刻的 wait 毫秒之后. 对于必须在一些输入(多是一些用户操作)停止到达之后执行的行为有帮助。 例如: 渲染一个Markdown格式的评论预览, 当窗口停止改变大小之后重新计算布局, 等等.
这段话是什么意思呢?我们看一个例子:
HTML结构:
请滚动页面
滚动数目
0
CSS样式:
h1 {
height: 2000px;
}
.countWrapper {
position: fixed;
top: 100px;
left: 100px;
}
对应的JS代码:
var count = 0;
var updateCount = function(ev) {
console.log(ev)
count++;
document.getElementById(“count”).innerHTML = count;
}
window.onscroll = updateCount;
可以看到,随着鼠标滚动,updateCount这个事件不停地被触发。我们的例子当中,updateCount所做的事情比较简单,函数执行也比较快,但是,如果在复杂的系统当中,如Underscore文档中提到的,渲染一个Markdown格式的评论预览,如果我们每次都在window.onresize改变的时候重新计算布局,那么由于单位时间内,我们可能触发几十次resize事件,那么我们就要重新计算几十次布局,这给了系统很大的 压力,可能造成卡顿,而且,很明显,我们最关心的是窗口停止resize时候的评论预览,中间那么多次渲染是没有必要的。
怎么解决这个问题呢?
那就是,是延迟updateCount的执行,即只有在onscroll这个函数停止调用wait毫秒时间之后,再去执行updateCount。
基本实现
debounce本质上,是一个定时器setTimeout,在wait毫秒时间之后,执行传入的函数:
function debounce(func, wait, immediate) {
var timeout;
var debounced = function() {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(func, wait);
}
return debounced;
}
调用方法:
window.onscroll = debounce(updateCount, 1000);
window.onscroll在每次滚动的时候,都会被调用debounce(updateCount, 1000),在debounce函数内部,如果之前已经存在定时器,那么就清除已有的定时器,重新开始计时。这样,如果滚动过于频繁地被触发,则之前滚动所开启的定时器都会被紧接而来的下一个滚动事件清除,只有最后一个滚动事件触发的定时器才会被保存,最终在1000ms之后执行updateCount函数。
完善程序
看过我之前文章的朋友们,应该会想到,一般模拟某个函数的时候,都需要处理一些事情:比如函数内部this的指向、参数的传递、原型链是否正确、是否能够正确处理返回值,等等。
我们先看this的指向。
如果直接调用window.onscroll = updateCount;的时候,updateCount内部的this是隐式绑定(如果不清楚什么是隐式绑定,请阅读我的《前端面试题——十分钟搞懂this》),那么this指向的是调用onscroll的元素,我的例子中是window,当然,你也可以在其他元素上调用onscroll,如:target.onscroll,那么这时的this就指向target。但是我们的模拟debounce当中,updateCount是在1000ms之后调用的,这个时候的调用环境是?恩?global?
怎么办呢?
我们把debounce调用时候的this保存给context,然后使用apply调用内部的func,并指定context为func的this:
function debounce(func, wait, immediate) {
var timeout;
var debounced = function() {
var context = this;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context);
}, wait);
}
return debounced;
}
然后再看看参数的传递。
这个就很容易了,把参数保存为args,然后传入apply作为第二个参数。直接看代码:
function debounce(func, wait, immediate) {
var timeout;
var debounced = function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
}
return debounced;
}
最后处理返回值。
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
result = func.apply(context, args);
}, wait);
return result;
}
return debounced;
}
由于result是在setTimeout内部更新的,所以,其实return result会返回undefined,因此这个result并不是func函数执行之后真正的返回值。不过result接下来会用到,所以我们先放在那里吧。
立即执行
到这里我们的debounce就写完了么?对,已经写完了。不过underscore中的debounce还有第三个参数:immediate。这个参数是做什么用的呢?传参 immediate 为 true, debounce会在 wait 时间间隔的开始调用这个函数 。(注:并且在 wait 的时间之内,不会再次调用。)在类似不小心点了提交按钮两下而提交了两次的情况下很有用。
把true传递给immediate参数,会让debounce在wait时间开始计算之前就触发函数(也就是没有任何延时就触发函数),而不是过了wait时间才触发函数,而且在wait时间内也不会触发(相当于把func的执行锁住)。 如果不小心点了两次提交按钮,第二次提交就会不会执行。
那我们根据immediate的值来决定如何执行func。如果是immediate的情况下,我们立即执行func,并在wait时间内锁住func的执行,wait时间之后再触发,才会重新执行func,以此类推。
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = setTimeout(function() {
result = func.apply(context, args);
}, wait);
}
return result;
}
return debounced;
}
如果使用window.onscroll = debounce(updateCount, 1000, true);调用函数,那么会进入if (immediate) 这种情况。我们分为首次调用,调用后wait结束之前再次调用和调用后wait结束之后再次调用三种情况讨论。
首次调用:如果是第一次调用的话,timeout是undefined,那么 callNow就是true。而timeout会被更新为定时器返回的ID,然后调用result = func.apply(this, args);。另外,result值在这里能被正确更新,并正确返回。
调用后wait结束之前再次调用:这个时候,timeout还是等于定时器ID(clearTimeout并不会删掉timeout中保存的ID),那么callNow就是false,就不会执行func.apply(this, args);
调用后wait结束之后再次调用:由于定时器的wait时间已过,timeout被更新为null,那么callNow就是true,又可以执行func.apply(this, args);了,同时锁住timeout,以此类推。
程序可以简单改一下:
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
var later = function() {
timeout = null;
if (!immediate) result = func.apply(context, args);
};
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = setTimeout(later, wait);
}
return result;
}
return debounced;
}
使用later来保存func函数的调用情况。这么改的原因么?可能是为了和underscore本身的debounce长得像一点吧。
其实,上面的代码还可以优化一下,让代码更简洁:
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
var later = function() {
timeout = null;
if (!immediate) result = func.apply(context, args);
};
var callNow = immediate && !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
return result;
}
return debounced;
}
取消debounce
Underscore中的debounce还有一个功能:如果需要取消预定的 debounce ,可以在 debounce 函数上调用 .cancel()。
这个就把setTimeout事件清除掉,并把timeout直接更新为null就可以:
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
debounce我们就写完了,完整代码如下所示:
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
var later = function() {
timeout = null;
if (!immediate) result = func.apply(context, args);
};
var callNow = immediate && !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
return result;
}
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
希望看这篇文章,你不仅知道什么时候使用debounce,也可以写出自己的debounce,顺顺利利通过面试。祝大家前端学习一切顺利!
关注我的公众号:前端三剑客。分享前端面试与算法面试的分析与总结!
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/151567.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...