防抖函式和節流函式本質是不一樣的。防抖函式是將多次執行變為最後一次執行或第一次執行,節流函式是將多次執行變成每隔一段時間執行。
節流函式
前言
防抖函式和節流函式本質是不一樣的。防抖函式是將多次執行變為最後一次執行或第一次執行,節流函式是將多次執行變成每隔一段時間執行。
比如說,噹噹我們做圖片懶載入(lazyload)時,需要通過滾動位置,實時顯示圖片時,如果使用防抖函式,懶載入(lazyload)函式將會不斷被延時, 當我們做圖片懶載入(lazyload)時,需要通過滾動位置,實時顯示圖片時,如果使用防抖函式,懶載入(lazyload)函式將會不斷被延時, 只有停下來的時候才會被執行,對於這種需要週期性觸發事件的情況,防抖函式就顯得不是很友好了,此時就應該使用節流函式來實現了。
例子
<div id="container"></div>
複製程式碼
div{
height: 200px;
line-height: 200px;
text-align: center; color: #fff;
background-color: #444;
font-size: 25px;
border-radius: 3px;
}
複製程式碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
container.innerHTML = count ++ ;
}
container.addEventListener('mousemove',updateCount);
複製程式碼
我們來看一下效果:
我們可以看到,滑鼠從左側滑到右側,我們繫結的事件執行了119次
節流函式的實現
現在我們來實現一個節流函式,使得滑鼠移動過程中每間隔一段時間事件觸發一次。
使用時間戳來實現節流
首先我們想到使用時間戳計時的方式,每次事件執行時獲取當前時間並進行比較判斷是否執行事件。
/**
* 節流函式
* @param func 使用者傳入的節流函式
* @param wait 間隔的時間
*/
const throttle = function (func,wait = 50) {
let preTime = 0;
return function (...args) {
let now = Date.now();
if(now - preTime >= wait){
func.apply(this,args);
preTime = now;
}
}
};
複製程式碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
container.innerHTML = count ++ ;
}
let action = throttle(updateCount,1000);
container.addEventListener('mousemove',action);
複製程式碼
此時當滑鼠移入的時候,事件立即執行,在滑鼠移動的過程中,每隔1000ms事件執行一次,旦在最後滑鼠停止移動後,事件不會被執行
此時會有這樣的兩個問題:
- 如果我們希望滑鼠剛進入的時候不立即觸發事件,此時該怎麼辦呢?
- 如果我們希望滑鼠停止移動後,等到間隔時間到來的時候,事件依然執行,此時該怎麼辦呢?
使用定時器實現節流
為滿足上面的需求,我們考慮使用定時器來實現節流函式
當事件觸發的時候,我們設定一個定時器,再觸發的時候,定時器存在就不執行,等到定時器執行並執行函式,清空定時器,然後接著設定定時器
/**
* 節流函式
* @param func 使用者傳入的節流函式
* @param wait 間隔的時間
*/
const throttle = function (func,wait = 50) {
let timer = null;
return function (...args) {
if(!timer){
timer = setTimeout(()=>{
func.apply(this,args);
timer = null;
},wait);
}
}
};
複製程式碼
使用這個定時器節流函式應用在最開始的例子上:
let action = throttle(updateCount,2000);
container.addEventListener('mousemove',action);
複製程式碼
我們可以看到,當滑鼠移入的時候,時間不會立即執行,等待2000ms後執行了一次,此後2000ms執行一次,當滑鼠移除後,前一次觸發事件的時間2000ms後還會觸發一次事件。
比較時間戳節流與定時器節流
- 時間戳節流
- 開始時,事件立即執行
- 停止觸發後,沒有辦法再執行事件
- 定時器節流
- 開始時,會在間隔時間後第一次執行
- 停止觸發後,依然會再次執行一次事件
對於我們日常的工作需求來說,可能出現的需求是,既需要開始時立即執行,也需要結束時還能再執行一次的節流函式。
綜合時間戳節流和定時器節流
/**
* 節流函式
* @param func 使用者傳入的節流函式
* @param wait 間隔的時間
*/
const throttle = function (func,wait = 50) {
let preTime = 0,
timer = null;
return function (...args) {
let now = Date.now();
// 沒有剩餘時間 || 修改了系統時間
if(now - preTime >= wait || preTime > now){
if(timer){
clearTimeout(timer);
timer = null;
}
preTime = now;
func.apply(this,args);
}else if(!timer){
timer = setTimeout(()=>{
preTime = Date.now();
timer = null;
func.apply(this,args)
},wait - now + preTime);
}
}
};
複製程式碼
使用這個定時器節流函式應用在最開始的例子上:
let action = throttle(updateCount,2000);
container.addEventListener('mousemove',action);
複製程式碼
我們可以看到,當滑鼠移入時,事件立即執行,之後每間隔2000ms後,事件均會執行,當滑鼠離開時,前一次事件觸發2000ms後,事件最後會再一次執行
我們繼續考慮下面的場景
- 有時候我們的需求變成滑鼠移入時立即執行,滑鼠移除後事件不在執行呢?
- 有時候我們的需求變成滑鼠移入時不立即執行,滑鼠移除後事件還會執行呢?
繼續優化
我們設定 opts 作為 throttle 函式的第三個引數,然後根據 opts 所攜帶的值來判斷實現那種效果,約定如下:
- leading : Boolean 是否使用第一次執行
- trailing : Boolean 是否使用停止觸發的回撥執行
修改程式碼如下:
/**
* 節流函式
* @param func 使用者傳入的節流函式
* @param wait 間隔的時間
* @param opts leading 是否第一次執行 trailing 是否停止觸發後執行
*/
const throttle = function (func,wait = 50,opts = {}) {
let preTime = 0,
timer = null,
{ leading = true, trailing = true } = opts;
return function (...args) {
let now = Date.now();
if(!leading && !preTime){
preTime = now;
}
// 沒有剩餘時間 || 修改了系統時間
if(now - preTime >= wait || preTime > now){
if(timer){
clearTimeout(timer);
timer = null;
}
preTime = now;
func.apply(this,args);
}else if(!timer && trailing){
timer = setTimeout(()=>{
preTime = Date.now();
timer = null;
func.apply(this,args)
},wait - now + preTime);
}
}
};
複製程式碼
這裡需要注意的是,leading:false 和 trailing: false 不能同時設定。 因為如果同時設定的時候,當滑鼠移除的時候,停止觸發的時候不會設定定時器,也就是說,等到過了設定的時間,preTime不會被更新,此後再次移入的話就會立即執行,就違反了 leading: false
取消
在 debounce 的實現中,我們加了一個 cancel 方法,throttle 我們也加個 cancel 方法:
/**
* 節流函式
* @param func 使用者傳入的節流函式
* @param wait 間隔的時間
* @param opts leading 是否第一次執行 trailing 是否停止觸發後執行
*/
const throttle = function (func,wait = 50,opts = {}) {
let preTime = 0,
timer = null,
{ leading = false, trailing = true } = opts,
throttled = function (...args) {
let now = Date.now();
if(!leading && !preTime){
preTime = now;
}
// 沒有剩餘時間 || 修改了系統時間
if(now - preTime >= wait || preTime > now){
if(timer){
clearTimeout(timer);
timer = null;
}
preTime = now;
func.apply(this,args);
}else if(!timer && trailing){
timer = setTimeout(()=>{
preTime = Date.now();
timer = null;
func.apply(this,args)
},wait - now + preTime);
}
};
throttled.cancel = function () {
clearTimeout(timer);
timer = null;
preTime = 0;
};
return throttled;
};
複製程式碼
至此我們完成了一個節流函式。