【進階 6-3 期】深入淺出節流函式 throttle
高階前端進階(id:FrontendGaoji)
作者:木易楊,資深,前網易工程師,13K star Daily-Interview-Question 作者
引言
上一節我們詳細聊了聊高階函式之柯里化,透過介紹其定義和三種柯里化應用,並在最後實現了一個通用的 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 被清除,然後重新設定定時器。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/36/viewspace-2823642/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 函式防抖debounce與節流throttle函式
- 理解並優化函式節流Throttle優化函式
- 節流函式throttle是什麼鬼?函式
- 效能優化之節流函式---throttle優化函式
- js函式防抖debounce和節流throttleJS函式
- [JS效能優化]函式去抖(debounce)與函式節流(throttle)JS優化函式
- React16 生命週期函式深入淺出React函式
- 【進階 6-1 期】JavaScript 高階函式淺析JavaScript函式
- 關於js節流函式throttle和防抖動debounceJS函式
- 深入理解函式節流與函式防抖函式
- 淺談js函式節流和函式防抖JS函式
- 【進階 7-5 期】淺出篇 | 7 個角度吃透 Lodash 防抖節流原理
- 淺聊函式防抖與節流函式
- 【深入淺出ES6】函式函式
- 【深入淺出 Yarn 架構與實現】6-3 NodeManager 分散式快取Yarn架構分散式快取
- debounce(防抖) & throttle(節流)
- SAP UI5和Angular的函式防抖(Debounce)和函式節流(Throttle)實現原理介紹UIAngular函式
- 【進階3-2期】JavaScript深入之重新認識箭頭函式的thisJavaScript函式
- 函式防抖和函式節流函式
- 函式節流與函式防抖函式
- 前端效能優化之節流-throttle前端優化
- Python 函式進階-高階函式Python函式
- 深入淺出JS - 變數提升(函式宣告提升)JS變數函式
- JS函式節流和函式防抖JS函式
- 區分函式防抖&函式節流函式
- Java開發工程師進階篇-深入淺出RedisJava工程師Redis
- 深入淺出 Gin 生命週期
- 函式節流和防抖函式
- JS函式節流,去抖JS函式
- 函式防抖和節流函式
- 節流函式怎麼寫?函式
- 函式的進階函式
- 第 64 期深入淺出 Golang RuntimeGolang
- Python 函式進階-遞迴函式Python函式遞迴
- 深入淺出理解 React高階元件React元件
- js 函式防抖和節流JS函式
- 函式的防抖和節流函式
- JS專題之節流函式JS函式