函數節流與函數防抖
題圖:Mike Labrum
什麼是函數節流與函數防抖
舉個栗子,我們知道目前的一種說法是當 1 秒內連續播放 24 張以上的圖片時,在人眼的視覺中就會形成一個連貫的動畫,所以在電影的播放(以前是,現在不知道)中基本是以每秒 24 張的速度播放的,為什麼不 100 張或更多是因為 24 張就可以滿足人類視覺需求的時候,100 張就會顯得很浪費資源。再舉個栗子,假設電梯一次只能載一人的話,10 個人要上樓的話電梯就得走 10 次,是一種浪費資源的行為;而實際生活正顯然不是這樣的,當電梯里有人準備上樓的時候如果外面又有人按電梯的話,電梯會再次打開直到滿載位置,從電梯的角度來說,這時一種節約資源的行為(相對於一次只能載一個人)。
- 函數節流: 指定時間間隔內只會執行一次任務;
- 函數防抖: 任務頻繁觸發的情況下,只有任務觸發的間隔超過指定間隔的時候,任務才會執行。
函數節流(throttle)
這裡以判斷頁面是否滾動到底部為例,普通的做法就是監聽 `window` 對象的 `scroll` 事件,然後再函數體中寫入判斷是否滾動到底部的邏輯:
$(window).on(scroll, function () { // 判斷是否滾動到底部的邏輯 let pageHeight = $(body).height(), scrollTop = $(window).scrollTop(), winHeight = $(window).height(), thresold = pageHeight - scrollTop - winHeight; if (thresold > -100 && thresold <= 20) { console.log(end); }});
這樣做的一個缺點就是比較消耗性能,因為當在滾動的時候,瀏覽器會無時不刻地在計算判斷是否滾動到底部的邏輯,而在實際的場景中是不需要這麼做的,在實際場景中可能是這樣的:在滾動過程中,每隔一段時間在去計算這個判斷邏輯。而函數節流所做的工作就是每隔一段時間去執行一次原本需要無時不刻地在執行的函數,所以在滾動事件中引入函數的節流是一個非常好的實踐:
$(window).on(scroll, throttle(function () { // 判斷是否滾動到底部的邏輯 let pageHeight = $(body).height(), scrollTop = $(window).scrollTop(), winHeight = $(window).height(), thresold = pageHeight - scrollTop - winHeight; if (thresold > -100 && thresold <= 20) { console.log(end); }}));
加上函數節流之後,當頁面再滾動的時候,每隔 `300ms` 才會去執行一次判斷邏輯。
簡單來說,函數的節流就是通過閉包保存一個標記(`canRun = true`),在函數的開頭**判斷**這個標記是否為 `true`,如果為 `true` 的話就繼續執行函數,否則則 return 掉,判斷完標記後立即把這個標記設為 `false`,然後把外部傳入的函數的執行包在一個 `setTimeout` 中,最後在 `setTimeout` 執行完畢後再把標記設置為 `true`(這裡很關鍵),表示可以執行下一次的循環了。當 `setTimeout` 還未執行的時候,`canRun` 這個標記始終為 `false`,在開頭的判斷中被 return 掉。
function throttle(fn, interval = 300) { let canRun = true; return function () { if (!canRun) return; canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, interval); };}
函數防抖(debounce)
這裡以用戶註冊時驗證用戶名是否被佔用為例,如今很多網站為了提高用戶體驗,不會再輸入框失去焦點的時候再去判斷用戶名是否被佔用,而是在輸入的時候就在判斷這個用戶名是否已被註冊:
$(input.user-name).on(input, function () { $.ajax({ url: `https://just.me/check`, method: post, data: { username: $(this).val(), }, success(data) { if (data.isRegistered) { $(.tips).text(該用戶名已被註冊!); } else { $(.tips).text(恭喜!該用戶名還未被註冊!); } }, error(error) { console.log(error); }, });});
很明顯,這樣的做法不好的是當用戶輸入第一個字元的時候,就開始請求判斷了,不僅對伺服器的壓力增大了,對用戶體驗也未必比原來的好。而理想的做法應該是這樣的,當用戶輸入第一個字元後的一段時間內如果還有字元輸入的話,那就暫時不去請求判斷用戶名是否被佔用。在這裡引入函數防抖就能很好地解決這個問題:
$(input.user-name).on(input, debounce(function () { $.ajax({ url: `https://just.com/check`, method: post, data: { username: $(this).val(), }, success(data) { if (data.isRegistered) { $(.tips).text(該用戶名已被註冊!); } else { $(.tips).text(恭喜!該用戶名還未被註冊!); } }, error(error) { console.log(error); }, });}));
其實函數防抖的原理也非常地簡單,通過閉包保存一個標記來保存 `setTimeout` 返回的值,每當用戶輸入的時候把前一個 `setTimeout` clear 掉,然後又創建一個新的 `setTimeout`,這樣就能保證輸入字元後的 `interval` 間隔內如果還有字元輸入的話,就不會執行 `fn` 函數了。
function debounce(fn, interval = 300) { let timeout = null; return function () { clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(this, arguments); }, interval); };}
總結
其實函數節流與函數防抖的原理非常簡單,巧妙地使用 `setTimeout` 來存放待執行的函數,這樣可以很方便的利用 `clearTimeout` 在合適的時機來清除待執行的函數。
使用函數節流與函數防抖的目的,在開頭的栗子中應該也能看得出來,就是為了節約計算機資源。
推薦閱讀: