一. 概念
防抖:顧名思義,防止抖動,指連續(xù)調(diào)用時,只有最后一次生效。
節(jié)流:節(jié)省流量,固定時間間隔觸發(fā)一次,減小頻率。
二. 舉例:
- 搜索功能,在用戶輸入結(jié)束以后才開始發(fā)送搜索請求,可以使用函數(shù)防抖來實現(xiàn);
- 驗證賬號密碼功能,在用戶輸入密碼結(jié)束以后才開始發(fā)送驗證請求,可以使用函數(shù)防抖來實現(xiàn);
簡單來說:某件事你并不想它太過頻繁觸發(fā),那么設(shè)置一個定時器,每次進來的時候都清除原本的定時器,然后重新開始計時。
- 你不想頻繁為你女票買單,于是約好每月清空購物車1次
- 防止過快拖動滾動條,導致 JS 跟不上拖動頻率,通過節(jié)流限制每秒觸發(fā)監(jiān)聽的次數(shù)(固定時間固定頻率)
簡單來說:年輕人要保持一日三餐,規(guī)律作息,那就通過節(jié)流來限制。
三. 手寫防抖、節(jié)流思路
1. 首先函數(shù)需要哪些參數(shù)?
- 都需要傳遞一個函數(shù)進去,返回一個防抖、節(jié)流后的新函數(shù),因此第一個參數(shù)是需要處理的原函數(shù)fn
- 光有原函數(shù)還不行,因為我們需要規(guī)定一個時間間隔。對于防抖而言:時間間隔用于表明最后一次調(diào)用時,隔多久沒有再次觸發(fā),我們才真正調(diào)用函數(shù)。譬如,放鹽時,手一直抖動,直到連續(xù)5秒沒有抖動了,那么才放鹽。這個5秒就是時間間隔。對于節(jié)流而言:時間間隔用于標志,每隔多久我們真正觸發(fā)函數(shù)調(diào)用。就像是,一天只吃3頓,那么只有每頓間隔4個小時,我們才能吃飯。在沒到4個小時時,不允許吃東西。所以,第二個參數(shù)是時間間隔,可寫為wait
現(xiàn)在,我們可以寫出防抖和節(jié)流函數(shù)的基本結(jié)構(gòu)和參數(shù):
// 防抖
function debounce(fn, wait=500) {
return function () {
}
}
// 節(jié)流
function throttled(fn, wait=500) {
return function () {
}
}
2. 原函數(shù)的執(zhí)行條件是什么?
對于防抖,當連續(xù)觸發(fā)時,我們的目標是,不抖動時才調(diào)用。這也就是說,不滿足時間間隔時,后一次觸發(fā)會覆蓋掉前一次觸發(fā)。當滿足時間間隔時,才會真正執(zhí)行。
滿足時間間隔,指的是某一次觸發(fā)之后,時間間隔范圍內(nèi),沒有下一次觸發(fā),這樣不產(chǎn)生覆蓋,因此會真正執(zhí)行。由于有時間間隔的判斷,因此即便真正執(zhí)行,也需要延遲到時間間隔之后。這顯然是定時器的概念。對于節(jié)流,當連續(xù)觸發(fā)時,我們的目標是,每隔時間間隔時調(diào)用一次。這也就是說,我們每次設(shè)定固定時間的一個定時器,當定時器存在就什么都不做,當定時器不存在,就設(shè)定下一次的定時器。
綜上,我們可以大致寫出函數(shù)的執(zhí)行時機,結(jié)構(gòu)如下:
// 防抖
function debounce(func, wait=500) {
let timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(() => {
// 原函數(shù)執(zhí)行部分
}, wait);
}
}
// 節(jié)流
function throttle(fn, wait = 500) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(() => {
// 原函數(shù)執(zhí)行部分
}, wait);
}
}
}
3. 原函數(shù)執(zhí)行
我們只需要直接調(diào)用fn,即fn()即可。但這樣不夠完善,需要注意兩點:
- 其一是this指向,新函數(shù)中調(diào)用原函數(shù),兩者的this指向應該一致;
- 其二是參數(shù),新函數(shù)中調(diào)用原函數(shù),兩者接收的參數(shù)應該一致。
故而,我們應該努力讓新舊函數(shù)在這兩方面保持一致。
// 防抖
function debounce(func, wait=500) {
let timeout;
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
fn.apply(this, args)
}, wait);
}
}
// 節(jié)流
function throttle(fn, wait=500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
}, wait);
}
}
}
注1:setTimeout回調(diào)中使用arguments:
若不是箭頭函數(shù),接收的是回調(diào)的實際參數(shù),而不是新函數(shù)的實際參數(shù)。
若是箭頭函數(shù),由于箭頭函數(shù)沒有arguments對象,接收的就是新函數(shù)的實際參數(shù)。
注2:setTimeout回調(diào)中使用this:
若是箭頭函數(shù),由于箭頭函數(shù)沒有this,因此回調(diào)中的this是外層的this,
若不是箭頭函數(shù),回調(diào)中的this指向window。
4. 計時器何時重置?
在上面的代碼中,防抖和節(jié)流何時進行計時器的重置呢?
- 對于防抖:每次調(diào)用都會清除掉上一次的定時器,因此每次觸發(fā)都會重置,可以看到clearTimeout寫在定時器之前。
- 對于節(jié)流:一開始沒有定時器,觸發(fā)事件后檢測定時器是否存在,如果不存在則開啟定時器執(zhí)行函數(shù),執(zhí)行完函數(shù)后,在把定時器設(shè)為無,準備下一次開啟定時器執(zhí)行函數(shù)
故針對節(jié)流函數(shù),增加清除操作,即增加timer=null重置語句:
// 防抖
function debounce(func, wait=500) {
let timeout;
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait);
}
}
// 節(jié)流
function throttle(fn, wait = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
},wait);
}
}
}
完整的防抖用例
<body>
<button id="debounce">點我防抖!</button>
<script>
// 防抖
function debounce(func, wait = 500) {
let timeout = null;
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait);
}
}
function sayDebounce() {
console.log(this);
console.log("防抖成功!");
}
window.onload = function () {
var myDebounce = document.getElementById("debounce");
myDebounce.addEventListener("click", debounce(sayDebounce));
}
</script>
</body>
完整的節(jié)流用例
<body>
<button id="throttle">點我節(jié)流!</button>
<script>
// 節(jié)流
function throttle(fn, wait = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, wait);
}
}
}
function sayThrottle() {
console.log("節(jié)流成功!");
}
window.onload = function () {
var myThrottle = document.getElementById("throttle");
myThrottle.addEventListener("click", throttle(sayThrottle));
}
</script>
</body>