函數防抖(debounce)和節流(throttle)在H5編輯器項目中的應用
- 2020 年 2 月 1 日
- 筆記
函數防抖(debounce)和節流(throttle)在H5編輯器項目中的應用
前端開發者對函數防抖和函數節流或多或少有些了解,最近在做一個H5編輯器的項目,由於畫布功能複雜,計算量較大,在鼠標拖拽操作時尤其是在低配電腦上能夠明顯感受到卡頓,自然聯想運用函數防抖(debounce)和函數節流(throttle)來優化 mousemove
等實時計算方法的計算頻率再合適不過了。
1. 為什麼要防抖節流
1.1 客戶端性能瓶頸
眾所周知,瀏覽器JavaScript單線程的性能有限,一般用戶瀏覽網頁時的交互較少,所以和後端及網絡性能比起來,前端渲染時間佔比較小,然而在H5編輯器這類功能複雜的純前端交互系統中,每當 mousemove
、scroll
、 resize
等事件觸發時,會不斷調用綁定的回調函數,非常耗費計算資源,如果能減少計算頻率,對前端用戶體驗會有明顯提升。
1.2 刷新率的必要性
目前大部分的顯示器的刷新率是 60hz,而且人的肉眼也只能分辨出一定頻率的變化,可以說1000fps和100fps對於人體感官的差異是微乎其微的,目前主流瀏覽器的 mousemove
事件的頻率在 130/s 左右,如果不是在畫質精良的遊戲大作中,其實是完全沒有必要的。
1.3 需求所迫
還有很多時候,為了減少不必要的臟數據以保證數據的準確性,以及降低服務器負載等,前端必須減少函數觸發次數,不得不使用函數節流防抖。
2. 防抖 & 節流的概念
對於頻率限制,前端開發中有兩種常見操作:函數防抖(debounce)
和 函數節流(throttle)
,兩種方法雖然都是降低頻率,卻又存在差異,下面用一個網絡上看到的例子來方便理解。
2.1 函數防抖 (debounce)
效果:等待足夠的空閑時間後,才執行代碼一次
比如坐公交,在一定時間內,如果有人陸續刷卡上車,司機就不會開車。只有沒人刷卡上車了,司機才會開車,這是防抖的思想。
2.2 函數節流 (throttle)
效果:一定時間內只執行代碼一次
水龍頭放水,如果想節水,可以手動減小水流,但是它仍會一直流,這是節流的思想。
2.3 圖示對比
如果上述還不夠明了,下圖三種方式對 mousemove
的監聽可以很好地解釋 debounce
和 throttle
的區別,當鼠標停下一定時間,debounce
才會執行,而 throttle
也會一直執行,但是頻率明顯低於常規 mousemove
。

3. 實際工程應用場景
函數防抖節流在本人開發的H5編輯器中有很多處應用,列舉幾處:
3.1 函數節流應用場景
1. 防止重複點擊
按鈕防止重複點擊是每個前端開發者的必修課,尤其是面向C端的系統,例如營銷活動,玩家拚命點擊領獎按鈕,沒有防重機制會對服務器造成較大壓力。很多時候,開發者會優先用遮罩層來防止用戶重複點擊,其實,理論上單用遮罩是不能防重的,因為如果客戶端性能不足,遮罩層的顯示會在用戶多次點擊之後。最好的方法是函數節流配合遮罩,保證在遮罩啟動前按鈕事件只會觸發一次,就可以很好地解決按鈕重複點擊的問題。
2. mousemove
時位置計算
鼠標移動時候需要計算元素位置、碰撞檢測、邊緣檢測、參考線計算、網格吸附,十分消耗資源,函數節流,實測 50 ~ 60fps 已然足夠。
3. scroll
時畫布計算與標尺繪製
在滾動畫布時候,canvas
繪製的標尺需要固定在屏幕位置,但刻度需要跟隨畫布移動,計算新的起點、繪製輔助尺等工具,函數節流也非常合適
4. resize
時重繪整個畫布
由於 resize
可能帶來整個畫布尺寸的變化,重繪畫布是非常必要的,否則可能出現樣式錯位等問題,雖然一般用戶調整窗口尺寸的次數不多,但是使用函數節流後的體驗還是非常好的。
3.2 函數防抖應用場景
1. autosave
狀態保存
H5編輯器支持 撤銷
、前進
功能,需要實時監聽 setter
引起的狀態數據的變化,自動保存狀態用於回滾,然而就拿移動元素來說,如果實時記錄元素移動中所有的坐標變化,不僅浪費大量的存儲空間來記錄狀態,而且真正應用撤銷功能的時候用戶也會崩潰,顯然這是不合理的,最好的方法就是通過函數防抖,監聽用戶一段時間內的操作,但只有當用戶當前單步操作停止後才會記錄,比如拖拽停止後記錄下元素放置的狀態。
2. 素材搜索框自動拉取
H5編輯器需要從素材庫拉取圖片素材,如果等用戶輸入完關鍵詞點擊搜索,效率太低,如果在搜索過程中實時拉取服務器數據,對服務器壓力又會過大,折中的方法就是使用函數防抖,當用戶輸入停頓一定時間後觸發 ajax
請求拉取數據。
3. 配置信息保存
越來越多的產品傾向於使用無保存按鈕的交互方式,用戶每操作完一步後自動提交請求保存,如果使用函數防抖,H5編輯器就可以減少例如計數器頻繁操作觸發的保存頻率。
4. 函數防抖節流的實現
實現防抖節流非常簡單,利用定時器就可以輕鬆實現。
4.1 函數防抖(debounce)實現
debounce
的實現非常簡單,需要在一定時間後執行,一個定時器輕鬆搞定,需要主要在啟動定時器時修改傳入函數的上下文環境。
// debounce 接受一個函數和延遲時間作為參數 const _.debounce = function (func, delay) { // 維護一個 timer let timer = null return function() { clearTimeout(timer) const context = this const args = arguments timer = setTimeout(function () { func.apply(context, args) }, delay) } }
4.2 函數節流(throttle)實現
throttle
需要在一定時間內只執行一次,有時間戳和定時器兩種簡單的實現方式:
1. 時間戳方式:
// 每次調用記錄當前時間,執行回調函數前比對間隔時間 const _.throttle = function (func, delay) { let prev = Date.now() return function () { const context = this const args = arguments const now = Date.now() if(now - prev >= delay){ func.apply(context, args) prev = Date.now() } } }
時間戳實現的節流函數會在第一次觸發時立即執行,並且最後一次觸發事件不會被執行
2. 定時器方式:
const _.throttle = function (func, delay) { let timer = null return function () { const context = this const args = arguments if (!timer) { timer = setTimeout(function () { func.apply(context, args) timer = null },delay) } } }
定時器實現的節流函數在第一次觸發時不會執行,delay 秒後才執行,並且當最後一次停止觸發後,還會再執行一次函數
4.3 函數調用
調用方法如下:
function foo () { console.log('trigger') } window.addEventListener('resize', debounce(foo, 2000)) window.addEventListener('resize', throttle(foo, 2000)) // 大部分場景下 resize 更適合使用節流
5. 總結
函數防抖節流的實現非常簡單,卻能解決前端開發過程中的很多問題,提升性能,優化用戶體驗,尤其是應對像H5編輯器這樣的交互複雜的前端項目更是不可或缺,在實際的工程項目中,防抖函數還是節流函數的選擇需要開發者針對不同的應用場景進行選擇。