原文:函數防抖和節流;
序言:
我們在平時開發的時候,會有很多場景會頻繁觸發事件,比如說搜索框實時發請求,onmousemove, resize, onscroll等等,有些時候,我們並不能或者不想頻繁觸發事件,咋辦呢?這時候就應該用到函數防抖和函數節流了!
准備材料:
<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div> <script> let num = 1; let content = document.getElementById('content'); function count() { content.innerHTML = num++; }; content.onmousemove = count; </script>
這段代碼, 在灰色區域內鼠標隨便移動,就會持續觸發 count() 函數,導致的效果如下:
接下來我們通過防抖和節流限制頻繁操作。
函數防抖(debounce)
短時間內多次觸發同一事件,只執行最后一次,或者只執行最開始的一次,中間的不執行。
// 非立即執行版 function debounce(func, wait) { let timer; return function() { let context = this; // 注意 this 指向 let args = arguments; // arguments中存着e if (timer) clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args) }, wait) } }
我們是這樣使用的:
content.onmousemove = debounce(count,1000);
非立即執行版的意思是觸發事件后函數不會立即執行,而是在 n 秒后執行,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。效果如下:
// 立即執行版 function debounce(func, wait) { let timer; return function() { let context = this; // 這邊的 this 指向誰? let args = arguments; // arguments中存着e if (timer) clearTimeout(timer); let callNow = !timer; timer = setTimeout(() => { timer = null; }, wait) if (callNow) func.apply(context, args); } }
立即執行版的意思是觸發事件后函數會立即執行,然后 n 秒內不觸發事件才能繼續執行函數的效果。用法同上,效果如下:
// 合成版 /** * @desc 函數防抖 * @param func 目標函數 * @param wait 延遲執行毫秒數 * @param immediate true - 立即執行, false - 延遲執行 */ function debounce(func, wait, immediate) { let timer; return function() { let context = this, args = arguments; if (timer) clearTimeout(timer); if (immediate) { let callNow = !timer; timer = setTimeout(() => { timer = null; }, wait); if (callNow) func.apply(context, args); } else { timer = setTimeout(() => { func.apply }, wait) } } }
節流(throttle)
指連續觸發事件但是在 n 秒中只執行一次函數。即 2n 秒內執行 2 次... 。節流如字面意思,會稀釋函數的執行頻率。
同樣有兩個版本,時間戳和定時器版。
// 時間戳版 function throttle(func, wait) { let previous = 0; return function() { let now = Date.now(); let context = this; let args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } } }
使用方式如下:
content.onmousemove = throttle(count,1000);
效果如下:
可以看到,在持續觸發事件的過程中,函數會立即執行,並且每 1s 執行一次。
// 定時器版 function throttle(func, wait) { let timeout; return function() { let context = this; let args = arguments; if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args) }, wait) } } }
用法同上,效果如下:
可以看到,在持續觸發事件的過程中,函數不會立即執行,並且每 1s 執行一次,在停止觸發事件后,函數還會再執行一次。
我們應該可以很容易的發現,其實時間戳版和定時器版的節流函數的區別就是,時間戳版的函數觸發是在時間段內開始的時候,而定時器版的函數觸發是在時間段內結束的時候。
同樣地,我們也可以將時間戳版和定時器版的節流函數結合起來,實現雙劍合璧版的節流函數。
/** * @desc 函數節流 * @param func 函數 * @param wait 延遲執行毫秒數 * @param type 1 表時間戳版,2 表定時器版 */ function throttle(func, wait, type) { if (type === 1) { let previous = 0; } else if (type === 2) { let timeout; } return function() { let context = this; let args = arguments; if (type === 1) { let now = Date.now(); if (now - previous > wait) { func.apply(context, args); previous = now; } } else if (type === 2) { if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args) }, wait) } } } }
附錄:
關於節流/防抖函數中 context(this) 的指向解析:
首先,在執行 throttle(count, 1000) 這行代碼的時候,會有一個返回值,這個返回值是一個新的匿名函數,因此 content.onmousemove = throttle(count,1000); 這句話最終可以這樣理解:
content.onmousemove = function() { let now = Date.now(); let context = this; let args = arguments; ... console.log(this) }
到這邊為止,只是綁定了事件函數,還沒有真正執行,而 this 的具體指向需要到真正運行時才能夠確定下來。所以這個時候如果我們把前面的 content.onmousemove 替換成 var fn 並執行 fn fn() ,此時內部的 this 打印出來就會是 window 對象。
其次,當我們觸發 onmousemove 事件的時候,才真正執行了上述的匿名函數,即 content.onmousemove() 。此時,上述的匿名函數的執行是通過 對象.函數名() 來完成的,那么函數內部的 this 自然指向 對象。
最后,匿名函數內部的 func 的調用方式如果是最普通的直接執行 func() ,那么 func 內部的 this 必然指向 window ,雖然在代碼簡單的情況下看不出什么異常(結果表現和正常一樣),但是這將會是一個隱藏 bug,不得不注意啊!所以,我們通過匿名函數捕獲 this,然后通過 func.apply() 的方式,來達到 content.onmousemove = func 這樣的效果。
可以說,高階函數內部都要注意 this 的綁定。