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