摘要:那么還有最后一個問題,那我之前設(shè)置的定時器怎么辦呢定時器執(zhí)行的是這個函數(shù),而這個函數(shù)又會通過進行一次判斷。
我們在處理事件的時候,有些事件由于觸發(fā)太頻繁,而每次事件都處理的話,會消耗太多資源,導致瀏覽器崩潰。最常見的是我們在移動端實現(xiàn)無限加載的時候,移動端本來滾動就不是很靈敏,如果每次滾動都處理的話,界面就直接卡死了。
因此,我們通常會選擇,不立即處理事件,而是在觸發(fā)一定次數(shù)或一定時間之后進行處理。這時候我們有兩個選擇: debounce(防抖動)和 throttle(節(jié)流閥)。
之前看過很多文章都還是沒有太弄明白兩者之間的區(qū)別,最后通過看源碼大致了解了兩者之間的區(qū)別以及簡單的實現(xiàn)思路。
首先,我們通過實踐來最簡單的看看二者的區(qū)別:
可以看到,throttle會在第一次事件觸發(fā)的時候就執(zhí)行,然后每隔wait(我這里設(shè)置的2000ms)執(zhí)行一次,而debounce只會在事件結(jié)束之后執(zhí)行一次。
有了一個大概的印象之后,我們看一看lodash的源碼對debounce和throttle的區(qū)別。
這里討論默認情況
function throttle(func, wait, options) { let leading = true, trailing = true; if (typeof func !== "function") { throw new TypeError(FUNC_ERROR_TEXT); } if (typeof options === "object") { leading = "leading" in options ? !!options.leading : leading; trailing = "trailing" in options ? !!options.trailing : trailing; } return debounce(func, wait, { leading, maxWait: wait, trailing, }); }
可以看到,throttle最后返回的還是debounce函數(shù),只是指定了options選項。那么接下來我們就集中分析debounce。
function debounce(fn, wait, options) { var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true; function debounced() { var time = now(), isInvoking = shouldInvoke(time); lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; }
為了記錄每次執(zhí)行的相關(guān)信息,debounce函數(shù)最后返回的是一個函數(shù),形成一個閉包。
這也解釋了為什么這樣寫不行:
window.addEventListener("resize", function(){ _.debounce(onResize, 2000); });
這樣寫根本就不會調(diào)用內(nèi)部的debounced函數(shù)。
解決第一個不同在debounced內(nèi)部呢,首先記錄了當前調(diào)用的時間,然后通過shouldInvoke這個函數(shù)判斷是否應該調(diào)用傳入的func。
function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we"re at the // trailing edge, the system time has gone backwards and we"re treating // it as the trailing edge, or we"ve hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); }
可以看到,該函數(shù)返回true的幾個條件。其中需要我們引起注意的是最后一個條件,這是debounce 與throttle的區(qū)別之一。
首先maxing通過函數(shù)開始的幾行代碼判斷:
if (isObject(options)) { leading = !!options.leading; maxing = "maxWait" in options; maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; trailing = "trailing" in options ? !!options.trailing : trailing; }
我們看到,在定義throttle的時候, 給debounce函數(shù)給傳入了options, 而里面包含maxWait這個屬性,因此,對于throttle來說,maxing為true, 而沒有傳入options的debounce則為false。這就是二者區(qū)別之一。在這里決定了shouldInvoke函數(shù)返回的值,以及是否執(zhí)行接下去的邏輯判斷。
我們再回到debounced這個函數(shù):
if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); }
在第一次調(diào)用的時候,debounce 和 throttle 的 isInvoking
為true, 且此時timerId === undefined也成立,就返回leadingEdge(lastCallTime)這個函數(shù)。
那么我們再來看看leadingEdge 這個函數(shù);
function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = setTimeout(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; }
這里出現(xiàn)了debounce和throttle的第二個區(qū)別。這個函數(shù)首先是設(shè)置了一個定時器,隨后返回的結(jié)果由leading決定。在默認情況下,throttle傳入的leading為true,而debounce為false。因此,throttle會馬上執(zhí)行傳入的函數(shù),而debounce不會。
這里我們就解決了它們的第一個不同:throttle會在第一次調(diào)用的時候就執(zhí)行,而debounce不會。
解決第二個不同我們再回到shouldInvoke的返回條件那里,如果在一個時間內(nèi)頻繁的調(diào)用, 前面三個條件都不會成立,對于debounce來說,最后一個也不會成立。而對于throttle來說,首先maxing為true, 而如果距離上一次*傳入的func 函數(shù)調(diào)用 大于maxWait最長等待時間的話,它也會返回true。
function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); }
if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } }
那么在shouldInvoke成立之后,throttle會設(shè)置一個定時器,返回執(zhí)行傳入函數(shù)的結(jié)果。
這就是debounce 和 throttle 之間的第二個區(qū)別:throttle會保證你每隔一段時間都會執(zhí)行,而debounce不會。
那么還有最后一個問題,那我之前設(shè)置的定時器怎么辦呢?
timerId = setTimeout(timerExpired, wait);
定時器執(zhí)行的是timerExpired這個函數(shù),而這個函數(shù)又會通過shouldInvoke進行一次判斷。
function timerExpired() { var time = now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = setTimeout(timerExpired, remainingWait(time)); }
最后,傳入的func怎么執(zhí)行的呢?下面這個函數(shù)實現(xiàn):
function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; }餓了么的簡單實現(xiàn)
在看餓了么的infinite scroll這個源碼的時候,看到了一個簡單版本的實現(xiàn):
var throttle = function (fn, delay) { var now, lastExec, timer, context, args; var execute = function () { fn.apply(context, args); lastExec = now; }; return function () { context = this; args = arguments; now = Date.now(); if (timer) { clearTimeout(timer); timer = null; } if (lastExec) { var diff = delay - (now - lastExec); if (diff < 0) { execute(); } else { timer = setTimeout(() => { execute(); }, diff); } } else { execute(); } }; };
那么它的思路很簡單:
通過lastExec判斷是否是第一次調(diào)用,如果是,就馬上執(zhí)行處理函數(shù)。
隨后就會監(jiān)測,每次調(diào)用的時間與上次執(zhí)行函數(shù)的時間差,如果小于0,就立馬執(zhí)行。大于0就會在事件間隔之后執(zhí)行。
每次調(diào)用的時候都會清除掉上一次的定時任務(wù),這樣就會保證只有一個最近的定時任務(wù)在等待執(zhí)行。
那么它與lodash的一個最大的區(qū)別呢,就是它是關(guān)注與上次執(zhí)行處理函數(shù)的時間差, 而lodash的shouldInvoke關(guān)注的是兩次事件調(diào)用函數(shù)的時間差。
總結(jié)總的來說,這種實現(xiàn)的主要部分呢,就是時間差 和 定時器
最后,自己參照寫了簡單的debounce 和 throttle: Gist求指教!
參考資料debouncing-throttling-explained-examples | CSS-Tricks
Lodash源碼
餓了么 vue-infinite-scroll
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/84048.html
摘要:事情是如何發(fā)生的最近干了件事情,發(fā)現(xiàn)了源碼的一個。樓主找到的關(guān)于和區(qū)別的資料如下關(guān)于拿來主義為什么這么多文章里會出現(xiàn)澤卡斯的錯誤代碼樓主想到了一個詞,叫做拿來主義。的文章,就深刻抨擊了拿來主義這一現(xiàn)象。 事情是如何發(fā)生的 最近干了件事情,發(fā)現(xiàn)了 underscore 源碼的一個 bug。這件事本身并沒有什么可說的,但是過程值得我們深思,記錄如下,各位看官仁者見仁智者見智。 平時有瀏覽別...
摘要:自己嘗試一下年在的文章中第一次看到的實現(xiàn)方法。這三種實現(xiàn)方法內(nèi)部不同,但是接口幾乎一致。如你所見,我們使用了參數(shù),因為我們只對用戶停止改變?yōu)g覽器大小時最后一次事件感興趣。 前幾天看到一篇文章,我的公眾號里也分享了《一次發(fā)現(xiàn)underscore源碼bug的經(jīng)歷以及對學術(shù)界拿來主義的思考》具體文章詳見,微信公眾號:showImg(https://segmentfault.com/img/b...
摘要:舉例舉例通過拖拽瀏覽器窗口,可以觸發(fā)很多次事件。不支持,所以不能在服務(wù)端用于文件系統(tǒng)事件。總結(jié)將一系列迅速觸發(fā)的事件例如敲擊鍵盤合并成一個單獨的事件。確保一個持續(xù)的操作流以每毫秒執(zhí)行一次的速度執(zhí)行。 Debounce 和 Throttle 是兩個很相似但是又不同的技術(shù),都可以控制一個函數(shù)在一段時間內(nèi)執(zhí)行的次數(shù)。 當我們在操作 DOM 事件的時候,為函數(shù)添加 debounce 或者 th...
摘要:目的都是為了降低回調(diào)函數(shù)執(zhí)行頻率,節(jié)省計算機資源,優(yōu)化性能,提升用戶體驗。函數(shù)防抖事件頻繁觸發(fā)的情況下,只有經(jīng)過足夠的空閑時間,才執(zhí)行代碼一次。 函數(shù)節(jié)流和函數(shù)防抖的對比分析 一、前言 前端開發(fā)中,函數(shù)節(jié)流(throttle) 和 函數(shù)防抖(debounce) 作為常用的性能優(yōu)化方法,兩者都是用于優(yōu)化高頻率執(zhí)行 js 代碼的手段,那具體它們有什么異同點呢?有對這兩個概念不太了解的小伙伴...
閱讀 3331·2021-11-18 10:02
閱讀 1548·2021-10-12 10:08
閱讀 1370·2021-10-11 10:58
閱讀 1356·2021-10-11 10:57
閱讀 1258·2021-10-08 10:04
閱讀 2209·2021-09-29 09:35
閱讀 851·2021-09-22 15:44
閱讀 1346·2021-09-03 10:30