'淺談 JS 防抖和節流'

瀏覽器 Chrome 設計 成都朗沃教育 2019-09-13
"

防抖和節流嚴格算起來應該屬於性能優化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。


所以還是很有必要早點掌握的(信我,你看完肯定就懂了)。


從滾動條監聽的例子說起


先說一個常見的功能,很多網站會提供這麼一個按鈕:用於返回頂部。


"

防抖和節流嚴格算起來應該屬於性能優化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。


所以還是很有必要早點掌握的(信我,你看完肯定就懂了)。


從滾動條監聽的例子說起


先說一個常見的功能,很多網站會提供這麼一個按鈕:用於返回頂部。


淺談 JS 防抖和節流


這個按鈕只會在滾動到距離頂部一定位置之後才出現,那麼我們現在抽象出這個功能需求——監聽瀏覽器滾動事件,返回當前滾條與頂部的距離。


這個需求很簡單,直接寫:


function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = showTop


但是!

"

防抖和節流嚴格算起來應該屬於性能優化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。


所以還是很有必要早點掌握的(信我,你看完肯定就懂了)。


從滾動條監聽的例子說起


先說一個常見的功能,很多網站會提供這麼一個按鈕:用於返回頂部。


淺談 JS 防抖和節流


這個按鈕只會在滾動到距離頂部一定位置之後才出現,那麼我們現在抽象出這個功能需求——監聽瀏覽器滾動事件,返回當前滾條與頂部的距離。


這個需求很簡單,直接寫:


function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = showTop


但是!

淺談 JS 防抖和節流

在運行的時候會發現存在一個問題:這個函數的默認執行頻率,太!高!了!高到什麼程度呢?


以chrome為例,我們可以點擊選中一個頁面的滾動條,然後點擊一次鍵盤的【向下方向鍵】,會發現函數執行了8-9次!


"

防抖和節流嚴格算起來應該屬於性能優化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。


所以還是很有必要早點掌握的(信我,你看完肯定就懂了)。


從滾動條監聽的例子說起


先說一個常見的功能,很多網站會提供這麼一個按鈕:用於返回頂部。


淺談 JS 防抖和節流


這個按鈕只會在滾動到距離頂部一定位置之後才出現,那麼我們現在抽象出這個功能需求——監聽瀏覽器滾動事件,返回當前滾條與頂部的距離。


這個需求很簡單,直接寫:


function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = showTop


但是!

淺談 JS 防抖和節流

在運行的時候會發現存在一個問題:這個函數的默認執行頻率,太!高!了!高到什麼程度呢?


以chrome為例,我們可以點擊選中一個頁面的滾動條,然後點擊一次鍵盤的【向下方向鍵】,會發現函數執行了8-9次!


淺談 JS 防抖和節流


然而實際上我們並不需要如此高頻的反饋,畢竟瀏覽器的性能是有限的,不應該浪費在這裡,所以接著討論如何優化這種場景。


防抖(debounce)


基於上述場景,首先提出第一種思路:在第一次觸發事件時,不立即執行函數,而是給出一個期限值比如200ms,然後:


  • 如果在200ms內沒有再次觸發滾動事件,那麼就執行函數。
  • 如果在200ms內再次觸發滾動事件,那麼當前的計時取消,重新開始計時。


效果:如果短時間內大量觸發同一事件,只會執行一次函數。


實現:既然前面都提到了計時,那實現的關鍵就在於 setTimeOut這個函數,由於還需要一個變量來保存計時,考慮維護全局純淨,可以藉助閉包來實現:


