防抖和節流到底是什麼?
防抖和節流屬於效能優化的知識,它可以有效的降低高頻事件觸發時,你定義的方法的執行次數。
還是沒有感覺???那麼,來看下面的場景:
- 使用者在搜尋框輸入關鍵詞(只有當他輸入完成時我們才去向伺服器傳送請求,然後給出搜尋結果)
- 自動儲存使用者填寫的表單資料
上面的場景都對應著一個高頻事件,即input或者textarea的onKeyUp事件,我們一般是在使用者觸發這個事件後去向伺服器傳送請求(這樣做的好處是不需要使用者去點選搜尋按鈕,有一種實時查詢的感覺)。
那麼問題來了,當使用者輸入一個要查詢的關鍵詞,可能需要多次按下和抬起鍵盤的按鍵,難道每次onKeyUp的時候我們都要去請求伺服器嗎?顯然不夠優雅(因為如果有大量使用者同時搜尋,伺服器壓力會很大)。而 防抖(debounce) 正是要解決類似這樣的問題。
在瀏覽器中我們經常會遇到類似的事件(如瀏覽器scroll,resize,mousemove...)接下來,我們使用 自動儲存 的場景來說明一下在 JavaScript 中如何實現防抖。
場景描述:使用者在textarea中輸入文字後,要為他自動儲存到伺服器(可以理解為儲存為草稿)這時我們需要做的是 優化 請求伺服器的次數,需要用到防抖函式。
防抖函式
先來看一個常見的錯誤寫法,注意!!!百度中搜出的很多結果都是這個樣子,用了之後就會發現,你的函式還是會立刻執行,並不會延時執行。
function debounce(fn, delay) {
let timer = null
return function (args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(fn.call(this, args), delay)
}
}
問題出在 timer = setTimeout(fn.call(this, args), delay) 這一行。
修改成下面的樣子,就可以按設定的delay延時執行了:
function debounce(fn, delay) {
let timer = null
return function (args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function() {
fn.call(this, args)
}, delay)
}
}
// 或者
function debounce(fn, delay) {
let timer = null
return function (args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function() {
fn(args)
}, delay)
}
}
不要小看這小小的區別,它可能會浪費你大量的時間,而且讓你對防抖產生懷疑...
下面貼一個完整的例子,還有 防抖線上演示地址,方便你更好的理解這個場景。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖和節流</title>
<style>
.de_wrapper {
padding: 20px;
display: flex;
}
.col {
width: 40%;
}
.log {
height: 300px;
overflow-y: scroll;
background-color: #fff;
}
</style>
</head>
<body>
<div class="de_wrapper">
<div class="col">
<h3>未使用防抖(每次按鍵抬起都會觸發儲存)</h3>
<textarea name="" id="1" cols="30" rows="10" onKeyUp="printLog(event)"></textarea>
<div id="log" class="log"></div>
</div>
<div class="col">
<h3>使用防抖(停止輸入2秒後儲存)</h3>
<!-- <textarea name="" id="2" cols="30" rows="10"></textarea> -->
<textarea name="" id="2" cols="30" rows="10" onKeyUp="debounceLog(event)"></textarea>
<div id="log1" class="log"></div>
</div>
</div>
<script>
let log = null
let log1 = null
window.onload = function() {
log = document.getElementById('log')
log1 = document.getElementById('log1')
// 寫法1
// document.getElementById('2').addEventListener('keyup', function(e) {
// debounceLog(e)
// })
// 寫法2
// document.getElementById('2').addEventListener('keyup', debounceLog)
// 寫法3
// document.getElementById('2').addEventListener('keyup', debounce(printDebounceLog, 2000))
}
function printLog(e) {
log.innerText += `keyup 事件觸發【請求伺服器儲存資料...】: ${e.target.value}\n`
}
function printDebounceLog(e) {
log1.innerText += `keyup 事件觸發【請求伺服器儲存資料...】: ${e.target.value}\n`
}
let debounceLog = debounce(printDebounceLog, 2000)
function debounce(fn, delay) {
let timer = null
return function (args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function() {
fn(args)
}, delay)
}
}
</script>
</body>
</html>
節流函式
節流函式(throttle)與防抖函式的區別:函式節流無論事件觸發多麼頻繁,在一定時間內只會執行一次回撥;而函式防抖是在高頻事件的最後一次觸發回撥。
節流函式使用場景:一個很形象的例子就是mousedown發射子彈,每秒只能發出一顆子彈,線上演示地址。
function throttle(fn, limit) {
let lastTime
return function(args) {
if (!lastTime) {
fn.apply(this. args)
lastTime = Date.now()
} else {
if ((Date.now() - lastTime) >= limit) {
fn.apply(this. args)
lastTime = Date.now()
}
}
}
}
// 頁面結構
<div class="de_wrapper">
<div class="col">
<h3>未使用節流(點選按鈕可以瘋狂發射子彈)</h3>
<div class="sky"></div>
<button class="fire_btn">發射</button>
</div>
<div class="col">
<h3>使用節流(發射子彈速度會被限制)</h3>
<div class="sky"></div>
<button class="fire_btn">發射</button>
</div>
</div>
<script>
let sky = null
let sky1 = null
let btn = null
let btn1 = null
window.onload = function() {
sky = document.querySelectorAll('.sky')[0]
sky1 = document.querySelectorAll('.sky')[1]
btn = document.querySelectorAll('.fire_btn')[0]
btn1 = document.querySelectorAll('.fire_btn')[1]
btn.addEventListener('click', fire)
btn1.addEventListener('click', throttle(t_fire, 1000))
}
function fire() {
const b = document.createElement('span')
b.classList.add('bullet')
sky.appendChild(b)
setTimeout(() => {
sky.removeChild(b)
}, 1000)
}
function t_fire() {
const b = document.createElement('span')
b.classList.add('bullet')
sky1.appendChild(b)
setTimeout(() => {
sky1.removeChild(b)
}, 1000)
}
function throttle(fn, limit) {
let lastTime
return function(args) {
if (!lastTime) {
fn.apply(this. args)
lastTime = Date.now()
} else {
if ((Date.now() - lastTime) >= limit) {
fn.apply(this. args)
lastTime = Date.now()
}
}
}
}
總結
- 函式防抖:將多次操作合併為一次操作進行,原理是維護一個計時器,後設定的定時器會取代之前的定時器,如果高頻事件一直在觸發那麼回撥函式一直不會執行。
- 函式節流:使得一定時間內只觸發一次函式。原理是通過判斷是否滿足限制時間,滿足則執行。