節流是保證在一段時間內,代碼只執行了一次。這個一段時間內指的是無論用戶操做了幾回,最終僅執行一次。好比說一個按鈕,用戶狂點按鈕,可是若是用節流技術的話,無論用戶點擊了幾回,最終某個時間段內只執行了一次代碼。這個時間段是能夠自行設置,好比說每一秒執行一次。html
防抖其實和節流有些相似,畢竟它們的最終目的都是一模一樣。防抖是在一段時間結束以後,才觸發一次事件。若是一段時間內未結束再次觸發了事件,那麼就會從新計算這段時間。一樣的例子,仍是用戶狂點按鈕。可是僅在用戶中止點擊按鈕後的一段時間以後纔會執行一次。若是用戶暫停點擊按鈕的時間不到一段時間內又再次點擊按鈕,那麼就會從新計算時間。這個時間一樣能夠自行設置。閉包
爲了優化高頻率事件,好比說onscroll滾動 oninput搜索框聯想 resize窗口大小變化 onkeydown onkeyup...等等。這些高頻率事件頗有可能致使頁面卡頓,影響用戶體驗。運用防抖和節流能夠有效下降代碼的執行頻率,從而解決高頻率事件的頁面卡頓問題。或許還有疑問,爲啥高頻事件就會致使頁面卡頓呢?
這就要從頁面的展現過程提及了。app
展現過程大體爲如下順序:函數
首先,Javascript階段會往頁面中添加一些DOM或動畫,而後到Style階段肯定每一個DOM應該用什麼樣式規則。在Layout階段佈局,最終肯定DOM顯示的位置和大小。在Paint階段進行DOM的繪製,它是在不一樣層上進行繪製。注意,樣式變化是重繪,佈局和位置變化是重排。重排必定致使重繪,重繪不必定致使重排。最後一個階段Composite進行渲染層合併。(因此作一些動畫效果儘可能用CSS3的transform等屬性,由於該屬性是脫離文檔流,不用合併渲染層的。)因而可知,若是觸發了不少高頻率的事件,就會致使頁面不停的肯定位置和大小 ,不停的重排重繪而且合併渲染層。因此致使頁面卡頓也能夠解釋了。佈局
接下來會用例子來一步步實現節流和防抖的原理。測試
首先 好比頁面上有個按鈕,用戶能夠點擊該按鈕。該按鈕上綁定了一個點擊事件,用戶能夠瘋狂點擊觸發該事件,確定結果就是瘋狂觸發該事件。目標是讓該按鈕無論用戶點擊的多快,最終該事件每秒僅執行一次。優化
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>throttle</title> <style> .btn{ width: 250px; height: 60px; background-color: hotpink; color: #fff; display: block; text-align: center; line-height: 60px; cursor: pointer; border-radius: 10px; } </style> </head> <body> <btn class="btn">按鈕</btn> <script> let btn = document.getElementsByClassName('btn')[0]; function logger(){ console.log('log'); } /* 按鈕綁定了一個事件 打印log */ btn.addEventListener('click',logger); </script> </body> </html>
能夠看到,用戶瘋狂點擊了20次,那麼該事件也理所固然的執行了20次,這顯然不是咱們想要的。
基礎版:動畫
<body> <btn class="btn">按鈕</btn> <script> let btn = document.getElementsByClassName('btn')[0]; function logger(){ console.log('log'); } btn.addEventListener('click',throttle(logger,1000)); function throttle(func, wait){ /* 上次的時間戳 默認第一次0 */ let pre = 0; return function(){ let now = Date.now(); /* 若是當前時間與上次時間的間隔大於wait */ if(now - pre > wait){ func.apply(this,arguments); pre = now; } } } </script> </body>
爲了儘量的減小篇幅,把一些無用的代碼都刪除了。
定義一個throttle方法,該方法傳入了兩個參數,一個是要執行的事件,另外一個是間隔時間。該throttle方法是一個閉包的寫法,而且返回了一個函數。首先定義了上次的時間戳pre,pre默認第一次爲0。而後獲取到當前時間,用當前時間減去上次的時間戳也就是pre,若是這個差值大於了傳遞的時間間隔wait,也就代表能夠執行下一次的函數了。因此執行方法而且傳遞this和參數。並把當前時間賦給pre,以便作下一次節流的判斷。 看下效果:ui
能夠看到,雖然瘋狂點擊按鈕,可是事件卻沒有瘋狂觸發,保持了每一秒執行一次的速度。也就達成了咱們的目標。
可是還有一個問題就是,我最後點擊按鈕的那次也應該延遲觸發最後一次的事件,可是結果並無。須要補上最後一次沒有觸發事件的問題,接下來優化它。
進階版:this
<body> <btn class="btn">按鈕</btn> <script> let btn = document.getElementsByClassName('btn')[0]; function logger(){ console.log('log'); } btn.addEventListener('click',throttle(logger,1000,{trailing:true})); function throttle(func, wait, options){ let pre = 0; /* 定義一個timeout定時器 */ let timeout; return function(){ let now = Date.now(); if(now - pre > wait){ if(timeout){ clearTimeout(timeout); timeout = null; } func.apply(this,arguments); pre = now; }else if(!timeout && options.trailing !== false){ /* 若是當前時間和上次️時間的間隔小於wait 而且trailing爲true */ timeout = setTimeout(later,wait-(now-pre)); } } function later(){ func.apply(this,arguments); } } </script> </body>
很明顯看到,進階版多傳了一個參數對象,trailing:true。該參數用來表示是否執行最後一次觸發的方法。
在函數中,首先定義了一個空的定時器變量timeout,用來計算時間間隔。其次多了一個else if的條件判斷,判斷若是時間間隔小於wait,就表示該方法要保留起來延遲去執行。因此生成了一個定時器,延遲執行later函數,later函數就是執行該func函數。此處注意一點,這個延遲時間的問題。延遲時間不能是wait,必須是wait減去當前時間和上次時間的時間獎額。剩下的纔是剩餘時間延遲。還有一點要注意,在if中必定要清楚定時器,否則會影響else if的條件判斷。通過測試,確實能在點擊的最後一次後,延遲不到一秒觸發了該事件。
剩下最後一個優化點,其實第一次點擊按鈕,也應該延遲觸發事件。目前的版本是點擊按鈕的第一次就直接觸發該事件。優化它:
最終版:
<body> <btn class="btn">按鈕</btn> <script> let btn = document.getElementsByClassName('btn')[0]; function logger(){ console.log('log'); } btn.addEventListener('click',throttle(logger,1000,{leading:false})); function throttle(func, wait, options){ let pre = 0; let timeout; let now = Date.now(); /* leading爲false 把當前時間賦給上次時間pre */ if(!options.leading) pre = now; return function(){ if(now - pre > wait){ if(timeout){ clearTimeout(timeout); timeout = null; } func.apply(this,arguments); pre = now; }else if(!timeout && options.trailing !== false){ timeout = setTimeout(later,wait-(now-pre)); } } function later(){ /* 若是leading爲false 校訂pre時間爲0 */ pre = options.leading===false?0:Date.now(); func.apply(this,arguments); } } </script> </body>
能夠看到,傳遞一個新的參數對象leading爲false。用來表示第一次也延遲執行。那麼問題來了,怎樣才能第一次延遲執行呢?實現其實很簡單,進階版已經實現了else if延遲執行,現只需讓第一次不走if,走else if就可實現第一次的延遲執行。總共改動僅兩處,第一處:判斷用戶是否傳遞了參數leading爲false。若是傳遞了leading爲false,則把當前時間now賦給上次時間pre。爲什麼這樣作呢? 目的就是爲了第一步的時候也走else if。這麼看。pre=now 那麼if判斷條件就至關與now-now。now-now=0,固然不知足if條件,即第一次走了else if。這還不算完,在else if中要校訂pre時間。若是option.leading爲false,那麼pre就初始爲0。pre爲0的就會走if。只有走了if纔會清空定時器,否則的話只會執行一次便不會繼續往下執行。由於if和else if的判斷條件都不知足。👌,節流到此爲止,接下來是防抖。
基礎版:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>debounce</title> <style> .btn{ width: 250px; height: 60px; background-color: hotpink; color: #fff; display: block; text-align: center; line-height: 60px; cursor: pointer; border-radius: 10px; } </style> </head> <body> <btn class="btn">按鈕</btn> <script> let btn = document.getElementsByClassName('btn')[0]; function logger(){ console.log('log'); } btn.addEventListener('click',debounce(logger,1000)); function debounce(func, wait){ let timeout; return function(){ clearTimeout(timeout); timeout = setTimeout(()=>{ func.apply(this,arguments) },wait); } } </script> </body> </html>
和節流相似,也是一個按鈕,按鈕上綁定事件。經過debounce函數,傳入了func和wait。首先,debounce函數也是一個閉包寫法,並返回了一個函數。該函數作了兩件事。第一,清除上次的定時器。第二,執行並定時器並把wait延遲時間傳進去(定時器中執行了func函數)。至此,便可以實現防抖功能。
能夠看到,無論瘋狂點擊了多少次。僅僅執行了最後的那一次。只有當時間間隔大於1秒後,纔有機會觸發下一個函數。可是咱們不只僅知足於此,若是用戶第一次點擊的時候就想立刻執行一次,接下來的點擊才延遲執行呢?實現它:
最終版:
<body> <btn class="btn">按鈕</btn> <script> let btn = document.getElementsByClassName('btn')[0]; function logger(){ console.log('log'); } btn.addEventListener('click',debounce(logger,1000,true)); function debounce(func, wait, firstRun){ let timeout; return function(){ clearTimeout(timeout); if(firstRun){ func.apply(this,arguments); firstRun = false; }else{ timeout = setTimeout(()=>{ func.apply(this,arguments) },wait); } } } </script> </body>
首先看到參數的變化,debounce多了第三個參數firstRun。參數firstRun是第一次是否延遲執行的標識。true表示第一次當即執行。反之,延遲執行。debounce函數體中多了一個條件判斷if。首先判斷了第三個參數是否爲true,爲true就當即執行func並把this綁定把參數傳遞進去。而且,很重要一點,執行完func後把firstRun置爲false。這樣以後的執行都會走else if。else if就是正常的延遲執行。看下效果吧:
能夠看到。當點擊按鈕的第一次就立刻觸發了函數,以後的瘋狂點擊暫停後也僅僅執行了一次。效果達成~
ok,至此。咱們分別實現了防抖和節流。喜歡點個👍,thx~