探究防抖(debounce)和节流(throttle)
本文來自我的博客,歡迎大家去GitHub上star我的博客
本文從防抖和節流出發,分析它們的特性,并拓展一種特殊的節流方式requestAnimationFrame,最后對lodash中的debounce源碼進行分析
防抖和節流是前端開發中經常使用的一種優化手段,它們都被用來控制一段時間內方法執行的次數,可以為我們節省大量不必要的開銷
防抖(debounce)
當我們需要及時獲知窗口大小變化時,我們會給window綁定一個resize函數,像下面這樣:
window.addEventListener('resize', () => {console.log('resize') });我們會發現,即使是極小的縮放操作,也會打印數十次resize,也就是說,如果我們需要在onresize函數中搞一些小動作,也會重復執行幾十次。但實際上,我們只關心鼠標松開,窗口停止變化的那一次resize,這時候,就可以使用debounce優化這個過程:
const handleResize = debounce(() => {console.log('resize'); }, 500); window.addEventListener('resize', handleResize);運行上面的代碼(你得有現成的debounce函數),在停止縮放操作500ms后,默認用戶無繼續操作了,才會打印resize
這就是防抖的功效,它把一組連續的調用變為了一個,最大程度地優化了效率
再舉一個防抖的常見場景:
搜索欄常常會根據我們的輸入,向后端請求,獲取搜索候選項,顯示在搜索欄下方。如果我們不使用防抖,在輸入“debounce”時前端會依次向后端請求"d"、"de"、"deb"..."debounce"的搜索候選項,在用戶輸入很快的情況下,這些請求是無意義的,可以使用防抖優化
觀察上面這兩個例子,我們發現,防抖非常適于只關心結果,不關心過程如何的情況,它能很好地將大量連續事件轉為單個我們需要的事件
為了更好理解,下面提供了最簡單的debounce實現:返回一個function,第一次執行這個function會啟動一個定時器,下一次執行會清除上一次的定時器并重起一個定時器,直到這個function不再被調用,定時器成功跑完,執行回調函數
const debounce = function(func, wait) {let timer;return function() {!!timer && clearTimeout(timer);timer = setTimeout(func, wait);}; };那如果我們不僅關心結果,同時也關心過程呢?
節流(throttle)
節流讓指定函數在規定的時間里執行次數不會超過一次,也就是說,在連續高頻執行中,動作會被定期執行。節流的主要目的是將原本操作的頻率降低
實例:
我們模擬一個可無限滾動的feed流
html:
<div id="wrapper"><div class="feed"></div><div class="feed"></div><div class="feed"></div><div class="feed"></div><div class="feed"></div> </div>css:
#wrapper {height: 500px;overflow: auto; } .feed {height: 200px;background: #ededed;margin: 20px; }js:
const wrapper = document.getElementById("wrapper"); const loadContent = () => {const {scrollHeight,clientHeight,scrollTop} = wrapper;const heightFromBottom = scrollHeight - scrollTop - clientHeight;if (heightFromBottom < 200) {const wrapperCopy = wrapper.cloneNode(true);const children = [].slice.call(wrapperCopy.children);children.forEach(item => {wrapper.appendChild(item);})} } const handleScroll = throttle(loadContent, 200); wrapper.addEventListener("scroll", handleScroll);可以看到,在這個例子中,我們需要不停地獲取滾動條距離底部的高度,以判斷是否需要增加新的內容。我們知道,srcoll同樣也是種會高頻觸發的事件,我們需要減少它有效觸發的次數。如果使用的是防抖,那么得等我們停止滾動之后一段時間才會加載新的內容,沒有那種無限滾動的流暢感。這時候,我們就可以使用節流,將事件有效觸發的頻率降低的同時給用戶流暢的瀏覽體驗。在這個例子中,我們指定throttle的wait值為200ms,也就是說,如果你一直在滾動頁面,loadCotent函數也只會每200ms執行一次
同樣,這里有throttle最簡單的實現,當然,這種實現很粗糙,有不少缺陷(比如沒有考慮最后一次執行),只供初步理解使用:
const throttle = function (func, wait) {let lastTime;return function () {const curTime = Date.now();if (!lastTime || curTime - lastTime >= wait) {lastTime = curTime;return func();}} }requestAnimationFrame(rAF)
rAF在一定程度上和throttle(func,16)的作用相似,但它是瀏覽器自帶的api,所以,它比throttle函數執行得更加平滑。調用window.requestAnimationFrame(),瀏覽器會在下次刷新的時候執行指定回調函數。通常,屏幕的刷新頻率是60hz,所以,這個函數也就是大約16.7ms執行一次。如果你想讓你的動畫更加平滑,用rAF就再好不過了,因為它是跟著屏幕的刷新頻率來的
rAF的寫法與debounce和throttle不同,如果你想用它繪制動畫,需要不停地在回調函數里調用自身,具體寫法可以參考mdn
rAF支持ie10及以上瀏覽器,不過因為是瀏覽器自帶的api,我們也就無法在node中使用它了
總結
debounce將一組事件的執行轉為最后一個事件的執行,如果你只關注結果,debounce再適合不過
如果你同時關注過程,可以使用throttle,它可以用來降低高頻事件的執行頻率
如果你的代碼是在瀏覽器上運行,不考慮兼容ie10,并且要求頁面上的變化盡可能的平滑,可以使用rAF
參考:https://css-tricks.com/debouncing-throttling-explained-examples/
附:lodash源碼解析
lodash的debounce功能十分強大,集debounce、throttle和rAF于一身,所以我特意研讀一下,下面是我的解析(我刪去了一些不重要的代碼,比如debounced的cancel方法):
function debounce(func, wait, options) {/*** lastCallTime是上一次執行debounced函數的時間* lastInvokeTime是上一次調用func的時間*/let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;let lastInvokeTime = 0;let leading = false;let maxing = false;let trailing = true;/*** 如果沒設置wait且raf可用 則默認使用raf*/const useRAF =!wait && wait !== 0 && typeof root.requestAnimationFrame === "function";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*/function invokeFunc(time) {const args = lastArgs;const thisArg = lastThis;lastArgs = lastThis = undefined;/*** 更新lastInvokeTime*/lastInvokeTime = time;result = func.apply(thisArg, args);return result;}/*** 調用定時器*/function startTimer(pendingFunc, wait) {if (useRAF) {root.cancelAnimationFrame(timerId);return root.requestAnimationFrame(pendingFunc);}return setTimeout(pendingFunc, wait);}/*** 在每輪debounce開始調用*/function leadingEdge(time) {lastInvokeTime = time;timerId = startTimer(timerExpired, wait);return leading ? invokeFunc(time) : result;}/*** 計算剩余時間* 1是 wait 減去 距離上次調用debounced時間(lastCallTime)* 2是 maxWait 減去 距離上次調用func時間(lastInvokeTime)* 1和2取最小值*/function remainingWait(time) {const timeSinceLastCall = time - lastCallTime;const timeSinceLastInvoke = time - lastInvokeTime;const timeWaiting = wait - timeSinceLastCall;return maxing? Math.min(timeWaiting, maxWait - timeSinceLastInvoke): timeWaiting;}/*** 判斷是否需要執行*/function shouldInvoke(time) {const timeSinceLastCall = time - lastCallTime;const timeSinceLastInvoke = time - lastInvokeTime;/*** 4種情況返回true,否則返回false* 1.第一次調用* 2.距離上次調用debounced時間(lastCallTime)>=wait* 3.系統時間倒退* 4.設置了maxWait,距離上次調用func時間(lastInvokeTime)>=maxWait*/return (lastCallTime === undefined ||timeSinceLastCall >= wait ||timeSinceLastCall < 0 ||(maxing && timeSinceLastInvoke >= maxWait));}/*** 通過shouldInvoke函數判斷是否執行* 執行:調用trailingEdge函數* 不執行:調用startTimer函數重新開始timer,wait值通過remainingWait函數計算*/function timerExpired() {const time = Date.now();if (shouldInvoke(time)) {return trailingEdge(time);}// Restart the timer.timerId = startTimer(timerExpired, remainingWait(time));}/*** 在每輪debounce結束調用*/function trailingEdge(time) {timerId = undefined;/*** trailing為true且lastArgs不為undefined時調用*/if (trailing && lastArgs) {return invokeFunc(time);}lastArgs = lastThis = undefined;return result;}function debounced(...args) {const time = Date.now();const isInvoking = shouldInvoke(time);lastArgs = args;lastThis = this;/*** 更新lastCallTime*/lastCallTime = time;if (isInvoking) {/*** 第一次調用*/if (timerId === undefined) {return leadingEdge(lastCallTime);}/*** 【注1】*/if (maxing) {timerId = startTimer(timerExpired, wait);return invokeFunc(lastCallTime);}}/*** 【注2】*/if (timerId === undefined) {timerId = startTimer(timerExpired, wait);}return result;}return debounced; }推薦是從返回的方法debounced開始,順著執行順序閱讀,理解起來更輕松
【注1】一開始我沒看明白if(maxing)里面這段代碼的作用,按理說,是不會執行這段代碼的,后來我去lodash的倉庫里看了test文件,發現對這段代碼,專門有一個case對其測試。我剝除了一些代碼,并修改了測試用例以便展示,如下:
var limit = 320,withCount = 0var withMaxWait = debounce(function () {console.log('invoke');withCount++; }, 64, {'maxWait': 128 });var start = +new Date; while ((new Date - start) < limit) {withMaxWait(); }執行代碼,打印了3次invoke;我又將if(maxing){}這段代碼注釋,再執行代碼,結果只打印了1次。結合源碼的英文注釋Handle invocations in a tight loop,我們不難理解,原本理想的執行順序是withMaxWait->timer->withMaxWait->timer這種交替進行,但由于setTimeout需等待主線程的代碼執行完畢,所以這種短時間快速調用就會導致withMaxWait->withMaxWait->timer->timer,從第二個timer開始,由于lastArgs被置為undefined,也就不會再調用invokeFunc函數,所以只會打印一次invoke。
同時,由于每次執行invokeFunc時都會將lastArgs置為undefined,在執行trailingEdge時會對lastArgs進行判斷,確保不會出現執行了if(maxing){}中的invokeFunc函數又執行了timer的invokeFunc函數
這段代碼保證了設置maxWait參數后的正確性和時效性
【注2】執行過一次trailingEdge后,再執行debounced函數,可能會遇到shouldInvoke返回false的情況,需單獨處理
【注3】對于lodash的debounce來說,throttle是一種leading為true且maxWait等于wait的特殊debounce
總結
以上是生活随笔為你收集整理的探究防抖(debounce)和节流(throttle)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 组件生命周期管理和通信方案
- 下一篇: HTTP代理ip的这些误区你知道吗?