一、節流
在JavaScript中,節流(throttle)是一種常用的效能最佳化技術,用於限制某個函式在一定時間內的執行頻率。具體來說,節流函式允許你在一段時間內只執行一次回撥函式,即使在這段時間內觸發了多次事件。這有助於防止因為頻繁觸發事件而導致的效能問題。
節流的實現原理是,在事件被觸發後,一個定時器會被設定。如果在定時器完成之前,相同的事件再次被觸發,那麼這次觸發不做任何處理,直到定時器任務完成後,清空定時器,才能對以後新的觸發事件做出響應。這樣,在一定的時間間隔內,只會對觸發事件做一次處理。
應用場景:
- 滾動事件處理:在網頁或應用中,滾動事件可能會非常頻繁地觸發,如果不加以控制,可能會導致效能問題。透過使用節流,可以限制滾動事件處理函式的執行頻率,提高頁面的響應速度和流暢度。
- 網路請求:對於需要連續傳送請求的場景,如滾動載入資料或自動完成搜尋,過多的請求不僅會增加伺服器壓力,還可能導致使用者體驗下降。透過節流,可以減少請求的次數,降低伺服器壓力,同時保證資料的及時載入。
- 動畫效果:在動畫過程中,如果過度頻繁地重新整理和渲染,可能會造成閃爍和卡頓現象。透過節流,可以控制動畫的渲染頻率,提高動畫的流暢性和使用者體驗。
二、前置準備
-
準備一個html檔案和一個throttle.js檔案,throttle.js檔案用來編寫節流函式
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>節流</title> </head> <body> <div class="throttle">觸發節流事件</div> <script src="./throttle.js"></script> </body> </html>
// throttle.js const throttle = () => {};
-
給div繫結點選事件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>節流</title> </head> <body> <div class="throttle">觸發節流事件</div> <script src="./throttle.js"></script> <script> const clickEvent = function (e) { console.log("點選事件觸發", e, this) } document.querySelector(".throttle").addEventListener("click", clickEvent) </script> </body> </html>
-
將clickEvent方法傳遞給throttle進行處理
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>節流</title> </head> <body> <div class="throttle">觸發節流事件</div> <script src="./throttle.js"></script> <script> const clickEvent = function (e) { console.log("點選事件觸發", e, this) console.log("執行時間", new Date().getSeconds()) } const throttleClickEvent = throttle(clickEvent, 2000) document.querySelector(".throttle").addEventListener("click", throttleClickEvent) </script> </body> </html>
// throttle.js const throttle = (fun, time) => { return fun; };
在上面修改了一下clickEvent,用來列印時間。目前的效果和一開始沒什麼區別,下面就開始寫節流函式
三、基礎節流實現
-
第一次觸發事件,設定一個定時器,只要這個定時器存在,就不再對觸發事件做出響應。直到定時器任務完成,清空定時器
// throttle.js const throttle = (fun, time) => { let timer; return function () { if (!timer) { timer = setTimeout(() => { fun(); timer && clearTimeout(timer); timer = null; }, time); } }; };
現在頻繁觸發點選事件,但會間隔2秒觸發一次事件處理函式。所以基本實現了節流
-
修改this指向
// throttle.js const throttle = (fun, time) => { let timer; return function () { if (!timer) { timer = setTimeout(() => { fun.apply(this); timer && clearTimeout(timer); timer = null; }, time); } }; };
-
獲取引數
// throttle.js const throttle = (fun, time) => { let timer; return function (...args) { if (!timer) { timer = setTimeout(() => { fun.apply(this, args); timer && clearTimeout(timer); timer = null; }, time); } }; };
上面就實現了一個最基本的節流函式
四、立即執行
// throttle.js
const throttle = (fun, time, immediately = false) => {
let timer;
return function (...args) {
if (!timer) {
// 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發
if (immediately) {
fun.apply(this, args);
timer = setTimeout(() => {
timer && clearTimeout(timer);
timer = null;
}, time);
} else {
timer = setTimeout(() => {
fun.apply(this, args);
timer && clearTimeout(timer);
timer = null;
}, time);
}
}
};
};
五、尾部執行控制
-
首先我們修改一下html檔案,方便測試
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>節流</title> </head> <body> <div class="throttle">觸發節流事件</div> <input type="text" class="throttle-input"> <script src="./throttle.js"></script> <script> const clickEvent = function (e) { console.log("點選事件觸發", e, this) console.log("執行時間", new Date().getSeconds()) } const inputEvent = function (e) { console.log("表單資料", e, this) console.log("執行時間", new Date().getSeconds()) } const throttleClickEvent = throttle(clickEvent, 2000, true) document.querySelector(".throttle").addEventListener("click", throttleClickEvent) document.querySelector(".throttle-input").addEventListener("input", inputEvent) </script> </body> </html>
目前沒有做節流處理,每次輸入都會觸發
-
使用節流函式控制觸發頻率
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>節流</title> <style> input { width: 100%; } </style> </head> <body> <div class="throttle">觸發節流事件</div> <input type="text" class="throttle-input"> <script src="./throttle.js"></script> <script> const clickEvent = function (e) { console.log("點選事件觸發", e, this) console.log("執行時間", new Date().getSeconds()) } const inputEvent = function (e) { console.log("表單資料", this.value) console.log("執行時間", new Date().getSeconds()) } const throttleClickEvent = throttle(clickEvent, 2000, true) const throttleInputEvent = throttle(inputEvent, 2000, true) document.querySelector(".throttle").addEventListener("click", throttleClickEvent) document.querySelector(".throttle-input").addEventListener("input", throttleInputEvent) </script> </body> </html>
此時,第一次被觸發了,然後兩秒後,觸發了第二次。但是最後一次觸發事件卻沒有得到響應
-
修改節流函式
// throttle.js const throttle = (fun, time, immediately = false, tail = false) => { let timer; let tailTimer; return function (...args) { if (!timer) { // 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發 if (immediately) { fun.apply(this, args); timer = setTimeout(() => { timer && clearTimeout(timer); timer = null; }, time); } else { timer = setTimeout(() => { fun.apply(this, args); timer && clearTimeout(timer); timer = null; }, time); } } else if (tail) { // 如果當前已經觸發了響應事件,並且需要對最後一次觸發做出回應,則將後面觸發呼叫的處理函式加入定時器 // 每一次有新的觸發事件,則將上一次的定時器給清除,保證始終是最新的觸發事件被加入 tailTimer && clearTimeout(tailTimer); tailTimer = setTimeout(() => { fun.apply(this, args); }, time); } }; };
此時最後一次就會觸發了。我們再來試一試不立即執行的情況
const throttleInputEvent = throttle(inputEvent, 2000, false)
透過控制檯發現,最後一次輸入的數字被列印了兩次。為什麼?
首先,最後列印的肯定是放在
tailTimer
裡面的方法,它在53秒執行,是在51秒的時候觸發的。最後一次執行節流函式是52秒,它是在50秒的時候觸發的。之所以這兩個方法列印了同樣的值,是因為最後一次觸發點選事件是在51秒,兩個定時器裡面的方法,都是在51秒後執行的,等它們執行的時候,都會拿到最後那個最新的值。那立即執行為什麼沒有這個問題呢?因為立即執行列印的是當時獲取到值。簡單點說:就是延遲執行,在52秒和53秒都會列印51秒最後一次觸發時候的值。立即執行的話,50秒的時候會最後一次使用timer,此時,直接列印出50秒時候的值,在接下來的2秒之內,timer不會再執行,在這2秒內,觸發的最後一次事件會放進
tailTimer
裡面。所以,對於延遲執行的情況,其實沒有必要將最後一次加入到
tailTimer
,因為timer會獲取到最後一次的值。// throttle.js const throttle = (fun, time, immediately = false, tail = true) => { let timer; let tailTimer; return function (...args) { if (!timer) { // 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發 if (immediately) { fun.apply(this, args); timer = setTimeout(() => { timer && clearTimeout(timer); timer = null; }, time); } else { timer = setTimeout(() => { fun.apply(this, args); timer && clearTimeout(timer); timer = null; }, time); } } else if (tail && immediately) { // console.log("尾部定時器當前記錄引數:", args[0].srcElement.value); // 如果當前已經觸發了響應事件,並且需要對最後一次觸發做出回應,則將後面觸發呼叫的處理函式加入定時器 // 每一次有新的觸發事件,則將上一次的定時器給清除,保證始終是最新的觸發事件被加入 tailTimer && clearTimeout(tailTimer); tailTimer = setTimeout(() => { fun.apply(this, args); }, time); } }; };
這下列印結果就沒問題了。
再來測試一下立即執行的情況
const throttleInputEvent = throttle(inputEvent, 2000, true)
立即執行的情況,就會透過
tailTimer
來列印最後的結果了。
六、完整程式碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>節流</title>
<style>
input {
width: 100%;
}
</style>
</head>
<body>
<div class="throttle">觸發節流事件</div>
<input type="text" class="throttle-input">
<script src="./throttle.js"></script>
<script>
const clickEvent = function (e) {
console.log("點選事件觸發", e, this)
console.log("執行時間", new Date().getSeconds())
}
const inputEvent = function (e) {
const value = this.value
console.log("表單資料", value)
console.log("執行時間", new Date().getSeconds())
}
const throttleClickEvent = throttle(clickEvent, 2000, true)
const throttleInputEvent = throttle(inputEvent, 2000, true)
document.querySelector(".throttle").addEventListener("click", throttleClickEvent)
document.querySelector(".throttle-input").addEventListener("input", throttleInputEvent)
</script>
</body>
</html>
// throttle.js
const throttle = (fun, time, immediately = false, tail = true) => {
let timer;
let tailTimer;
return function (...args) {
if (!timer) {
// 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發
if (immediately) {
fun.apply(this, args);
timer = setTimeout(() => {
timer && clearTimeout(timer);
timer = null;
}, time);
} else {
timer = setTimeout(() => {
fun.apply(this, args);
timer && clearTimeout(timer);
timer = null;
}, time);
}
} else if (tail && immediately) {
// console.log("尾部定時器當前記錄引數:", args[0].srcElement.value);
// 如果當前已經觸發了響應事件,並且需要對最後一次觸發做出回應,則將後面觸發呼叫的處理函式加入定時器
// 每一次有新的觸發事件,則將上一次的定時器給清除,保證始終是最新的觸發事件被加入
tailTimer && clearTimeout(tailTimer);
tailTimer = setTimeout(() => {
fun.apply(this, args);
}, time);
}
};
};
更多的最佳化,比如:取消、獲取返回值可以參考防抖的處理。