摘要:如果使用的是防抖,那么得等我們停止?jié)L動(dòng)之后一段時(shí)間才會(huì)加載新的內(nèi)容,沒有那種無(wú)限滾動(dòng)的流暢感。這時(shí)候,我們就可以使用節(jié)流,將事件有效觸發(fā)的頻率降低的同時(shí)給用戶流暢的瀏覽體驗(yàn)。調(diào)用,瀏覽器會(huì)在下次刷新的時(shí)候執(zhí)行指定回調(diào)函數(shù)。
本文來自我的博客,歡迎大家去GitHub上star我的博客
本文從防抖和節(jié)流出發(fā),分析它們的特性,并拓展一種特殊的節(jié)流方式requestAnimationFrame,最后對(duì)lodash中的debounce源碼進(jìn)行分析
防抖和節(jié)流是前端開發(fā)中經(jīng)常使用的一種優(yōu)化手段,它們都被用來控制一段時(shí)間內(nèi)方法執(zhí)行的次數(shù),可以為我們節(jié)省大量不必要的開銷
防抖(debounce)當(dāng)我們需要及時(shí)獲知窗口大小變化時(shí),我們會(huì)給window綁定一個(gè)resize函數(shù),像下面這樣:
window.addEventListener("resize", () => { console.log("resize") });
我們會(huì)發(fā)現(xiàn),即使是極小的縮放操作,也會(huì)打印數(shù)十次resize,也就是說,如果我們需要在onresize函數(shù)中搞一些小動(dòng)作,也會(huì)重復(fù)執(zhí)行幾十次。但實(shí)際上,我們只關(guān)心鼠標(biāo)松開,窗口停止變化的那一次resize,這時(shí)候,就可以使用debounce優(yōu)化這個(gè)過程:
const handleResize = debounce(() => { console.log("resize"); }, 500); window.addEventListener("resize", handleResize);
運(yùn)行上面的代碼(你得有現(xiàn)成的debounce函數(shù)),在停止縮放操作500ms后,默認(rèn)用戶無(wú)繼續(xù)操作了,才會(huì)打印resize
這就是防抖的功效,它把一組連續(xù)的調(diào)用變?yōu)榱艘粋€(gè),最大程度地優(yōu)化了效率
再舉一個(gè)防抖的常見場(chǎng)景:
搜索欄常常會(huì)根據(jù)我們的輸入,向后端請(qǐng)求,獲取搜索候選項(xiàng),顯示在搜索欄下方。如果我們不使用防抖,在輸入“debounce”時(shí)前端會(huì)依次向后端請(qǐng)求"d"、"de"、"deb"..."debounce"的搜索候選項(xiàng),在用戶輸入很快的情況下,這些請(qǐng)求是無(wú)意義的,可以使用防抖優(yōu)化
觀察上面這兩個(gè)例子,我們發(fā)現(xiàn),防抖非常適于只關(guān)心結(jié)果,不關(guān)心過程如何的情況,它能很好地將大量連續(xù)事件轉(zhuǎn)為單個(gè)我們需要的事件
為了更好理解,下面提供了最簡(jiǎn)單的debounce實(shí)現(xiàn):返回一個(gè)function,第一次執(zhí)行這個(gè)function會(huì)啟動(dòng)一個(gè)定時(shí)器,下一次執(zhí)行會(huì)清除上一次的定時(shí)器并重起一個(gè)定時(shí)器,直到這個(gè)function不再被調(diào)用,定時(shí)器成功跑完,執(zhí)行回調(diào)函數(shù)
const debounce = function(func, wait) { let timer; return function() { !!timer && clearTimeout(timer); timer = setTimeout(func, wait); }; };
那如果我們不僅關(guān)心結(jié)果,同時(shí)也關(guān)心過程呢?
節(jié)流(throttle)節(jié)流讓指定函數(shù)在規(guī)定的時(shí)間里執(zhí)行次數(shù)不會(huì)超過一次,也就是說,在連續(xù)高頻執(zhí)行中,動(dòng)作會(huì)被定期執(zhí)行。節(jié)流的主要目的是將原本操作的頻率降低
實(shí)例:
我們模擬一個(gè)可無(wú)限滾動(dòng)的feed流
html:
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);
可以看到,在這個(gè)例子中,我們需要不停地獲取滾動(dòng)條距離底部的高度,以判斷是否需要增加新的內(nèi)容。我們知道,srcoll同樣也是種會(huì)高頻觸發(fā)的事件,我們需要減少它有效觸發(fā)的次數(shù)。如果使用的是防抖,那么得等我們停止?jié)L動(dòng)之后一段時(shí)間才會(huì)加載新的內(nèi)容,沒有那種無(wú)限滾動(dòng)的流暢感。這時(shí)候,我們就可以使用節(jié)流,將事件有效觸發(fā)的頻率降低的同時(shí)給用戶流暢的瀏覽體驗(yàn)。在這個(gè)例子中,我們指定throttle的wait值為200ms,也就是說,如果你一直在滾動(dòng)頁(yè)面,loadCotent函數(shù)也只會(huì)每200ms執(zhí)行一次
同樣,這里有throttle最簡(jiǎn)單的實(shí)現(xiàn),當(dāng)然,這種實(shí)現(xiàn)很粗糙,有不少缺陷(比如沒有考慮最后一次執(zhí)行),只供初步理解使用:
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函數(shù)執(zhí)行得更加平滑。調(diào)用window.requestAnimationFrame(),瀏覽器會(huì)在下次刷新的時(shí)候執(zhí)行指定回調(diào)函數(shù)。通常,屏幕的刷新頻率是60hz,所以,這個(gè)函數(shù)也就是大約16.7ms執(zhí)行一次。如果你想讓你的動(dòng)畫更加平滑,用rAF就再好不過了,因?yàn)樗歉聊坏乃⑿骂l率來的
rAF的寫法與debounce和throttle不同,如果你想用它繪制動(dòng)畫,需要不停地在回調(diào)函數(shù)里調(diào)用自身,具體寫法可以參考mdn
rAF支持ie10及以上瀏覽器,不過因?yàn)槭菫g覽器自帶的api,我們也就無(wú)法在node中使用它了
總結(jié)debounce將一組事件的執(zhí)行轉(zhuǎn)為最后一個(gè)事件的執(zhí)行,如果你只關(guān)注結(jié)果,debounce再適合不過
如果你同時(shí)關(guān)注過程,可以使用throttle,它可以用來降低高頻事件的執(zhí)行頻率
如果你的代碼是在瀏覽器上運(yùn)行,不考慮兼容ie10,并且要求頁(yè)面上的變化盡可能的平滑,可以使用rAF
參考:https://css-tricks.com/debouncing-throttling-explained-examples/
附:lodash源碼解析lodash的debounce功能十分強(qiáng)大,集debounce、throttle和rAF于一身,所以我特意研讀一下,下面是我的解析(我刪去了一些不重要的代碼,比如debounced的cancel方法):
function debounce(func, wait, options) { /** * lastCallTime是上一次執(zhí)行debounced函數(shù)的時(shí)間 * lastInvokeTime是上一次調(diào)用func的時(shí)間 */ let lastArgs, lastThis, maxWait, result, timerId, lastCallTime; let lastInvokeTime = 0; let leading = false; let maxing = false; let trailing = true; /** * 如果沒設(shè)置wait且raf可用 則默認(rèn)使用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; } /** * 執(zhí)行func */ function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; /** * 更新lastInvokeTime */ lastInvokeTime = time; result = func.apply(thisArg, args); return result; } /** * 調(diào)用定時(shí)器 */ function startTimer(pendingFunc, wait) { if (useRAF) { root.cancelAnimationFrame(timerId); return root.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } /** * 在每輪debounce開始調(diào)用 */ function leadingEdge(time) { lastInvokeTime = time; timerId = startTimer(timerExpired, wait); return leading ? invokeFunc(time) : result; } /** * 計(jì)算剩余時(shí)間 * 1是 wait 減去 距離上次調(diào)用debounced時(shí)間(lastCallTime) * 2是 maxWait 減去 距離上次調(diào)用func時(shí)間(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; } /** * 判斷是否需要執(zhí)行 */ function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; /** * 4種情況返回true,否則返回false * 1.第一次調(diào)用 * 2.距離上次調(diào)用debounced時(shí)間(lastCallTime)>=wait * 3.系統(tǒng)時(shí)間倒退 * 4.設(shè)置了maxWait,距離上次調(diào)用func時(shí)間(lastInvokeTime)>=maxWait */ return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait) ); } /** * 通過shouldInvoke函數(shù)判斷是否執(zhí)行 * 執(zhí)行:調(diào)用trailingEdge函數(shù) * 不執(zhí)行:調(diào)用startTimer函數(shù)重新開始timer,wait值通過remainingWait函數(shù)計(jì)算 */ function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time)); } /** * 在每輪debounce結(jié)束調(diào)用 */ function trailingEdge(time) { timerId = undefined; /** * trailing為true且lastArgs不為undefined時(shí)調(diào)用 */ 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) { /** * 第一次調(diào)用 */ 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開始,順著執(zhí)行順序閱讀,理解起來更輕松
【注1】一開始我沒看明白if(maxing)里面這段代碼的作用,按理說,是不會(huì)執(zhí)行這段代碼的,后來我去lodash的倉(cāng)庫(kù)里看了test文件,發(fā)現(xiàn)對(duì)這段代碼,專門有一個(gè)case對(duì)其測(cè)試。我剝除了一些代碼,并修改了測(cè)試用例以便展示,如下:
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(); }
執(zhí)行代碼,打印了3次invoke;我又將if(maxing){}這段代碼注釋,再執(zhí)行代碼,結(jié)果只打印了1次。結(jié)合源碼的英文注釋Handle invocations in a tight loop,我們不難理解,原本理想的執(zhí)行順序是withMaxWait->timer->withMaxWait->timer這種交替進(jìn)行,但由于setTimeout需等待主線程的代碼執(zhí)行完畢,所以這種短時(shí)間快速調(diào)用就會(huì)導(dǎo)致withMaxWait->withMaxWait->timer->timer,從第二個(gè)timer開始,由于lastArgs被置為undefined,也就不會(huì)再調(diào)用invokeFunc函數(shù),所以只會(huì)打印一次invoke。
同時(shí),由于每次執(zhí)行invokeFunc時(shí)都會(huì)將lastArgs置為undefined,在執(zhí)行trailingEdge時(shí)會(huì)對(duì)lastArgs進(jìn)行判斷,確保不會(huì)出現(xiàn)執(zhí)行了if(maxing){}中的invokeFunc函數(shù)又執(zhí)行了timer的invokeFunc函數(shù)
這段代碼保證了設(shè)置maxWait參數(shù)后的正確性和時(shí)效性
【注2】執(zhí)行過一次trailingEdge后,再執(zhí)行debounced函數(shù),可能會(huì)遇到shouldInvoke返回false的情況,需多帶帶處理
【注3】對(duì)于lodash的debounce來說,throttle是一種leading為true且maxWait等于wait的特殊debounce
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/101866.html
摘要:函數(shù)節(jié)流函數(shù)防抖函數(shù)節(jié)流和函數(shù)防抖函數(shù)節(jié)流和函數(shù)防抖二者很容易被混淆起來。函數(shù)防抖函數(shù)在特定的時(shí)間內(nèi)不被再調(diào)用后執(zhí)行。一句話概括函數(shù)節(jié)流是從用戶開始輸入就開始計(jì)時(shí),而函數(shù)節(jié)流是從用戶停止輸入開始計(jì)時(shí)。 函數(shù)節(jié)流 & 函數(shù)防抖 函數(shù)節(jié)流和函數(shù)防抖 函數(shù)節(jié)流和函數(shù)防抖二者很容易被混淆起來。下面貼英文原文,建議認(rèn)真閱讀:Debouncing enforces that a function ...
摘要:首先重置防抖函數(shù)最后調(diào)用時(shí)間,然后去觸發(fā)一個(gè)定時(shí)器,保證后接下來的執(zhí)行。這就避免了手動(dòng)管理定時(shí)器。 ??之前遇到過一個(gè)場(chǎng)景,頁(yè)面上有幾個(gè)d3.js繪制的圖形。如果調(diào)整瀏覽器可視區(qū)大小,會(huì)引發(fā)圖形重繪。當(dāng)圖中的節(jié)點(diǎn)比較多的時(shí)候,頁(yè)面會(huì)顯得異常卡頓。為了限制類似于這種短時(shí)間內(nèi)高頻率觸發(fā)的情況,我們可以使用防抖函數(shù)。 ??實(shí)際開發(fā)過程中,這樣的情況其實(shí)很多,比如: 頁(yè)面的scroll事件 ...
摘要:防抖防抖,簡(jiǎn)單來說就是防止抖動(dòng)。兩者間的核心區(qū)別就在于持續(xù)觸發(fā)事件時(shí),前者合并事件并在最后時(shí)間去觸發(fā)事件,而后者則是隔間時(shí)間觸發(fā)一次關(guān)鍵知識(shí)點(diǎn)定時(shí)器閉包資源在線測(cè)試源代碼 防抖和節(jié)流 窗口的resize、scroll,輸入框內(nèi)容校驗(yàn)等操作時(shí),如果這些操作處理函數(shù)較為復(fù)雜或頁(yè)面頻繁重渲染等操作時(shí),如果事件觸發(fā)的頻率無(wú)限制,會(huì)加重瀏覽器的負(fù)擔(dān),導(dǎo)致用戶體驗(yàn)非常糟糕。此時(shí)我們可以采用debo...
摘要:防抖防抖,簡(jiǎn)單來說就是防止抖動(dòng)。兩者間的核心區(qū)別就在于持續(xù)觸發(fā)事件時(shí),前者合并事件并在最后時(shí)間去觸發(fā)事件,而后者則是隔間時(shí)間觸發(fā)一次關(guān)鍵知識(shí)點(diǎn)定時(shí)器閉包資源在線測(cè)試源代碼 防抖和節(jié)流 窗口的resize、scroll,輸入框內(nèi)容校驗(yàn)等操作時(shí),如果這些操作處理函數(shù)較為復(fù)雜或頁(yè)面頻繁重渲染等操作時(shí),如果事件觸發(fā)的頻率無(wú)限制,會(huì)加重瀏覽器的負(fù)擔(dān),導(dǎo)致用戶體驗(yàn)非常糟糕。此時(shí)我們可以采用debo...
摘要:基礎(chǔ)防抖我們現(xiàn)在寫一個(gè)最基礎(chǔ)的防抖處理標(biāo)記事件也做如下改寫現(xiàn)在試一下,我們會(huì)發(fā)現(xiàn)只有我們停止?jié)L動(dòng)秒鐘的時(shí)候,控制臺(tái)才會(huì)打印出一行隨機(jī)數(shù)。 為何要防抖和節(jié)流 有時(shí)候會(huì)在項(xiàng)目開發(fā)中頻繁地觸發(fā)一些事件,如 resize、 scroll、 keyup、 keydown等,或者諸如輸入框的實(shí)時(shí)搜索功能,我們知道如果事件處理函數(shù)無(wú)限制調(diào)用,會(huì)大大加重瀏覽器的工作量,有可能導(dǎo)致頁(yè)面卡頓影響體驗(yàn);后臺(tái)...
閱讀 3222·2021-11-11 16:55
閱讀 2458·2021-10-13 09:39
閱讀 2392·2021-09-13 10:27
閱讀 2155·2019-08-30 15:55
閱讀 3083·2019-08-30 15:54
閱讀 3127·2019-08-29 16:34
閱讀 1819·2019-08-29 12:41
閱讀 1065·2019-08-29 11:33