前端進擊的巨人(八):淺談函式防抖與節流
本篇課題,或許早已是爛大街的解讀文章。不過春招系列面試下來,不少夥伴們還是似懂非懂地栽倒在(~面試官~)深意的笑容之下,權當溫故知新。
JavaScript的執行過程,是基於棧來進行的。複雜的程式程式碼被封裝到函式中,程式執行時,函式不斷被推入執行棧中。所以 "執行棧" 也稱 "函式執行棧" 。
函式中封裝的程式碼塊,一般都有相對複雜的邏輯處理(計算/判斷),例如函式中可能會涉及到 DOM
的渲染更新,複雜的計算與驗證, Ajax
資料請求等等。
前端頁面的操作權,大部分都是屬於瀏覽斷的客戶爸爸們(單身三十年的手速,惹不起惹不起!!!)。如果函式被頻繁呼叫,造成的效能開銷絕對不只一點點。
DOM
既要提升使用者體驗,又要減少後端服務開銷,可見我們大前端的使命不只一頁PPT。說好前因,接著就是後果了。既然有優化的需求,必然就要有相應的解決方案。隆重請出主角: “防抖” 與 “節流” 。
防抖(debounce)
在事件被觸發 n 秒後再執行回撥函式,如果在這 n 秒內又被觸發,則重新計時延遲時間。
生活化理解:英雄的技能條,技能條讀完才能使用技能(R大招60s)
防抖的實現方式分兩種 “立即執行” 和 “非立即執行” ,區別在於第一次觸發時,是否立即執行回撥函式。
非立即執行
”非立即執行防抖“ 指事件觸發後,回撥函式不會立即執行,會在延遲時間 n 秒後執行,如果 n 秒內被呼叫多次,則重新計時延遲時間
// e.g. 防抖 - 非立即執行 function debounce(func, delay) { var timeout; return function() { var context = this; var args = arguments; // && 短路運算 == if(timeout) else {...} timeout && clearTimeout(timeout); timeout = setTimeout(function(){ func.apply(context, args); }, delay); } } // 呼叫 var printUserName = debounce(function(){ console.log(this.value); }, 800); document.getElementById('username') .addEventListener('keyup', printUserName);
立即執行
“立即執行防抖” 指事件觸發後,回撥函式會立即執行,之後要想觸發執行回撥函式,需等待 n 秒延遲
// e.g. 防抖 - 立即執行 function debounce(func, delay) { var timeout; return function() { var context = this; var args = arguments; callNow = !timeout; timeout = setTimeout(function() { timeout = null; }, delay); callNow && func.apply(context, args); } }
函式防抖原理:通過維護一個定時器,其延遲計時以最後一次觸發為計時起點,到達延遲時間後才會觸發函式執行。
節流(throttle)
規定在一個單位時間內,只能觸發一次函式。如果這個單位時間內觸發多次函式,只有一次生效(間隔執行)
生活化理解:
- FPS射擊遊戲子彈射速(即使按住滑鼠左鍵,射出子彈的速度也是限定的)
- 水龍頭的滴水(水滴攢到一定重量才會下落)
函式節流實現的方式有 “時間戳” 和 “定時器” 兩種。
時間戳
// e.g. 節流 - 時間戳 function throttle(func, delay) { var lastTime = 0; return function() { var context = this; var args = arguments; var nowTime = +new Date(); if (nowTime > lastTime + delay) { func.apply(context, args) lastTime = nowTime; } } }
“時間戳”的方式,函式在時間段開始時執行。
缺點:假定函式間隔1s執行,如果最後一次停止觸發,卡在4.2s,則不會再執行。
定時器
// e.g. 節流 - 定時器 function throttle(func, delay) { var timeout; return function() { var context = this; var args = arguments; if (!timeout) { setTimeout(function(){ func.apply(context, args); timeout = null; }, delay) } } }
“定時器”的方式,函式在時間段結束時執行。可理解為函式並不會立即執行,而是等待延遲計時完成才執行。 (由於定時器延時,最後一次觸發後,可能會再執行一次回撥函式)
時間戳 + 定時器(互補優化)
// e.g. 節流 - 時間戳 + 定時器 function throttle(func, delay) { let lastTime, timeout; return function() { let context = this; let args = arguments; let nowTime = +new Date(); if (lastTime && nowTime < lastTime + delay) { timeout && clearTimeout(timeout); timeout = setTimeout(function(){ lastTime = nowTime; func.apply(context, args); }, delay); } else { lastTime = nowTime; func.apply(context, args); } } }
合併優化的原理:“時間戳”方式讓函式在時間段開始時執行(第一次觸發立即執行),“定時器”方式讓函式在最後一次事件觸發後(如4.2s)也能觸發。
函式節流原理:一定時間內只觸發一次,間隔執行。通過判斷是否到達指定觸發時間,間隔時間固定。
“防抖” 與 “節流” 的異同
相同:都是防止某一時間段內,函式被頻繁呼叫執行,通過時間頻率控制,減少回撥函式執行次數,來實現相關效能優化。
區別:“防抖”是某一時間內只執行一次,最後一次觸發後過段時間執行,而“節流”則是間隔時間執行,間隔時間固定。
“防抖” 與 “節流” 的應用場景
防抖
- 文字輸入搜尋聯想
- 文字輸入驗證(包括 Ajax 後端驗證)
節流
scroll resize mousemove
應用場景還有很多,具體場景需具體分析。只要涉及高頻的函式呼叫,都可參考函式防抖節流的優化方案。
鼓起勇氣寫在結尾:以上程式碼都不是 “完美” 的 “防抖 / 節流” 實現程式碼!!!僅就實現方式和基本原理,淺談分解一二。
實際程式碼開發中,一般會引入 lodash
相對 “靠譜” 的第三方庫,幫我們去實現防抖節流的工具函式。有興趣的夥伴們可閱讀 lodash
相關原始碼,加深印象理解可再讀以下參考文章。