本文來自個人博客,歡迎你們去GitHub上star個人博客css
本文從防抖和節流出發,分析它們的特性,並拓展一種特殊的節流方式requestAnimationFrame,最後對lodash中的debounce源碼進行分析html
防抖和節流是前端開發中常用的一種優化手段,它們都被用來控制一段時間內方法執行的次數,能夠爲咱們節省大量沒必要要的開銷前端
當咱們須要及時獲知窗口大小變化時,咱們會給window綁定一個resize函數,像下面這樣:node
window.addEventListener('resize', () => { console.log('resize') });
咱們會發現,即便是極小的縮放操做,也會打印數十次resize,也就是說,若是咱們須要在onresize函數中搞一些小動做,也會重複執行幾十次。但實際上,咱們只關心鼠標鬆開,窗口中止變化的那一次resize,這時候,就可使用debounce優化這個過程:git
const handleResize = debounce(() => { console.log('resize'); }, 500); window.addEventListener('resize', handleResize);
運行上面的代碼(你得有現成的debounce函數),在中止縮放操做500ms後,默認用戶無繼續操做了,纔會打印resizegithub
這就是防抖的功效,它把一組連續的調用變爲了一個,最大程度地優化了效率後端
再舉一個防抖的常見場景:api
搜索欄經常會根據咱們的輸入,向後端請求,獲取搜索候選項,顯示在搜索欄下方。若是咱們不使用防抖,在輸入「debounce」時前端會依次向後端請求"d"、"de"、"deb"..."debounce"的搜索候選項,在用戶輸入很快的狀況下,這些請求是無心義的,可使用防抖優化瀏覽器
觀察上面這兩個例子,咱們發現,防抖很是適於只關心結果,不關心過程如何的狀況,它能很好地將大量連續事件轉爲單個咱們須要的事件app
爲了更好理解,下面提供了最簡單的debounce實現:返回一個function,第一次執行這個function會啓動一個定時器,下一次執行會清除上一次的定時器並重起一個定時器,直到這個function再也不被調用,定時器成功跑完,執行回調函數
const debounce = function(func, wait) { let timer; return function() { !!timer && clearTimeout(timer); timer = setTimeout(func, wait); }; };
那若是咱們不只關心結果,同時也關心過程呢?
節流讓指定函數在規定的時間裏執行次數不會超過一次,也就是說,在連續高頻執行中,動做會被按期執行。節流的主要目的是將本來操做的頻率下降
實例:
咱們模擬一個可無限滾動的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(); } } }
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的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 = 0 var 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