防抖和節流

2022-12-19 15:00:19

簡單理解節流就是節省資源開銷,之前說迴流是GPU給元素畫圖之前需要根據佈局去計算元素的一些位置屬性,例如寬、高、橫縱座標等等,那反覆計算這些就是迴流。節流又是節省資源開銷,不讓一些事件函數高頻率的反覆執行.....其實迴流和節流還是有一定關係但也不是特別大。

理解

首先什麼是防抖?為什麼要防抖?

某天晚上大春和馬冬梅決定偷偷FQ去看望受傷的夏洛,在夏洛窗外,冬梅讓大春放哨,看見有人過來了就說一聲,當冬梅FQ一半的時候,大春剛好看到一個拿三叉戟的黑影出現在電話亭那邊,大春很著急,立馬告訴冬梅「冬梅啊有人來了要不咱們撤吧,冬梅啊有人來了要不咱們撤吧,冬梅啊有人來了要不咱們撤吧.....」。

好回到正題,防抖之前:大春反覆的說「冬梅啊有人來了要不咱們撤吧」,防抖之後:就一句「冬梅啊有人來了要不咱們撤吧」。所以所謂防抖也就是防止事件沒必要的重複觸發

為什麼要防抖,其實這個問題以前自己肯定是遇到這種場景的,只是當時能力還不夠來考慮這種效能優化的問題。例如:使用vue時@input事件繫結在input元件上隨意輸入一個內容他都會觸發事件對吧,那我就想要輸入完成後再觸發嘞(不槓@change), 再例如監聽捲動事件 scroll 時會密集的觸發對吧,那我就是不想讓他密集的觸發。還有瀏覽器視窗大小改變的事件resize、keypress、mousemove等等都是高頻事件,他們在觸發時會不斷地呼叫繫結在事件上的回撥函數,極大地浪費資源,降低前端效能。

像 scroll 這種高頻事件整體的觸發完成之後,再進行事件操作,這才是我想要的。就是所謂的「防抖」,其本質就是優化高頻執行程式碼的一種手段

什麼是節流?為什麼要節流?

這時候在牆上的冬梅瞅了瞅原來是個漁夫在那裡打電話而已,告訴大春「大春沒事兒,他不是保安,真走過來了你再告訴我」,不一會大春又看到那漁夫真過來了,一步兩步好像在跳舞邊跳邊走,於是大春跟著他的節奏,走兩步就告訴冬梅「冬梅那漁夫好像過來了」,走兩步就告訴冬梅「冬梅那漁夫好像過來了」......。

好回到正題,節流之前:大春反覆的說「冬梅那漁夫好像過來了」,節流之後:大春跟著節奏每兩步才說。
還有個栗子防抖就像王者的回城,打斷了就要重新來,節流是技能冷卻,需要等cd時間過了才能繼續使用技能。我王者這個栗子感覺不是特別準確,看怎麼理解吧。

節流跟防抖差不太多,區別在於防抖是避免事件沒必要地大量重複執行,而節流是這種「避免」放鬆一點,在事件連續觸發的時候規定時間間隔每執行一次就好,例如給scroll事件設定防抖那他從開始捲動到最後也就執行一次,而給scroll事件設定節流則是在開始捲動到結束捲動的時間內定時輪番的去執行操作。這就是節流

區別

節流和防抖的區別:

  • 防抖是 多次觸發,只執行最後一次。適用於只需要一次觸發生效的場景。

  • 節流是 每隔一段時間觸發一次操作。適用於多次觸發要多次生效的場景。

應用場景:

  • debounce

    • search搜尋聯想,使用者在不斷輸入值時,用防抖來節約請求資源。

    • window觸發resize的時候,不斷的調整瀏覽器視窗大小會不斷的觸發這個事件,用防抖來讓其只觸發一次

  • throttle

    • 滑鼠不斷點選觸發,mousedown(單位時間內只觸發一次)

    • 監聽scroll捲動事件,比如是否滑到底部自動載入更多,用throttle來判斷

 

防抖debounce

大致思路:通過定時器(延遲器)實現,第一次觸發之後設定好定時器,如果之後再有該事件觸發那就把上一次的定時器清理再重新設定定時器,定時時間到則執行目標操作,如此反覆。

非立即執行版本

先看一種比較簡單的防抖實現(非立即執行版本),就是需要經過定時器的延遲才會執行。

// 防抖 定時器實現(非立即執行版本)
 function debounce(fn, delay = 200) {
   let timer = null
   return function() {
     if(timer){   // 如果設定過了定時器
       clearTimeout(timer)
     }
     timer = setTimeout(() => {
       fn.apply(this, arguments); // 透傳 this和引數
       timer = null
     },delay)
   }
 }

立即執行版本

再看立即執行版本:立即執行的意思是觸發事件後函數會立即執行,然後 n 秒內不觸發事件才能繼續執行函數的效果。

// 防抖 定時器實現(立即執行版本)
 function debounce(fn, delay = 200) {
     let timer = null
     return function () {
         let args = arguments
         let now = !timer
         timer && clearTimeout(timer)   //timer不為null時執行clearTimeout函數
         timer = setTimeout(() => {
             timer = null
         }, delay)
         if (now) {   
             fn.apply(this, args)
         }  //或者:now && fn.apply(this, args)
     }
 }

上邊呼叫設定之後,在繫結事件的時候這樣使用就好

window.onscroll = debounce(lozyLoad,1000);  //lozyLoad是想要執行的回撥函數

 

節流throttle

非立即執行版本

節流的簡單實現:

// 節流函數(簡單版本):第一次觸發時不會執行,而是在delay毫秒之後才執行
 function throttle(fn, delay = 200) {
   let  timer = 0
   return function () {
     if(timer){
       return
     }
     timer = setTimeout(() =>{
       fn.apply(this, arguments); // 透傳 this和引數
       timer = 0
     },delay)
   }
 }

立即執行版本

也是節流的簡單實現:

//節流函數(時間戳版):觸發事件時立即執行,以後每過delay毫秒之後才執行一次,並且最後一次觸發事件若不滿足要求不會被執行
 function throttle(fn ,delay = 200){
     let oldtime = Date.now();
     return function(){
         let context = this;
         let args = arguments;
         let newtime = Date.now();
         if(newtime - oldtime >= delay){
             fn.apply(context,args);
             oldtime = Date.now();
         }
     }
 }

精準節流方案

其實就是將以上兩種方式相結合,實現一個更加精準的節流:第一次會馬上執行,最後一次也會執行

//節流函數:定時器和時間戳結合版本
 function throttle(fn ,delay = 200){
     let timer = null;
     let starttime = Date.now();
     return function(){
         let curTime = Date.now();
         let remaining = delay -(curTime - starttime);
         let  context = this ;
         let args = arguments;
         clearTimeout(timer);
         if(remaining <= 0) {
             fn.apply(context,args);
             starttime = Date.now();
         }else{
             timer = setTimeout(fn,remaining);
         }
     }
 }

 

可以參考這篇文章對程式碼有更詳細的註釋:連結