防抖和節流原理分析

goooooooogle發表於2018-08-21

視窗的resize、scroll、輸入框內容校驗等操作時,如果這些操作處理函式是較為複雜或頁面頻繁重渲染等操作時,在這種情況下如果事件觸發的頻率無限制,會加重瀏覽器的負擔,導致使用者體驗非常糟糕。此時我們可以採用debounce(防抖)和throttle(節流)的方式來減少觸發的頻率,同時又不影響實際效果。

eg:搜尋框的請求優化,輸入搜尋詞條需要立即觸發搜尋請求時,防抖和節流可以將多個請求合併為一個請求

github筆記傳送門,走過路過 star 一個?

首先準備 html 檔案中程式碼如下:

<
div id="content" style="height:150px;
line-height:150px;
text-align:center;
color: #fff;
background-color:#ccc;
font-size:80px;
"
>
<
/div>
<
script>
var num = 1;
var content = document.getElementById('content');
function count() {
content.innerHTML = num++;

};
content.onmousemove = count;
<
/script>
複製程式碼

防抖

debounce(防抖),簡單來說就是防止抖動。

當持續觸發事件時,debounce 會合併事件且不會去觸發事件當一定時間內沒有觸發再這個事件時,才真正去觸發事件

非立即執行版

防抖和節流原理分析

非立即執行版的意思是觸發事件後函式不會立即執行,而是在 n 秒後執行,如果在 n 秒內又觸發了事件,則會重新計算函式執行時間。

const debounce = (func, wait, ...args) =>
{
let timeout;
return function(){
const context = this;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() =>
{
func.apply(context, args)
},wait);

}
}複製程式碼

如此呼叫:

content.onmousemove = debounce(count,1000);
複製程式碼
alt

立即執行版

防抖和節流原理分析

立即執行版的意思是觸發事件後函式會立即執行,然後 n 秒內不觸發事件才能繼續執行函式的效果。

const debounce = (func, wait, ...args) =>
{
let timeout;
return function(){
const context = this;
if (timeout) cleatTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() =>
{
timeout = null;

},wait) if(callNow) func.apply(context,args)
}
}複製程式碼
alt

結合版

/** * @desc 函式防抖 * @param func 函式 * @param wait 延遲執行毫秒數 * @param immediate true 表立即執行,false 表非立即執行 */function debounce(func,wait,immediate) { 
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;

}, wait) if (callNow) func.apply(context, args)
} else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);

}
}
}複製程式碼

節流

throttle(節流),當持續觸發事件時,保證隔間時間觸發一次事件。

持續觸發事件時,throttle 會合並一定時間內的事件,並在該時間結束時真正去觸發一次事件。

時間戳版

防抖和節流原理分析

在持續觸發事件的過程中,函式會立即執行,並且每 1s 執行一次。

const throttle = (func, wait, ...args) =>
{
let pre = 0;
return function(){
const context = this;
let now = Date.now();
if (now - pre >
= wait){
func.apply(context, args);
pre = Date.now();

}
}
}複製程式碼
alt

定時器版

防抖和節流原理分析

在持續觸發事件的過程中,函式不會立即執行,並且每 1s 執行一次,在停止觸發事件後,函式還會再執行一次。

const throttle = (func, wait, ...args) =>
{
let timeout;
return function(){
const context = this;
if(!timeout){
timeout = setTimeout(() =>
{
timeout = null;
func.apply(context,args);

},wait)
}
}
}複製程式碼
alt

結合版

其實時間戳版和定時器版的節流函式的區別就是,時間戳版的函式觸發是在時間段內開始的時候,而定時器版的函式觸發是在時間段內結束的時候。

