摘要:防抖函數防抖和節流是一對常常被放在一起的場景。同時,這里會設置一個定時器,在等待后會執行,的主要作用就是觸發。最后,如果不再有函數調用,就會在定時器結束時執行。
函數節流和去抖的出現場景,一般都伴隨著客戶端 DOM 的事件監聽。比如scroll resize等事件,這些事件在某些場景觸發非常頻繁。
比如,實現一個原生的拖拽功能(不能用 H5 Drag&Drop API),需要一路監聽 mousemove 事件,在回調中獲取元素當前位置,然后重置 dom 的位置(樣式改變)。如果我們不加以控制,每移動一定像素而觸發的回調數量是會非常驚人的,回調中又伴隨著 DOM 操作,繼而引發瀏覽器的重排與重繪,性能差的瀏覽器可能就會直接假死,這樣的用戶體驗是非常糟糕的。
我們需要做的是降低觸發回調的頻率,比如讓它 500ms 觸發一次,或者 200ms,甚至 100ms,這個閾值不能太大,太大了拖拽就會失真,也不能太小,太小了低版本瀏覽器可能就會假死,這樣的解決方案就是函數節流,英文名字叫「throttle」。
函數節流的核心是,讓一個函數不要執行得太頻繁,減少一些過快的調用來節流。也就是在一段固定的時間內只觸發一次回調函數,即便在這段時間內某個事件多次被觸發也只觸發回調一次。
防抖(debounce)函數防抖(debounce)和節流是一對常常被放在一起的場景。防抖的原理是在事件被觸發n秒后再執行回調,如果在這n秒內又被觸發,則重新計時。也就是說事件來了,先setTimeout定個時,n秒后再去觸發回調函數。它和節流的不同在于如果某段時間內事件以間隔小于n秒的頻率執行,那么這段時間回調只會觸發一次。節流則是按照200ms或者300ms定時觸發,而不僅僅是一次。
兩者應用場景初看覺得兩個概念好像差不多啊,到底什么時候用節流什么時候用防抖呢?
防抖常用場景防抖的應用場景是連續的事件響應我們只觸發一次回調,比如下面的場景:
resize/scroll 觸發統計事件
文本輸入驗證,不用用戶輸一個文字調用一次ajax請求,隨著用戶的輸入驗證一次就可以
節流常用場景節流是個很公平的函數,隔一段時間就來觸發回調,比如下面的場景:
DOM 元素的拖拽功能實現(mousemove)
計算鼠標移動的距離(mousemove)
搜索聯想(keyup)
為什么這些適合節流而不是防抖呢?
我們想想哈,按照防抖的概念如果n秒內用戶連續不斷觸發事件,則防抖會在用戶結束操作結束后觸發回調。 那對于拖動來說,我拖了半天沒啥反應,一松手等n秒,啪。元素蹦過來了,這還是拖動嗎?這是跳動吧,2333;
function throttle(func, gapTime){ if(typeof func !== "function") { throw new TypeError("need a function"); } gapTime = +gapTime || 0; let lastTime = 0; return function() { let time = + new Date(); if(time - lastTime > gapTime || !lastTime) { func(); lastTime = time; } } } setInterval(throttle(() => { console.log("xxx") }, 1000),10)
如上,沒10ms觸發一次,但事實上是每1s打印一次 "xxx";
基本防抖實現弄清防抖的原理后,我們先來實現一個簡單的 debounce 函數。
// 我的debounce 實現 function my_debounce(func, wait) { if(typeof func !== "function") { throw new TypeError("need a function"); } wait = +wait || 0; let timeId = null; return function() { // console.log("滾動了滾動了"); // 測試時可放開 const self = this; const args = arguments; if(timeId) { clearTimeout(timeId); // 清除定時器,重新設定一個新的定時器 } timeId = setTimeout(() => { func.apply(self, args); // arguments 是傳給函數的參數,這里是 event 對象 }, wait); } }
我們來分析一下這個函數, 首先它是一個閉包。它的核心是 定時器的設置,如果第一次進來, timeId 不存在直接設置一個延遲 wait 毫秒的定時器; 如果timeId 已經存在則先清除定時器再 重新設置延遲。
如上所說,如果在延遲時間內 來了一個事件,則從這個事件到來的時候開始定時。
用該防抖函數試一下對scroll去抖效果。我在 防抖函數中放開日志 console.log("滾動了滾動了");, 然后
對滾動添加事件響應.
function onScroll_1() { console.log("執行滾動處理函數啦"); } window.addEventListener("scroll", my_debounce(onScroll_1, 1000));
打開頁面,不斷滾動可以在控制臺看到如下圖的console.
從圖中可以看出,我觸發了90次滾動響應,但實際上 滾動處理函數執行了一次。
嗯,對上面簡單的例子我們分析下不同情況下,4秒時間內防抖調用的時機和次數.
每隔1.5秒滾動一次,4秒內等待1秒觸發的情況下,會調用響應函數 2次
每隔 0.5 秒滾動一次,4秒內等待1秒觸發的情況下,一次也不會調用。
下圖展示了這兩種情況下定時器設置和函數調用情況(費死個猴勁畫的,湊合看不清楚的可以留言)
從上面的分析來看,這個單純的 防抖函數還是有個硬傷的,是什么呢?
那就是每次觸發定時器就重新來,每次都重新來,如果某段時間用戶一直一直觸發,防抖函數一直重新設置定時器,就是不執行,頻繁的延遲會導致用戶遲遲得不到響應,用戶同樣會產生“這個頁面卡死了”的觀感。
既然如此,那我們是不是可以設置一個最常等待時間,超過這個事件不管還有沒有事件在觸發,就去執行函數呢?或者我可不可以設置第一次觸發的時候立即執行函數,再次觸發的時候再去防抖,也就是說不管如何先 響應一次,告訴那些 心急的 用戶我響應你啦,我是正常的,接下來慢慢來哦~
答案是,都是可以的。這些屬于更自由的配置,加上這些, debounce 就是一個成熟的防抖函數了。嗯,是噠~成熟的
既然說到成熟,咱們還是來看下大名鼎鼎的==lodash==庫是怎么將 debounce 成熟的吧!
loadsh中debounce源碼解讀為了方便,我們忽略lodash 開始對function的注釋完里整版在這 。成熟的 debounce 也才 100多行而已,小場面~~
先來看下完整函數,里面加上了我自己的理解,然后再詳細分析
function debounce(func, wait, options) { let lastArgs, // debounced 被調用后被賦值,表示至少調用 debounced一次 lastThis, // 保存 this maxWait, // 最大等待時間 result, // return 的結果,可能一直為 undefined,沒看到特別的作用 timerId, // 定時器句柄 lastCallTime // 上一次調用 debounced 的時間,按上面例子可以理解為 上一次觸發 scroll 的時間 let lastInvokeTime = 0 // 上一次執行 func 的時間,按上面例子可以理解為 上次 執行 時的時間 let leading = false // 是否第一次觸發時立即執行 let maxing = false // 是否有最長等待時間 let trailing = true // 是否在等待周期結束后執行用戶傳入的函數 // window.requestAnimationFrame() 方法告訴瀏覽器您希望執行動畫并請求瀏覽器在下一次重繪之前調用指定的函數來更新動畫。該方法使用一個回調函數作為參數,這個回調函數會在瀏覽器重繪之前調用。 // 下面的代碼我先注釋,可以先不關注~意思是沒傳 wait 時 會在某個時候 調用 window.requestAnimationFrame() // 以上代碼被我注釋,可以先不關注 // 這個很好理解,如果傳入的 func 不是函數,拋出錯誤,老子干不了這樣的活 if (typeof func != "function") { throw new TypeError("Expected a function") } wait = +wait || 0 if (isObject(options)) { leading = !!options.leading maxing = "maxWait" in options maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait trailing = "trailing" in options ? !!options.trailing : trailing } // 執行 用戶傳入的 func // 重置 lastArgs,lastThis // lastInvokeTime 在此時被賦值,記錄上一次調用 func的時間 function invokeFunc(time) { const args = lastArgs const thisArg = lastThis lastArgs = lastThis = undefined lastInvokeTime = time result = func.apply(thisArg, args) return result } // setTimeout 一個定時器 function startTimer(pendingFunc, wait) { // 先不關注這個 //if (useRAF) { //return root.requestAnimationFrame(pendingFunc) //} return setTimeout(pendingFunc, wait) } // 清除定時器 function cancelTimer(id) { // 先不關注 //if (useRAF) { //return root.cancelAnimationFrame(id) //} clearTimeout(id) } // 防抖開始時執行的操作 // lastInvokeTime 在此時被賦值,記錄上一次調用 func的時間 // 設置了立即執行func,則執行func, 否則設置定時器 function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time // Start the timer for the trailing edge. timerId = startTimer(timerExpired, wait) // Invoke the leading edge. return leading ? invokeFunc(time) : result } // 計算還需要等待多久 // 沒設置最大等待時間,結果為 wait - (當前時間 - 上一次觸發(scroll) ) 時間,也就是 wait - 已經等候時間 // 設置了最長等待時間,結果為 最長等待時間 和 按照wait 計算還需要等待時間 的最小值 function remainingWait(time) { const timeSinceLastCall = time - lastCallTime const timeSinceLastInvoke = time - lastInvokeTime const timeWaiting = wait - timeSinceLastCall return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting } // 此時是否應該設置定時器/執行用戶傳入的函數,有四種情況應該執行 // 1, 第一次觸發(scroll) // 2. 距離上次觸發超過 wait, 參考上面例子中 1.5 秒觸發一次,在3s觸發的情況 // 3.當前時間小于 上次觸發時間,大概是系統時間被人為往后撥了,本來2018年,系統時間變為 2017年了,嘎嘎嘎 // 4. 設置了最長等待時間,并且等待時長不小于 最長等待時間了~ 參考上面例子,如果maxWait 為2s, 則在 2s scroll 時會執行 function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime const timeSinceLastInvoke = time - lastInvokeTime return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)) } // 執行函數呢 還是繼續設置定時器呢? 防抖的核心 // 時間滿足條件,執行 // 否則 重新設置定時器 function timerExpired() { const time = Date.now() if (shouldInvoke(time)) { return trailingEdge(time) } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time)) } // 執行用戶傳入的 func 之前的最后一道屏障 func os: 執行我一次能咋地,這么多屏障? // 重置 定時器 // 執行 func // 重置 lastArgs = lastThis 為 undefined function trailingEdge(time) { timerId = undefined // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time) } lastArgs = lastThis = undefined return result } // 取消防抖 // 重置所有變量 清除定時器 function cancel() { if (timerId !== undefined) { cancelTimer(timerId) } lastInvokeTime = 0 lastArgs = lastCallTime = lastThis = timerId = undefined } // 定時器已存在,去執行 嗯,我就是這么強勢 function flush() { return timerId === undefined ? result : trailingEdge(Date.now()) } // 是否正在 等待中 function pending() { return timerId !== undefined } // 正房來了! 這是入口函數,在這里運籌帷幄,根據敵情調配各個函數,勢必騙過用戶那個傻子,我沒有一直在執行但你以為我一直在響應你哦 function debounced(...args) { const time = Date.now() const isInvoking = shouldInvoke(time) lastArgs = args lastThis = this lastCallTime = time if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime) } if (maxing) { // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait) return invokeFunc(lastCallTime) } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait) } return result } debounced.cancel = cancel debounced.flush = flush debounced.pending = pending // 下面這句話證明 debounced 我是入口函數,是正宮娘娘! return debounced } export default debounce
第一看是不是有點暈?沒關系,我們結合例子理一遍 這個成熟的 debounce 是如何運作的。
用demo 理解 loadsh debounce調用如下:
function onScroll_1() { console.log("執行滾動處理函數啦"); } window.addEventListener("scroll", debounce(onScroll_1, 1000));
每 1500 ms 觸發(scroll)一次
每 600 ms 觸發(scroll)一次
再來看一下入口函數 debounced。
function debounced(...args) { const time = Date.now() const isInvoking = shouldInvoke(time) lastArgs = args // args 是 event 對象,是點擊、scroll等事件傳過來的 lastThis = this lastCallTime = time if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime) } if (maxing) { // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait) return invokeFunc(lastCallTime) } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait) } return result }
1500ms時scroll,開始執行 debounced:
首先判斷shouldInvoke(time),因為第一次 lastCallTime === undefined 所以返回true;
并且此時 timerId === undefined, 所以執行 leadingEdge(lastCallTime);
在 leadingEdge(lastCallTime) 函數中,設置 lastInvokeTime = time,這個挺關鍵的,并且設定一個 1000ms的定時器,如果leading 為true,則invokefunc,我們沒有設置leading這種情況不表~
1500ms~2500ms 之間沒什么事,定時器到點,執行 invokeFunc(time);
在 invokeFunc 中再次設置 lastInvokeTime, 并重置 lastThis,lastArgs;
第一次 scroll 完畢,接下來是 3000ms,這種間隔很大的調用與單純的 debounce 沒有太大差別,4s結束會執行 2次。
每 600ms 執行一次:
先用文字描述吧:
首次進入函數時因為 lastCallTime === undefined 并且 timerId === undefined,所以會執行 leadingEdge,如果此時 leading 為 true 的話,就會執行 func。同時,這里會設置一個定時器,在等待 wait(s) 后會執行 timerExpired,timerExpired 的主要作用就是觸發 trailing。
如果在還未到 wait 的時候就再次調用了函數的話,會更新 lastCallTime,并且因為此時 isInvoking 不滿足條件,所以這次什么也不會執行。
時間到達 wait 時,就會執行我們一開始設定的定時器timerExpired,此時因為time-lastCallTime < wait,所以不會執行 trailingEdge。
這時又會新增一個定時器,下一次執行的時間是 remainingWait,這里會根據是否有 maxwait 來作區分:
如果沒有 maxwait,定時器的時間是 wait - timeSinceLastCall,保證下一次 trailing 的執行。
如果有 maxing,會比較出下一次 maxing 和下一次 trailing 的最小值,作為下一次函數要執行的時間。
最后,如果不再有函數調用,就會在定時器結束時執行 trailingEdge。
簡單畫了個以時間為軸,函數執行的情況:
看不懂的多看兩遍吧~~~
在沒配置其他參數的情況下,連續觸發也是不執行,那我們增加一下 maxWait試一下:
function onScroll_1() { console.log("執行滾動處理函數啦"); } window.addEventListener("scroll", debounce(onScroll_1, 1000, { maxWait: 1000 }));
文字描述過程:
首次進入函數時因為 lastCallTime === undefined 并且 timerId === undefined,所以會執行 leadingEdge,這里會設置一個定時器,在等待 wait(s) 后會執行 timerExpired,timerExpired 的主要作用就是觸發 trailing。
如果在還未到 wait 的時候就再次調用了函數的話,會更新 lastCallTime,并且因為此時 isInvoking 不滿足條件,所以這次什么也不會執行。
時間到達 wait 時,就會執行我們一開始設定的定時器timerExpired,此時因為time-lastCallTime < wait,如果所以不會執行 trailingEdge。但是如果設置了maxWait,這里還會判斷 time-lastInvokeTime > maxWait,(參考上圖中1600ms處,會執行) 如果是則 trailingEdge。
這時又會新增一個定時器,下一次執行的時間是 remainingWait,這里會根據是否有 maxwait 來作區分:
如果沒有 maxwait,定時器的時間是 wait - timeSinceLastCall,保證下一次 trailing 的執行。
如果有 maxing,會比較出下一次 maxing 和下一次 trailing 的最小值,作為下一次函數要執行的時間。
最后,如果不再有函數調用,就會在定時器結束時執行 trailingEdge
常見問題,防抖函數如何傳參其實糾結這個問題的同學,看看函數式編程會理解一些~
其實很簡單,my_debounce會返回一個函數,那在函數調用時加上參數就OK了~~
window.addEventListener("scroll", my_debounce(onScroll_1, 1000)("test"));
我們的 onScroll_1 這樣寫,就把"test" 傳給 params了。。
function onScroll_1(params) { console.log("onScroll_1", params); // test console.log("執行滾動處理函數啦"); }
不過一般我們不會這樣寫吧,因為新傳的值會將 原來的 event 給覆蓋掉,也就拿不到 scroll 或者 mouseclick等事件對象 event 了~~
那你說,我既想獲取到 event對象,又想傳參,怎么辦?
我的辦法是,在自己的監聽函數上動手腳,比如我的onScroll 函數這樣寫:
function onScroll(param) { console.log("param:", param); // test return function(event) { console.log("event:", event); // event } }
如下這樣使用 debounce
window.addEventListener("scroll", my_debounce(onScroll("test"), 1000));
控制臺的日志確實如此~~
有了 debounce的基礎loadsh對throttle的實現就非常簡單了,就是一個傳了 maxWait的debounce.
function throttle(func, wait, options) { let leading = true let trailing = true if (typeof func != "function") { throw new TypeError("Expected a function") } if (isObject(options)) { leading = "leading" in options ? !!options.leading : leading trailing = "trailing" in options ? !!options.trailing : trailing } return debounce(func, wait, { "leading": leading, "maxWait": wait, "trailing": trailing }) }
上面已經分析了這種情況,它的結果是如果連續不斷觸發則每隔 wait 秒執行一次func。
參考資料輕松理解防抖
節流 防抖 場景辨析
從lodash源碼學習節流和防抖
聊聊lodash的debounce實現
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/99654.html
摘要:譯通過實例講解和防抖與節流源碼中推薦的文章,為了學習英語,翻譯了一下原文鏈接作者本文來自一位倫敦前端工程師的技術投稿。首次或立即你可能發現防抖事件在等待觸發事件執行,直到事件都結束后它才執行。 [譯]通過實例講解Debouncing和Throtting(防抖與節流) lodash源碼中推薦的文章,為了學習(英語),翻譯了一下~ 原文鏈接 作者:DAVID CORBACHO 本文來自一位...
摘要:最簡單的案例以最簡單的情景為例在某一時刻點只調用一次函數,那么將在時間后才會真正觸發函數。后續我們會逐漸增加黑色鬧鐘出現的復雜度,不斷去分析紅色鬧鐘的位置。 序 相比網上教程中的 debounce 函數,lodash 中的 debounce 功能更為強大,相應的理解起來更為復雜; 解讀源碼一般都是直接拿官方源碼來解讀,不過這次我們采用另外的方式:從最簡單的場景開始寫代碼,然后慢慢往源碼...
摘要:如果使用的是防抖,那么得等我們停止滾動之后一段時間才會加載新的內容,沒有那種無限滾動的流暢感。這時候,我們就可以使用節流,將事件有效觸發的頻率降低的同時給用戶流暢的瀏覽體驗。調用,瀏覽器會在下次刷新的時候執行指定回調函數。 本文來自我的博客,歡迎大家去GitHub上star我的博客 本文從防抖和節流出發,分析它們的特性,并拓展一種特殊的節流方式requestAnimationFrame...
摘要:首先重置防抖函數最后調用時間,然后去觸發一個定時器,保證后接下來的執行。這就避免了手動管理定時器。 ??之前遇到過一個場景,頁面上有幾個d3.js繪制的圖形。如果調整瀏覽器可視區大小,會引發圖形重繪。當圖中的節點比較多的時候,頁面會顯得異常卡頓。為了限制類似于這種短時間內高頻率觸發的情況,我們可以使用防抖函數。 ??實際開發過程中,這樣的情況其實很多,比如: 頁面的scroll事件 ...
閱讀 2327·2021-09-22 15:27
閱讀 3180·2021-09-03 10:32
閱讀 3510·2021-09-01 11:38
閱讀 2505·2019-08-30 15:56
閱讀 2221·2019-08-30 13:01
閱讀 1546·2019-08-29 12:13
閱讀 1429·2019-08-26 13:33
閱讀 900·2019-08-26 13:30