歪式絮叨:本来打算只用一篇文章总结下防抖和节流的,可是写着写着发现挺有意思的。因此准备会分红 3-4 篇来写了,单独的防抖和节流的实现,而后在去分享一下知名库的源码。今天先跟歪马一块儿看看防抖的实现吧。其余内容敬请期待~!html
歪老师:“马同窗,防抖和节流你知道吗?起来讲一下。”前端
马同窗:“老师,我不知道呀,都没据说过。”浏览器
歪老师:“好吧,那今天咱们就先来说讲防抖吧。先从基本的概念以及使用场景提及”。服务器
防抖 debounce
和节流 throttle
的概念并非 JS 所特有的。它们是在对函数持续调用时进行不一样控制的两个概念。今天咱们先介绍防抖。markdown
防抖是为了不用户无心间执行函数屡次。好比有些用户喜欢在点击的时候使用 double click 双击,或者就是手抖连续点了两下,这就可能会致使一些意想不到的问题。函数
经过防抖能够在事件触发必定时间后没有再次触发同一事件时,再去执行相关的处理函数。oop
就比如你去菜市场买菜,到某个小摊上开始挑菜,接连挑好一袋又一袋放在摊主面前,摊主并不会每一袋都给你结帐,而是会等着问你:“还要别的吗?”,等你确认完不要了,才会结帐。性能
你能够经过歪马写的这个 demo 查看常规无限制函数调用和防抖(节流)以后的可视化对比,完整 demo 地址以下: codesandbox.io/s/yibubupia…。优化
在具体实现以前,咱们先简单了解一下防抖和节流的做用以及在哪些业务中会用到。this
防抖和节流(这里先包含它吧)主要可以给咱们带来如下好处:
防抖的应用场景有如下几个方面:
window.resize
--> 避免 UI 渲染阻塞,浏览器卡顿歪老师:“概念咱们就介绍到这,下面咱们来看看该如何实现。”
歪老师:“防抖能够经过计时器来实现,经过
setTimeout
来指定必定时间后执行处理函数,若是在这以前事件再次触发,则清空计时器,从新计时。”
function debounce(fn, wait) {
let timerId = null;
return function(...args) {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
fn.call(this, args);
}, wait);
};
}
复制代码
歪老师:“上面就是比较基础的
debounce
功能的实现,同窗们都听懂了吧?马牛羊同窗:“听懂了。”
歪老师:“好,下面咱们来逐步拓展。马同窗,在这个基础上让你去实现首次触发时当即执行一次函数,你会怎么实现?”
马同窗:“老师,什么场景下才须要在首次就执行呢?”
歪老师:“😓 这个你别管,如今要求就是这样。你会怎么实现?”
马同窗:“哦,那让我想一想。我知道了,若是
timerId
为null
时,直接执行就好了。”
function debounce(fn, wait) {
let timerId = null;
return function(...args) {
- if (timerId) clearTimeout(timerId);
+ if (timerId) {
+ clearTimeout(timerId);
+ } else {
+ // 第一次直接调用函数
+ fn.call(this, args);
+ }
timerId = setTimeout(() => {
fn.call(this, args);
}, wait);
};
}
复制代码
歪老师:“很好,这样确实能实现首次触发当即执行。但若是在通过正常的延迟执行(debounced 执行),中间又间隔了一段时间,再次触发的时候,首次触发会执行吗?”
马同窗:“呃,我想一想。好像是不会执行了。由于
timerId
一直有上次的值。就像下图,中间应该有一次触发的。若是要实现这一功能的话,能够在每次延迟执行执行的时候将timerId
置为空。”
function debounce(fn, wait) {
let timerId = null;
return function(...args) {
if (timerId) {
clearTimeout(timerId);
} else {
fn.call(this, args);
}
timerId = setTimeout(() => {
fn.call(this, args);
+ timerId = null;
}, wait);
};
}
复制代码
歪老师:“不错,一点就通,这样确实能够了。可是这样的话,若是第一次延迟触发和后面的新的触发时间间隔小于咱们所设定的时间间隔。是否是也会触发一次?若是想保持触发间隔不小于 wait 事件间隔呢?”
![]()
马同窗:“呃(⊙o⊙)…不知道,老师我以为你故意刁难我。你咋不叫羊同窗他们”
歪老师:“哈哈,别这么说,老师是在锻炼你的思考能力。这里也能够借助相似上面的延时执行的思路。首次触发是由
timerId
是否为空决定的,要避免延迟执行以后的首次执行过早触发,只要将上一步的置空操做也延时就好了。以下所示:”
function debounce(fn, wait) {
let timerId = null;
return function(...args) {
if (timerId) {
clearTimeout(timerId);
} else {
fn.call(this, args);
}
timerId = setTimeout(() => {
fn.call(this, args);
+ setTimeout(() => {
timerId = null;
+ }, wait);
}, wait);
};
}
复制代码
歪老师:“那马同窗,你以为这样会有什么问题没?”
马同窗:“(思考片刻...)我以为没啥问题了。毕竟是老师你给出的方案”
歪老师:“不,这样仍是有问题的。”
马同窗:“老师,你这就是个连环套呀...一环扣一环。那还有什么问题呀?”
歪老师:“咱们刚才设置的延时置空定时器,并无 clear 的操做,因此在屡次连续触发事件时,取消的操做其实按照第一次触发的时间计算延时的,这就会致使首次执行在其后忽然触发,而后首次执行的提早又会致使正常延时执行函数出问题(不会清计时器了),致使其按照首次执行上一次的来执行。以下图所示:”
![]()
歪老师:“因此,这里咱们只要在清空
timerId
的时候,将延时置空也取消就好了。
function debounce(fn, wait) {
let timerId = null;
+ let leadingTimerId = null;
return function(...args) {
if (timerId) {
clearTimeout(timerId);
+ clearTimeout(leadingTimerId);
} else {
fn.call(this, true, args);
}
timerId = setTimeout(() => {
fn.call(this, false, args);
- setTimeout(() => {
+ leadingTimerId = setTimeout(() => {
timerId = null;
}, wait);
}, wait);
};
}
复制代码
歪老师:“如今马同窗,你以为还有问题吗?”
马同窗:“老师,我以为应该还有问题。”
歪老师:“那有什么问题呢?”
马同窗:“额,老师,我就猜的,其实不知道...您再给说说”
歪老师:“就知道耍小聪明,不过确实仍是存在问题的。先看下图。”
![]()
歪老师:“若是恰好只触发了一次事件(能够将 demo 里的
mousemove
换为click
再试),会执行首次触发,可是后续没有其余触发,也会再触发一次延时触发。若是想避免这种问题,也就是若是首次触发以后没有再触发,不进行延时触发,应该怎么作?”歪老师:“马同窗不用躲了,此次不问你了。老师直接说。”
function debounce(fn, wait) {
let timerId = null;
let leadingTimerId = null;
return function(...args) {
if (timerId) {
clearTimeout(timerId);
clearTimeout(leadingTimerId);
timerId = setTimeout(() => {
fn.call(this, false, args);
leadingTimerId = setTimeout(() => {
timerId = null;
}, wait);
}, wait);
} else {
fn.call(this, true, args);
// 为了解决只触发一次,会同时触发首次触发和延时触发的问题引入的特殊值
timerId = -1;
}
};
}
复制代码
歪老师:“好了,今天就讲这么多,你们记一下课后做业。这样还有没有问题呢?你们能够留言讨论哟”。
我知道这篇文章彷佛读起来让人晕晕乎乎的,而且你会发现,这和你想象中的防抖的实现彷佛并不同。可是这又怎样呢?多数时候,咱们都是根据具体的使用场景去实现咱们须要的功能,因此重要的是要懂得如何去实现,同时也要随机应变。
而且最后你也知道了如何去实现 debounce,而且知道可能会有哪些坑了不是吗?
相关连接已点亮到技能树,欢迎查收。
文档连接:mubu.com/doc/4VGWywo… 密码:歪码行空
若是你喜欢,欢迎扫码关注个人公众号,我会按期陪读,并分享一些其余的前端知识哟。