摘要:首先重置防抖函數最后調用時間,然后去觸發(fā)一個定時器,保證后接下來的執(zhí)行。這就避免了手動管理定時器。
??之前遇到過一個場景,頁面上有幾個d3.js繪制的圖形。如果調整瀏覽器可視區(qū)大小,會引發(fā)圖形重繪。當圖中的節(jié)點比較多的時候,頁面會顯得異??D。為了限制類似于這種短時間內高頻率觸發(fā)的情況,我們可以使用防抖函數。
??實際開發(fā)過程中,這樣的情況其實很多,比如:
頁面的scroll事件
input框等的輸入事件
拖拽事件用到的mousemove等
??先說說防抖和節(jié)流是個啥,有啥區(qū)別
防抖:設定一個時間間隔,當某個頻繁觸發(fā)的函數執(zhí)行一次后,在這個時間間隔內不會再次被觸發(fā),如果在此期間嘗試觸發(fā)這個函數,則時間間隔會重新開始計算。debounce(函數防抖)節(jié)流:設定一個時間間隔,某個頻繁觸發(fā)的函數,在這個時間間隔內只會執(zhí)行一次。也就是說,這個頻繁觸發(fā)的函數會以一個固定的周期執(zhí)行。
??大致捋一遍代碼結構。為了方便閱讀,我們先把源碼中的Function注釋掉。
function debounce(func, wait, options) { // 代碼一開始,以閉包的形式定義了一些變量 var lastArgs, // 最后一次debounce的arguments,它其實起一個標記位的作用,后面會提到 lastThis, // 就是last this,用來修正this指向 maxWait, // 存儲option里面?zhèn)魅氲膍axWait值,最大等待時間 result, // 其實這個result始終都是undefined timerId, // setTimeout賦給它,用于表示當前定時器 lastCallTime, // 最后一次調用debounce的時刻 lastInvokeTime = 0, // 最后一次調用用戶傳入函數的時刻 leading = false, // 是否在一開始就執(zhí)行用戶傳入的函數 maxing = false, // 是否有最大等待時間 trailing = true; // 是否在等待周期結束后執(zhí)行用戶傳入的函數 // 用戶傳入的fun必須是個函數,否則報錯 if (typeof func != "function") { throw new TypeError(FUNC_ERROR_TEXT); } // toNumber是lodash封裝的一個轉類型的方法 wait = toNumber(wait) || 0; // 獲取用戶傳入的配置 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; } // 執(zhí)行用戶傳入的函數 function invokeFunc(time) { // ...... } // 防抖開始時執(zhí)行的操作 function leadingEdge(time) {) // ...... } // 計算仍然需要等待的時間 function remainingWait(time) { // ...... } // 判斷此時是否應該執(zhí)行用戶傳入的函數 function shouldInvoke(time) { // ...... } // 等待時間結束后的操作 function timerExpired() { // ...... } // 執(zhí)行用戶傳入的函數 function trailingEdge(time) { // ...... } // 取消防抖 function cancel() { // ...... } // 立即執(zhí)行用戶傳入的函數 function flush() { // ...... } // 防抖開始的入口 function debounced() { // ...... } debounced.cancel = cancel; debounced.flush = flush; return debounced; }
我們先從入口函數開始。函數開始執(zhí)行后,首先會出現三種情況:
時間上達到了可以執(zhí)行的條件;
時間上不滿足條件,但是此時的定時器并沒有啟動;
不滿足條件,返回undefined
??說實話,第二種情況沒想到場景,哪位大佬給補充一下呢。
??代碼中timerId = setTimeout(timerExpired, wait);是用來設置定時器,到時間后觸發(fā)trailingEdge這個函數。
function debounced() { var time = now(), isInvoking = shouldInvoke(time); // 判斷此時是否可以開始執(zhí)行用戶傳入的函數 lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { // 如果此時并沒有定時器存在,就開始進入防抖階段 if (timerId === undefined) { return leadingEdge(lastCallTime); } // 如果設置了最大等待時間,便立即執(zhí)行用戶傳入的函數 if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } // 不滿足條件,return undefined return result; }
??我們先來看看shouldInvoke是如何判斷函數是否可以執(zhí)行的。
function shouldInvoke(time) { // lastCallTime初始值是undefined,lastInvokeTime初始值是0, // 防抖函數被手動取消后,這兩個值會被設為初始值 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 || // 初次執(zhí)行 (timeSinceLastCall >= wait) || // 上次調用時刻距離現在已經大于wait值 (timeSinceLastCall < 0) || // 當前時間-上次調用時間小于0,應該只可能是手動修改了系統時間吧 (maxing && timeSinceLastInvoke >= maxWait) // 設置了最大等待時間,且已超時 ); }
??我們繼續(xù)分析函數開始的階段leadingEdge。首先重置防抖函數最后調用時間,然后去觸發(fā)一個定時器,保證wait后接下來的執(zhí)行。最后判斷如果leading是true的話,立即執(zhí)行用戶傳入的函數:
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; }
??我們已經不止一次去設定觸發(fā)器了,來我們探究一下里面到底做了啥。其實很簡單,判斷時間是否符合執(zhí)行條件,符合的話觸發(fā)trailingEdge,也就是后續(xù)操作,否則計算需要等待的時間,并重新調用這個函數,其實這里就是防抖的核心所在了。
function timerExpired() { var time = now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = setTimeout(timerExpired, remainingWait(time)); }
??至于如何重新計算剩余時間的,這里不作過多解釋,大家一看便知。
function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; }
??我們說說等待時間到了以后的操作。重置了一些本周期的變量。并且,如果trailing是true而且lastArgs存在時,才會再次執(zhí)行用戶傳入的參數。這里解釋了文章開頭提到的lastArgs只是個標記位,如注釋所說,他表示debounce至少執(zhí)行了一次。
function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; }
??執(zhí)行用戶傳入的函數比較簡單,我們知道call和apply是會立即執(zhí)行的,其實最后的result還是undefined。
function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; // 重置了一些條件 lastArgs = lastThis = undefined; lastInvokeTime = time; // 執(zhí)行用戶傳入函數 result = func.apply(thisArg, args); return result; }
??最后就是取消防抖和立即執(zhí)行用戶傳入函數的過程了,代碼一目了然,不作過多解釋。
function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(now()); }throttle(函數節(jié)流)
??節(jié)流其實原理跟防抖是一樣的,只不過觸發(fā)條件不同而已,其實就是maxWait為wait的防抖函數。
function throttle(func, wait, options) { var leading = true, trailing = true; if (typeof func != "function") { throw new TypeError(FUNC_ERROR_TEXT); } if (isObject(options)) { leading = "leading" in options ? !!options.leading : leading; trailing = "trailing" in options ? !!options.trailing : trailing; } return debounce(func, wait, { "leading": leading, "maxWait": wait, "trailing": trailing }); }總結
??我們發(fā)現,其實lodash除了在cancle函數中使用了清除定時器的操作外,其他地方并沒有去關心定時器,而是很巧妙的在定時器里加了一個判斷條件來判斷后續(xù)函數是否可以執(zhí)行。這就避免了手動管理定時器。
??lodash替我們考慮到了一些比較少見的情景,而且還有一定的容錯性。即便ES6實現了很多目前常用的工具函數,但是面對復雜的情景,我們依然可以以按需引入的方式使用lodash的一些函數來提升開發(fā)效率,同時使得我們的程序更加健壯。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/107958.html
摘要:節(jié)流和防抖在開發(fā)項目過程中很常見,例如輸入實時搜索滾動更新了,等等,大量的場景需要我們對其進行處理。防抖多次觸發(fā),只在最后一次觸發(fā)時,執(zhí)行目標函數。節(jié)流限制目標函數調用的頻率,比如內不能調用次。 節(jié)流和防抖在開發(fā)項目過程中很常見,例如 input 輸入實時搜索、scrollview 滾動更新了,等等,大量的場景需要我們對其進行處理。我們由 Lodash 來介紹,直接進入主題吧。 Lod...
摘要:最簡單的案例以最簡單的情景為例在某一時刻點只調用一次函數,那么將在時間后才會真正觸發(fā)函數。后續(xù)我們會逐漸增加黑色鬧鐘出現的復雜度,不斷去分析紅色鬧鐘的位置。 序 相比網上教程中的 debounce 函數,lodash 中的 debounce 功能更為強大,相應的理解起來更為復雜; 解讀源碼一般都是直接拿官方源碼來解讀,不過這次我們采用另外的方式:從最簡單的場景開始寫代碼,然后慢慢往源碼...
摘要:節(jié)流節(jié)流限制了一個函數可以在短時間內被調用的次數。更新防抖防抖確保了一個函數只有在一個固定時間段內沒有被調用過后,才會再次被調用。再換句話說防抖會等待事件不再高頻發(fā)生,再觸發(fā)。這個網站很好的可視化了節(jié)流與防抖。 節(jié)流 Throttling 節(jié)流限制了一個函數可以在短時間內被調用的次數??梢赃@樣形容:在一毫秒內最多執(zhí)行此函數 1 次。 Throttling enforces a maxi...
摘要:個人博客原文地址背景我們在開發(fā)的過程中會經常使用如等事件,如果正常綁定事件處理函數的話,有可能在很短的時間內多次連續(xù)觸發(fā)事件,十分影響性能。 個人博客原文地址 背景 我們在開發(fā)的過程中會經常使用如scroll、resize、touchmove等事件,如果正常綁定事件處理函數的話,有可能在很短的時間內多次連續(xù)觸發(fā)事件,十分影響性能。因此針對這類事件要進行節(jié)流或者防抖處理 節(jié)流 節(jié)流的意思...
摘要:防抖函數防抖和節(jié)流是一對常常被放在一起的場景。同時,這里會設置一個定時器,在等待后會執(zhí)行,的主要作用就是觸發(fā)。最后,如果不再有函數調用,就會在定時器結束時執(zhí)行。 函數節(jié)流和去抖的出現場景,一般都伴隨著客戶端 DOM 的事件監(jiān)聽。比如scroll resize等事件,這些事件在某些場景觸發(fā)非常頻繁。 比如,實現一個原生的拖拽功能(不能用 H5 Drag&Drop API),需要一路監(jiān)聽...
閱讀 2563·2021-07-26 23:38
閱讀 3460·2019-08-30 13:10
閱讀 2351·2019-08-29 18:33
閱讀 2349·2019-08-29 16:12
閱讀 1027·2019-08-29 10:59
閱讀 1820·2019-08-26 17:40
閱讀 802·2019-08-26 11:59
閱讀 841·2019-08-26 11:41