/*
* fn [function] 需要防抖的函數
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //藉助閉包
return function() {
if(timer){
clearTimeout(timer) //進入該分支語句,說明當前正在一個計時過程中,並且又觸發了相同事件。所以要取消當前的計時,重新開始計時
timer = setTimeOut(fn,delay)
}else{
timer = setTimeOut(fn,delay) // 進入該分支說明當前並沒有在計時,那麼就開始一個計時
}
}
}


當然 上述代碼是為了貼合思路,方便理解(這麼貼心不給個贊咩?),寫完會發現其實 time=setTimeOut(fn,delay)是一定會執行的,所以可以稍微簡化下:


/*****************************簡化後的分割線 ******************************/
function debounce(fn,delay){
let timer = null //藉助閉包
return function() {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(fn,delay) // 簡化寫法
}
}
// 然後是舊代碼
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 為了方便觀察效果我們取個大點的間斷值,實際使用根據需要來配置


此時會發現,必須在停止滾動1秒以後,才會打印出滾動條位置。


到這裡,已經把防抖實現了,現在給出定義:


對於短時間內連續觸發的事件(上面的滾動事件),防抖的含義就是讓某個時間期限(如上面的1000毫秒)內,事件處理函數只執行一次。


節流(throttle)


繼續思考,使用上面的防抖方案來處理問題的結果是:


如果在限定時間段內,不斷觸發滾動事件(比如某個用戶閒著無聊,按住滾動不斷的拖來拖去),只要不停止觸發,理論上就永遠不會輸出當前距離頂部的距離。


但是如果產品同學的期望處理方案是:


即使用戶不斷拖動滾動條,也能在某個時間間隔之後給出反饋呢(此處暫且不論哪種方案更合適,既然產品爸爸說話了我們就先考慮怎麼實現)?


"

防抖和節流嚴格算起來應該屬於性能優化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。


所以還是很有必要早點掌握的(信我,你看完肯定就懂了)。


從滾動條監聽的例子說起


先說一個常見的功能,很多網站會提供這麼一個按鈕:用於返回頂部。


淺談 JS 防抖和節流


這個按鈕只會在滾動到距離頂部一定位置之後才出現,那麼我們現在抽象出這個功能需求——監聽瀏覽器滾動事件,返回當前滾條與頂部的距離。


這個需求很簡單,直接寫:


function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = showTop


但是!

淺談 JS 防抖和節流

在運行的時候會發現存在一個問題:這個函數的默認執行頻率,太!高!了!高到什麼程度呢?


以chrome為例,我們可以點擊選中一個頁面的滾動條,然後點擊一次鍵盤的【向下方向鍵】,會發現函數執行了8-9次!


淺談 JS 防抖和節流


然而實際上我們並不需要如此高頻的反饋,畢竟瀏覽器的性能是有限的,不應該浪費在這裡,所以接著討論如何優化這種場景。


防抖(debounce)


基於上述場景,首先提出第一種思路:在第一次觸發事件時,不立即執行函數,而是給出一個期限值比如200ms,然後:


  • 如果在200ms內沒有再次觸發滾動事件,那麼就執行函數。
  • 如果在200ms內再次觸發滾動事件,那麼當前的計時取消,重新開始計時。


效果:如果短時間內大量觸發同一事件,只會執行一次函數。


實現:既然前面都提到了計時,那實現的關鍵就在於 setTimeOut這個函數,由於還需要一個變量來保存計時,考慮維護全局純淨,可以藉助閉包來實現:


/*
* fn [function] 需要防抖的函數
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //藉助閉包
return function() {
if(timer){
clearTimeout(timer) //進入該分支語句,說明當前正在一個計時過程中,並且又觸發了相同事件。所以要取消當前的計時,重新開始計時
timer = setTimeOut(fn,delay)
}else{
timer = setTimeOut(fn,delay) // 進入該分支說明當前並沒有在計時,那麼就開始一個計時
}
}
}


當然 上述代碼是為了貼合思路,方便理解(這麼貼心不給個贊咩?),寫完會發現其實 time=setTimeOut(fn,delay)是一定會執行的,所以可以稍微簡化下:


/*****************************簡化後的分割線 ******************************/
function debounce(fn,delay){
let timer = null //藉助閉包
return function() {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(fn,delay) // 簡化寫法
}
}
// 然後是舊代碼
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 為了方便觀察效果我們取個大點的間斷值,實際使用根據需要來配置


