看了《JavaScript高級程序設計》和網上的一些博客,感受對函數節流和函數防抖的概念是反的,如下我寫的關於防抖和節流的概念取決於多數人的概念吧,而且基於倫敦前端工程師David Corbacho的客座文章。文章寫的很好,而且有對應的代碼能夠操做,更容易理解。其實我以爲叫什麼不重要,這個方法叫節流仍是這個方法叫防抖,只要你能說明白,而且在生產中能用上就能夠,一個名字,不用太去糾結。javascript
《復仇者聯盟4:終局之戰》表明着一個時代的結束,從2008年高二看300多MB的《鋼鐵俠》開始,漫威電影宇宙也像哈利波特的魔法世界同樣一路伴我前行。一個時代的落幕,必將開始一個新的時代。End Game??No!css
I LOVE YOU THREE THOUSANDS TIMEShtml
banner獻給復仇者聯盟的超級英雄們🙏🙏🙏I AM IRON MAN前端
防抖和節流是兩個類似的技術,都是爲了減小一個函數無用的觸發次數,以便提升性能或者說避免資源浪費。咱們都知道js在操做DOM的時候,代價很是昂貴,相對於非DOM操做須要更多的內存和和CPU時間,假如咱們一個函數是在滾動滾動條或者更改更改窗口大小的時候頻繁觸發,仍是會出現頁面卡頓,若是是一套複雜的操做DOM邏輯,可能還會引發瀏覽器崩潰。因此咱們須要控制一下觸發的次數,來優化一下代碼執行狀況。java
口說無憑,你們可能也不瞭解究竟是怎樣操做,那就來個例子:⬇️ node
<!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>我要節流</title>
<style> body{ height: 3000px; } #centerNum { width: 100px; height: 100px; line-height: 100px; text-align: center; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); } </style>
</head>
<body>
<h1 id="centerNum">0</h1>
<script> var num = 0; window.onscroll = function () { var root = document.getElementsByTagName('body'), h = document.getElementById('centerNum'); h.innerHTML = num; num ++; } </script>
</body>
</html>
複製代碼
咱們來一個window.onscroll
的函數,只要滾動,就改變一次<h1>
標籤中的數,在上面的圖中,咱們能看到這個觸發是很是頻繁的,若是咱們不加以干涉的話,讓這個函數肆意觸發,豈不是要上天了😡api
啥是防抖呢?我本身的理解就是,當連續觸發一個方法的時候,方法並不執行,而是在連續觸發結束的時候再執行這個方法。瀏覽器
舉個例子:一部直梯,陸續往上上人(連續觸發),當再也不上人的時候(中止連續觸發),電梯纔會關門並動起來(執行方法)。前端工程師
<!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>電梯上人</title>
<style> </style>
</head>
<body>
<button id="addBtn">電梯上人,人數+1</button><button id="resetBtn">重置</button>
<p id="personNum">電梯人數:0(假設電梯能夠無限裝人)</p>
<p id="elevatorStatus">電梯停靠</p>
<script> var personNum = 0; // 電梯人數 var closeDoor = null; // 電梯啓動延時程序 var addBtn = document.getElementById('addBtn'); // 獲取添加人數按鈕 var personNumP = document.getElementById('personNum'); // 獲取顯示人數的標籤 var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕 var elevatorStatus = document.getElementById('elevatorStatus'); // 獲取電梯狀態標籤 /** * @method 電梯內添加人數 * @description 點擊一次電梯內增長一人,增長完人數電梯啓動初始化 */ function addPerson() { personNum ++; personNumP.innerHTML = `電梯人數:${personNum}(假設電梯能夠無限裝人)` initElevatorStart(); } /** * @method 電梯啓動 * @description 電梯啓動,置灰添加人數按鈕,禁止上人 */ function elevatorStart() { elevatorStatus.innerHTML = '電梯啓動'; addBtn.disabled = true; } /** * @method 電梯啓動初始化 * @description 清除以前的關門延時,並從新計算關門延時500ms,意思是當不在觸發電梯啓動初始化函數時,500ms後啓動電梯 */ function initElevatorStart() { clearTimeout(closeDoor); closeDoor = setTimeout(function () { elevatorStart(); }, 500); } /** * @method 重置電梯 */ function reset() { personNum = 0; personNumP.innerHTML = `電梯人數:${personNum}(假設電梯能夠無限裝人)` elevatorStatus.innerHTML = '電梯停靠'; addBtn.disabled = false; } addBtn.addEventListener('click', addPerson); resetBtn.addEventListener('click', reset); </script>
</body>
</html>
複製代碼
上面的代碼意思就是我電梯上一我的,就須要關閉電梯門(觸發initElevatorStart()
方法),而後電梯啓動。可是我一直在點擊上人的按鈕,電梯是不會觸發關門啓動電梯的elevatorStart()
方法。閉包
代碼的核心是initElevatorStart()
方法,這個方法在實際須要執行的關門啓動電梯方法elevatorStart()
外面添加了一層setTimeout
方法,也就是爲了在調用這個方法的時候咱們過500毫秒再去執行真正須要執行的方法。若是這500毫秒以內,又從新觸發了initElevatorStart()
方法,就須要從新計時,要不不就夾到人了嘛,要賠錢的。。。。
這是防抖最粗糙的實現了😳😳😳
下面是這個防抖實現的最基本的形式,也是咱們在《JavaScript高級程序設計》中看到的樣子⬇️
var processor = {
timeoutId: null, // 至關於延時setTimeout的一個標記,方便清除的時候使用
// 實際進行處理的方法
// 連續觸發中止之後須要觸發的代碼
performProcessiong: function () {
// 實際執行的代碼
// 這裏實際就是須要在中止觸發的時候執行的代碼
},
// 初始處理調用的方法
// 在實際須要觸發的代碼外面包一層延時clearTimeout方法,以便控制連續觸發帶來的無用調用
process: function () {
clearTimeout(this.timeoutId); // 先清除以前的延時,並在下面從新開始計算時間
var that = this; // 咱們須要保存做用域,由於下面的setTimeout的做用域是在window,調用不要咱們須要執行的this.performProcessiong方法
this.timeoutId = setTimeout(function () { // 100毫秒之後執行performProcessiong方法
that.performProcessiong();
}, 100) // 若是尚未執行就又被觸發,會根據上面的clearTimeout來清除並從新開始計算
}
};
// 嘗試開始執行
processor.process(); // 須要從新綁定在一個觸發條件裏
複製代碼
上面這段代碼就是最基本的實現方式,包在一個對象中,而後在對象中互相調用,裏面的註釋應該能夠很清楚的說明每一步是幹什麼呢,最下面的processor.process()
咱們在實際使用的時候確定是須要綁定在一個觸發條件上的,好比以前的上電梯問題上,咱們就須要把processor.process()
方法綁定在增長人數的裏面,這樣纔會有屢次調用的狀況發生
上面再怎麼說都是很簡單的實現,在實際生產環境中,邏輯會相對複雜不少,可是萬變不離其宗,參透了最基礎的,再觸類旁通就不是什麼問題了
具體我也不知道應該叫啥,英文叫「Leading edge」,甭管中文叫啥了,知道是什麼意思就好了。以前咱們寫的代碼很明顯能夠看出來,在咱們連續觸發一個方法的時候,是在setTimeout
結束後纔去真正執行,可是還有一種狀況,那就是咱們在連續觸發一個方法的時候,第一次觸發就執行了,而後後面的連續觸發再也不執行,等連續觸發中止,通過延時之後,再次觸發纔會真正執行。
我仍是盜圖吧。。。廣泛的形式是下面這種
連續觸發結束時執行,而咱們如今說的「前搖」則是下面這種狀況 在連續觸發的一開始就執行了,而後日後的連續觸發不執行,連續觸發中止後再通過延時時間後觸發纔會再次執行下面是我本身寫的,大概意思是這樣,代碼實現也貼出來
<!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>電梯上人</title>
<style> </style>
</head>
<body>
<button id="addBtn">電梯上人,人數+1</button><button id="resetBtn">重置</button>
<p id="personNum">電梯人數:0(假設電梯能夠無限裝人)</p>
<script> var personNum = 0; // 電梯人數 var okNext = true; // 是否可進行下次執行 var timeoutFn = null; var addBtn = document.getElementById('addBtn'); // 獲取添加人數按鈕 var personNumP = document.getElementById('personNum'); // 獲取顯示人數的標籤 var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕 /** * @method 電梯添加人數 * @description 電梯能夠上人,可是上人之後就不能再上了,無論怎麼觸發都不行,除非中止觸發500毫秒之後,再觸發的時候才能夠繼續執行 */ function addPerson() { if (okNext) { okNext = false; personNum ++ personNumP.innerHTML = `電梯人數:${personNum}(假設電梯能夠無限裝人)` } clearTimeout(timeoutFn); timeoutFn = setTimeout(function () { okNext = true; }, 500) } /** * @method 重置 */ function reset() { personNum = 0; personNumP.innerHTML = '電梯人數:0(假設電梯能夠無限裝人)'; } addBtn.addEventListener('click', addPerson); resetBtn.addEventListener('click', reset); </script>
</body>
</html>
複製代碼
上面代碼要是看不太明白,能夠直接粘下去本身執行如下看看是什麼感受,就知道是什麼意思了。
代碼純我本身寫的,要是有不對的地方,請大佬指正啊
節流呢,也是我本身的理解,在連續觸發一個方法的某一時間段中,控制方法的執行次數。
一樣舉個例子吧,一個地鐵進站閘口,10秒進一我的(10秒內執行一個方法),管這10秒中來了是5我的、10我的仍是20我的,都只是進一我的(從第一次觸發後10秒無論被觸發多少次都不會執行,直到下一個10秒纔會再執行)。
咱們首先用時間戳來判斷先後的時間間隔,而後就能夠知道我從上次執行完這個方法過了多久,過了這麼長時間,是否是已經超過了本身規定的時長,若是時長超過了,我就能夠再次執行了
<!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>地鐵進站</title>
</head>
<body>
<button id="addBtn">進站人數+1</button><button id="resetBtn">重置</button>
<p id="personTotal">旅客總人數:0</p>
<p id="personNum">進站人數:0</p>
<script> var personNum = 0; // 進站人數 var personTotal = 0; // 一共來了多少人 var addBtn = document.getElementById('addBtn'); // 獲取添加人數按鈕 var personNumP = document.getElementById('personNum'); // 獲取顯示人數的標籤 var personTotalP = document.getElementById('personTotal'); // 獲取顯示總人數的標籤 var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕 /** * @method 增長進站人數 * @description 每一個時間間隔執行的方法 */ function addPerson() { personNum ++; personNumP.innerHTML = `進站人數:${personNum}`; } /** * @method 節流方法(時間戳) * @param {Function} fn 須要節流的實際方法 * @param {Number} wait 須要控制的時間長度 * @description 根據上一次執行的時間,和這一次執行的時間作比較,若是大於控制的時間,就能夠執行 */ function throttle(fn, wait) { var prev = 0; // 第一次執行的時候是0,因此第一次點擊的時候確定大於這個數,因此會立馬執行 return function () { var context = this; var args = arguments; var now = Date.now(); // 實際執行的時間 personTotal ++; personTotalP.innerHTML = `旅客總人數:${personTotal}`; if (now - prev >= wait) { // 執行的時間是否是比上次執行的時間大於須要延遲的時間,大於,咱們就執行 fn.apply(context, args); prev = now; // 執行了之後,重置上一次執行的時間爲剛剛執行此次函數的時間,下次執行就用這個時間爲基準 } } } /** * @method 重置 */ function reset() { personNum = 0; personTotal = 0; personNumP.innerHTML = '進站人數:0'; personTotalP.innerHTML = `旅客總人數:0`; } addBtn.addEventListener('click', throttle(addPerson, 1000)); resetBtn.addEventListener('click', reset); </script>
</body>
</html>
複製代碼
節流函數throttle
用到了做用域,call、apply和閉包等相關的知識,看不懂的能夠看我以前的文章
上面的代碼中我感受能夠很直觀的看出來是根據判斷先後兩次的時間,來得知可不能夠進行下一次函數的執行。參考着代碼中的註釋我以爲應該能夠看明白吧😳😳😳
若是咱們用setTimeout
的話,咱們只須要更改一下throttle
方法
/** * @method 節流方法(setTimeout) * @param {Function} fn 須要節流的實際方法 * @param {Number} wait 須要控制的時間長度 * @description 這個方法就很相似防抖了,就是判斷當前函數有沒有延遲setTimeout函數,有的話就不執行了 */
function throttle(fn, wait) {
var timeout = null;
return function () {
var context = this;
var args = arguments;
personTotal ++;
personTotalP.innerHTML = `旅客總人數:${personTotal}`;
if (!timeout) {
var that = this;
timeout = setTimeout(() => {
timeout = null;
fn.apply(context, args)
}, wait)
}
}
}
複製代碼
雖然咱們只須要更改幾行代碼就實現了用setTimeout
實現節流的這個方法,可是咱們仔細看上面的圖,咱們能夠發現,當我點擊第一次的時候,進站旅客是沒有增長的,這跟咱們實際狀況不同,咱們先來的,我不用等啊,我直接就能進站,對不對。還有當我結束增長人數的時候,進站旅客過去等待時間之後還會加一我的,這固然也不是咱們想看到的。
使用時間戳仍是setTimeout,取決於業務場景了
誒??rAF是什麼?什麼是requestAnimationFrame?這在我沒有寫這篇博客的時候,我根本不知道window下還有個這個方法,神奇吧,那這個方法是幹什麼的呢??
告訴瀏覽器——你但願執行一個動畫,而且要求瀏覽器在下次重繪以前調用指定的回調函數更新動畫。該方法須要傳入一個回調函數做爲參數,該回調函數會在瀏覽器下一次重繪以前執行。————《MDN Web Docs》
就是在用這個能夠一直重繪動畫,而後讓人看起來是個動畫,重繪的這個過程是個很頻繁的操做,因此若是咱們本身寫,不加以干涉,在性能和資源上會形成嚴重的浪費,因此咱們可使用requestAnimationFrame來使用咱們的動畫看起來很流暢,又不會頻繁調用
優勢
缺點
直接上圖
<!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>rAF使用</title>
<style> #SomeElementYouWantToAnimate { width: 100px; height: 100px; background-color: #000; } </style>
</head>
<body>
<div id="SomeElementYouWantToAnimate"></div>
<script> var start = null; var element = document.getElementById('SomeElementYouWantToAnimate'); element.style.position = 'absolute'; /** * @method 移動咱們的小黑方塊 */ function step(timestamp) { if (!start) start = timestamp; var progress = timestamp - start; element.style.left = Math.min(progress / 10, 200) + 'px'; if (progress < 2000) { window.requestAnimationFrame(step); } } window.requestAnimationFrame(step); </script>
</body>
</html>
複製代碼
rAF是一個內部api,固定的16毫秒執行一次,由於人眼接受60fps的動畫就會感到很流暢了,若是咱們須要改變rAF的執行時間,那咱們只能本身去寫動畫的方法,節流仍是防抖,看我的愛好了
防抖:連續觸發一個函數,不論是觸發開始執行仍是結束執行,只要在連續觸發,就只執行一次
節流:規定時間內只執行一次,不論是規定時間內被觸發了多少次
rAF:也算是一種節流手段,原生api,旨在使動畫在儘可能少佔用資源的狀況下使動畫流暢
lodash中相對應的_.throttle和_.debounce,在我看來是最佳實踐了,推薦使用
《復仇者聯盟4》現階段的漫威宇宙的結束,《哈利·波特》《火影忍者》一個個完結的電影,雖然在時刻提醒着咱們青春再慢慢的消失,正如英雄聯盟中的那句話,咱們有了新的敵人叫「生活」。當這些完結的並非真正的結束,《哈利·波特》有《神奇動物在哪裏》,《火影忍者》有《博人傳》,《鋼鐵俠》有《蜘蛛俠》,晚輩從前輩手中接過接力棒,繼續日後跑,咱們也從本身青蔥的歲月進入下一階段,努力奮鬥吧!!
我是前端戰五渣,一個前端界的小學生。