/** * @desc 函式節流 * @param func 函式 * @param wait 延遲執行毫秒數 * @param type 1 表時間戳版,2 表定時器版 */function throttle(func, wait ,type) { 
if(type===1){
var previous = 0;

}else if(type===2){
var timeout;

} return function() {
var context = this;
var args = arguments;
if(type===1){
var now = Date.now();
if (now - previous >
wait) {
func.apply(context, args);
previous = now;

}
}else if(type===2){
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}複製程式碼

underscore 原始碼

/** * underscore 防抖函式,返回函式連續呼叫時,空閒時間必須大於或等於 wait,func 才會執行 * * @param  {function
} func 回撥函式 * @param {number
} wait 表示時間視窗的間隔 * @param {boolean
} immediate 設定為ture時,是否立即呼叫函式 * @return {function
} 返回客戶呼叫函式 */
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
// 現在和上一次時間戳比較 var last = _.now() - timestamp;
// 如果當前間隔時間少於設定時間且大於0就重新設定定時器 if (last <
wait &
&
last >
= 0) {
timeout = setTimeout(later, wait - last);

} else {
// 否則的話就是時間到了執行回撥函式 timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;

}
}
};
return function() {
context = this;
args = arguments;
// 獲得時間戳 timestamp = _.now();
// 如果定時器不存在且立即執行函式 var callNow = immediate &
&
!timeout;
// 如果定時器不存在就建立一個 if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
// 如果需要立即執行函式的話 通過 apply 執行 result = func.apply(context, args);
context = args = null;

} return result;

};

};
複製程式碼
  • 對於按鈕防點選來說的實現:一旦我開始一個定時器,只要我定時器還在,不管你怎麼點選都不會執行回撥函式。一旦定時器結束並設定為 null,就可以再次點選了。
  • 對於延時執行函式來說的實現:每次呼叫防抖動函式都會判斷本次呼叫和之前的時間間隔,如果小於需要的時間間隔,就會重新建立一個定時器,並且定時器的延時為設定時間減去之前的時間間隔。一旦時間到了,就會執行相應的回撥函式。
/** * underscore 節流函式,返回函式連續呼叫時,func 執行頻率限定為 次 / wait * * @param  {function
} func 回撥函式 * @param {number
} wait 表示時間視窗的間隔 * @param {object
} options 如果想忽略開始函式的的呼叫,傳入{leading: false
}。 * 如果想忽略結尾函式的呼叫,傳入{trailing: false
} * 兩者不能共存,否則函式不能執行 * @return {function
} 返回客戶呼叫函式 */
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的時間戳 var previous = 0;
// 如果 options 沒傳則設為空物件 if (!options) options = {
};
// 定時器回撥函式 var later = function() {
// 如果設定了 leading,就將 previous 設為 0 // 用於下面函式的第一個 if 判斷 previous = options.leading === false ? 0 : _.now();
// 置空一是為了防止記憶體洩漏,二是為了下面的定時器判斷 timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;

};
return function() {
// 獲得當前時間戳 var now = _.now();
// 首次進入前者肯定為 true // 如果需要第一次不執行函式 // 就將上次時間戳設為當前的 // 這樣在接下來計算 remaining 的值時會大於0 if (!previous &
&
options.leading === false) previous = now;
// 計算剩餘時間 var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果當前呼叫已經大於上次呼叫時間 + wait // 或者使用者手動調了時間 // 如果設定了 trailing,只會進入這個條件 // 如果沒有設定 leading,那麼第一次會進入這個條件 // 還有一點,你可能會覺得開啟了定時器那麼應該不會進入這個 if 條件了 // 其實還是會進入的,因為定時器的延時 // 並不是準確的時間,很可能你設定了2秒 // 但是他需要2.2秒才觸發,這時候就會進入這個條件 if (remaining <
= 0 || remaining >
wait) {
// 如果存在定時器就清理掉否則會呼叫二次回撥 if (timeout) {
clearTimeout(timeout);
timeout = null;

} previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;

} else if (!timeout &
&
options.trailing !== false) {
// 判斷是否設定了定時器和 trailing // 沒有的話就開啟一個定時器 // 並且不能不能同時設定 leading 和 trailing timeout = setTimeout(later, remaining);

} return result;

};

};
複製程式碼

文章來源

來源:https://juejin.im/post/5b7b88d46fb9a019e9767405

相關文章