此時會發現,必須在停止滾動1秒以後,才會打印出滾動條位置。


到這裡,已經把防抖實現了,現在給出定義:


對於短時間內連續觸發的事件(上面的滾動事件),防抖的含義就是讓某個時間期限(如上面的1000毫秒)內,事件處理函數只執行一次。


節流(throttle)


繼續思考,使用上面的防抖方案來處理問題的結果是:


如果在限定時間段內,不斷觸發滾動事件(比如某個用戶閒著無聊,按住滾動不斷的拖來拖去),只要不停止觸發,理論上就永遠不會輸出當前距離頂部的距離。


但是如果產品同學的期望處理方案是:


即使用戶不斷拖動滾動條,也能在某個時間間隔之後給出反饋呢(此處暫且不論哪種方案更合適,既然產品爸爸說話了我們就先考慮怎麼實現)?


淺談 JS 防抖和節流


其實很簡單:


我們可以設計一種類似控制閥門一樣定期開放的函數,也就是讓函數執行一次後,在某個時間段內暫時失效,過了這段時間後再重新激活(類似於技能冷卻時間)。


效果:


如果短時間內大量觸發同一事件,那麼在函數執行一次之後,該函數在指定的時間期限內不再工作,直至過了這段時間才重新生效。


實現 這裡藉助 setTimeout來做一個簡單的實現,加上一個狀態位 valid來表示當前函數是否處於工作狀態:


function throttle(fn,delay){
let valid = true
return function() {
if(!valid){
//休息時間 暫不接客
return false
}
// 工作時間,執行函數並且在間隔期內把狀態位設為無效
valid = false
setTimeout(() => {
fn()
valid = true;
}, delay)
}
}
/*
請注意,節流函數並不止上面這種實現方案,
例如可以完全不借助setTimeout,可以把狀態位換成時間戳,然後利用時間戳差值是否大於指定間隔時間來做判定。
也可以直接將setTimeout的返回的標記當做判斷條件-判斷當前定時器是否存在,如果存在表示還在冷卻,並且在執行fn之後消除定時器表示激活,原理都一樣
*/
// 以下照舊
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000)


運行以上代碼的結果是:如果一直拖著滾動條進行滾動,那麼會以1s的時間間隔,持續輸出當前位置和頂部的距離。


其他應用場景舉例


講完了這兩個技巧,下面介紹一下平時開發中常遇到的場景:


1、搜索框input事件,例如要支持輸入實時搜索可以使用節流方案(間隔一段時間就必須查詢相關內容),或者實現輸入間隔大於某個值(如500ms),就當做用戶輸入完成,然後開始搜索,具體使用哪種方案要看業務需求。


2、頁面resize事件,常見於需要做頁面適配的時候。需要根據最終呈現的頁面情況進行dom渲染(這種情形一般是使用防抖,因為只需要判斷最後一次的變化情況)。


思考總結


上述內容基於防抖和節流的核心思路設計了簡單的實現算法,但是不代表實際的庫(例如undercore js)的源碼就直接是這樣的,最起碼的可以看出,在上述代碼實現中,因為 showTop本身的很簡單,無需考慮作用域和參數傳遞,所以連 apply都沒有用到,實際上肯定還要考慮傳遞 argument以及上下文環境(畢竟apply需要用到this對象)。


這裡的相關知識在《函數柯里化(curry)》(https://segmentfault.com/a/1190000015929416)和《解析js中的this對象》(https://segmentfault.com/a/1190000012704180)的文章裡也有提到。


本文依然堅持突出核心代碼,儘可能剝離無關功能點的思路行文因此不做贅述。

如果內容有錯誤的地方歡迎指出。

"

相關推薦

推薦中...