摘要:引言上一節我們詳細聊了聊高階函數之柯里化,通過介紹其定義和三種柯里化應用,并在最后實現了一個通用的函數。第二種方案來實現也存在一個問題,因為定時器是延遲執行的,所以事件停止觸發時必然會響應回調,所以時無法生效。
引言
上一節我們詳細聊了聊高階函數之柯里化,通過介紹其定義和三種柯里化應用,并在最后實現了一個通用的 currying 函數。這一小節會繼續之前的篇幅聊聊函數節流 throttle,給出這種高階函數的定義、實現原理以及在 underscore 中的實現,歡迎大家拍磚。
有什么想法或者意見都可以在評論區留言,下圖是本文的思維導圖,高清思維導圖和更多文章請看我的 Github。
定義及解讀函數節流指的是某個函數在一定時間間隔內(例如 3 秒)只執行一次,在這 3 秒內 無視后來產生的函數調用請求,也不會延長時間間隔。3 秒間隔結束后第一次遇到新的函數調用會觸發執行,然后在這新的 3 秒內依舊無視后來產生的函數調用請求,以此類推。
舉一個小例子,不知道大家小時候有沒有養過小金魚啥的,養金魚肯定少不了接水,剛開始接水時管道中水流很大,水到半滿時開始擰緊水龍頭,減少水流的速度變成 3 秒一滴,通過滴水給小金魚增加氧氣。
此時「管道中的水」就是我們頻繁操作事件而不斷涌入的回調任務,它需要接受「水龍頭」安排;「水龍頭」就是節流閥,控制水的流速,過濾無效的回調任務;「滴水」就是每隔一段時間執行一次函數,「3 秒」就是間隔時間,它是「水龍頭」決定「滴水」的依據。
如果你還無法理解,看下面這張圖就清晰多了,另外點擊 這個頁面 查看節流和防抖的可視化比較。其中 Regular 是不做任何處理的情況,throttle 是函數節流之后的結果,debounce 是函數防抖之后的結果(下一小節介紹)。
原理及實現函數節流非常適用于函數被頻繁調用的場景,例如:window.onresize() 事件、mousemove 事件、上傳進度等情況。使用 throttle API 很簡單,那應該如何實現 throttle 這個函數呢?
實現方案有以下兩種
第一種是用時間戳來判斷是否已到執行時間,記錄上次執行的時間戳,然后每次觸發事件執行回調,回調中判斷當前時間戳距離上次執行時間戳的間隔是否已經達到時間差(Xms) ,如果是則執行,并更新上次執行的時間戳,如此循環。
第二種方法是使用定時器,比如當 scroll 事件剛觸發時,打印一個 hello world,然后設置個 1000ms 的定時器,此后每次觸發 scroll 事件觸發回調,如果已經存在定時器,則回調不執行方法,直到定時器觸發,handler 被清除,然后重新設置定時器。
這里我們采用第一種方案來實現,通過閉包保存一個 previous 變量,每次觸發 throttle 函數時判斷當前時間和 previous 的時間差,如果這段時間差小于等待時間,那就忽略本次事件觸發。如果大于等待時間就把 previous 設置為當前時間并執行函數 fn。
我們來一步步實現,首先實現用閉包保存 previous 變量。
const throttle = (fn, wait) => { // 上一次執行該函數的時間 let previous = 0 return function(...args) { console.log(previous) ... } }
執行 throttle 函數后會返回一個新的 function,我們命名為 betterFn。
const betterFn = function(...args) { console.log(previous) ... }
betterFn 函數中可以獲取到 previous 變量值也可以修改,在回調監聽或事件觸發時就會執行 betterFn,即 betterFn(),所以在這個新函數內判斷當前時間和 previous 的時間差即可。
const betterFn = function(...args) { let now = +new Date(); if (now - previous > wait) { previous = now // 執行 fn 函數 fn.apply(this, args) } }
結合上面兩段代碼就實現了節流函數,所以完整的實現如下。
// fn 是需要執行的函數 // wait 是時間間隔 const throttle = (fn, wait = 50) => { // 上一次執行 fn 的時間 let previous = 0 // 將 throttle 處理結果當作函數返回 return function(...args) { // 獲取當前時間,轉換成時間戳,單位毫秒 let now = +new Date() // 將當前時間和上一次執行函數的時間進行對比 // 大于等待時間就把 previous 設置為當前時間并執行函數 fn if (now - previous > wait) { previous = now fn.apply(this, args) } } } // DEMO // 執行 throttle 函數返回新函數 const betterFn = throttle(() => console.log("fn 函數執行了"), 1000) // 每 10 秒執行一次 betterFn 函數,但是只有時間差大于 1000 時才會執行 fn setInterval(betterFn, 10)underscore 源碼解讀
上述代碼實現了一個簡單的節流函數,不過 underscore 實現了更高級的功能,即新增了兩個功能
配置是否需要響應事件剛開始的那次回調( leading 參數,false 時忽略)
配置是否需要響應事件結束后的那次回調( trailing 參數,false 時忽略)
配置 { leading: false } 時,事件剛開始的那次回調不執行;配置 { trailing: false } 時,事件結束后的那次回調不執行,不過需要注意的是,這兩者不能同時配置。
所以在 underscore 中的節流函數有 3 種調用方式,默認的(有頭有尾),設置 { leading: false } 的,以及設置 { trailing: false } 的。上面說過實現 throttle 的方案有 2 種,一種是通過時間戳判斷,另一種是通過定時器創建和銷毀來控制。
第一種方案實現這 3 種調用方式存在一個問題,即事件停止觸發時無法響應回調,所以 { trailing: true } 時無法生效。
第二種方案來實現也存在一個問題,因為定時器是延遲執行的,所以事件停止觸發時必然會響應回調,所以 { trailing: false } 時無法生效。
underscore 采用的方案是兩種方案搭配使用來實現這個功能。
const throttle = function(func, wait, options) { var timeout, context, args, result; // 上一次執行回調的時間戳 var previous = 0; // 無傳入參數時,初始化 options 為空對象 if (!options) options = {}; var later = function() { // 當設置 { leading: false } 時 // 每次觸發回調函數后設置 previous 為 0 // 不然為當前時間 previous = options.leading === false ? 0 : _.now(); // 防止內存泄漏,置為 null 便于后面根據 !timeout 設置新的 timeout timeout = null; // 執行函數 result = func.apply(context, args); if (!timeout) context = args = null; }; // 每次觸發事件回調都執行這個函數 // 函數內判斷是否執行 func // func 才是我們業務層代碼想要執行的函數 var throttled = function() { // 記錄當前時間 var now = _.now(); // 第一次執行時(此時 previous 為 0,之后為上一次時間戳) // 并且設置了 { leading: false }(表示第一次回調不執行) // 此時設置 previous 為當前值,表示剛執行過,本次就不執行了 if (!previous && options.leading === false) previous = now; // 距離下次觸發 func 還需要等待的時間 var remaining = wait - (now - previous); context = this; args = arguments; // 要么是到了間隔時間了,隨即觸發方法(remaining <= 0) // 要么是沒有傳入 {leading: false},且第一次觸發回調,即立即觸發 // 此時 previous 為 0,wait - (now - previous) 也滿足 <= 0 // 之后便會把 previous 值迅速置為 now if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); // clearTimeout(timeout) 并不會把 timeout 設為 null // 手動設置,便于后續判斷 timeout = null; } // 設置 previous 為當前時間 previous = now; // 執行 func 函數 result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 最后一次需要觸發的情況 // 如果已經存在一個定時器,則不會進入該 if 分支 // 如果 {trailing: false},即最后一次不需要觸發了,也不會進入這個分支 // 間隔 remaining milliseconds 后觸發 later 方法 timeout = setTimeout(later, remaining); } return result; }; // 手動取消 throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; // 執行 _.throttle 返回 throttled 函數 return throttled; };小結
函數節流指的是某個函數在一定時間間隔內(例如 3 秒)只執行一次,在這 3 秒內 無視后來產生的函數調用請求
節流可以理解為養金魚時擰緊水龍頭放水,3 秒一滴
「管道中的水」就是我們頻繁操作事件而不斷涌入的回調任務,它需要接受「水龍頭」安排
「水龍頭」就是節流閥,控制水的流速,過濾無效的回調任務
「滴水」就是每隔一段時間執行一次函數
「3 秒」就是間隔時間,它是「水龍頭」決定「滴水」的依據
節流實現方案有 2 種
第一種是用時間戳來判斷是否已到執行時間,記錄上次執行的時間戳,然后每次觸發事件執行回調,回調中判斷當前時間戳距離上次執行時間戳的間隔是否已經達到時間差(Xms) ,如果是則執行,并更新上次執行的時間戳,如此循環。
第二種方法是使用定時器,比如當 scroll 事件剛觸發時,打印一個 hello world,然后設置個 1000ms 的定時器,此后每次觸發 scroll 事件觸發回調,如果已經存在定時器,則回調不執行方法,直到定時器觸發,handler 被清除,然后重新設置定時器。
參考underscore.js文章穿梭機前端性能優化原理與實踐
underscore 函數節流的實現
【進階 6-2 期】深入高階函數應用之柯里化
【進階 6-1 期】JavaScript 高階函數淺析
【進階 5-3 期】深入探究 Function & Object 雞蛋問題
【進階 5-2 期】圖解原型鏈及其繼承優缺點
【進階 5-1 期】重新認識構造函數、原型和原型鏈
?? 看完三件事如果你覺得這篇內容對你挺有啟發,我想邀請你幫我三個小忙:
點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
關注我的 GitHub,讓我們成為長期關系
關注公眾號「高級前端進階」,每周重點攻克一個前端面試重難點,公眾號后臺回復「資料」 送你精選前端優質資料。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109934.html
摘要:主要實現思路就是通過定時器,通過設置延時時間,在第一次調用時,創建定時器,寫入需要執行的函數。如果這時前一個定時器暫未執行,則將其替換為新的定時器。 JS中的函數節流 一、什么是函數節流(throttle) 概念:限制一個函數在一定時間內只能執行一次。 舉個栗子,坐火車或地鐵,過安檢的時候,在一定時間(例如10秒)內,只允許一個乘客通過安檢入口,以配合安檢人員完成安檢工作。上例中,每1...
摘要:函數節流的原理函數節流的原理挺簡單的,估計大家都想到了,那就是定時器。在高級程序設計一書有介紹函數節流,里面封裝了這樣一個函數節流函數,它把定時器存為函數的一個屬性個人的世界觀不喜歡這種寫法。 什么是函數節流? 介紹前,先說下背景。在前端開發中,有時會為頁面綁定resize事件,或者為一個頁面元素綁定拖拽事件(其核心就是綁定mousemove),這種事件有一個特點,就是用戶不必特地搗亂...
摘要:使用上一篇文章的例子來說明下自由變量進階期深入淺出圖解作用域鏈和閉包訪問外部的今天是今天是其中既不是參數,也不是局部變量,所以是自由變量。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第二期,本周的主題是作用域閉包,今天是第7天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進階計...
摘要:本期推薦文章從作用域鏈談閉包,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。推薦理由這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。作用域鏈的頂端是全局對象,在全局環境中定義的變量就會綁定到全局對象中。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周開始前端進階的第二期,本周的主題是作用域閉包,今天是第6天。 本...
摘要:本計劃一共期,每期重點攻克一個面試重難點,如果你還不了解本進階計劃,點擊查看前端進階的破冰之旅本期推薦文章深入之執行上下文棧和深入之變量對象,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第一期,本周的主題是調用堆棧,今天是第二天。 本計劃一共28期,每期...
閱讀 3056·2021-10-12 10:12
閱讀 5375·2021-09-26 10:20
閱讀 1523·2021-07-26 23:38
閱讀 2812·2019-08-30 15:54
閱讀 1643·2019-08-30 13:45
閱讀 1962·2019-08-30 11:23
閱讀 3085·2019-08-29 13:49
閱讀 830·2019-08-26 18:23