2020-4-29 seo達(dá)人
函數(shù)節(jié)流與函數(shù)防抖是我們解決頻繁觸發(fā)DOM事件的兩種常用解決方案,但是經(jīng)常傻傻分不清楚。。。這不,在項(xiàng)目中又用遇到了,在此處記錄一下
函數(shù)防抖 debounce
原理:將若干函數(shù)調(diào)用合成為一次,并在給定時(shí)間過去之后,或者連續(xù)事件完全觸發(fā)完成之后,調(diào)用一次(僅僅只會(huì)調(diào)用一次!?。。。。。。。?!)。
舉個(gè)栗子:滾動(dòng)scroll事件,不?;瑒?dòng)滾輪會(huì)連續(xù)觸發(fā)多次滾動(dòng)事件,從而調(diào)用綁定的回調(diào)函數(shù),我們希望當(dāng)我們停止?jié)L動(dòng)的時(shí),才觸發(fā)一次回調(diào),這時(shí)可以使用函數(shù)防抖。
原理性代碼及測試:
// 給盒子較大的height,容易看到效果
<style>
* {
padding: 0;
margin: 0;
}
.box {
width: 800px;
height: 1200px;
}
</style>
<body>
<div class="container">
<div class="box" style="background: tomato"></div>
<div class="box" style="background: skyblue"></div>
<div class="box" style="background: red"></div>
<div class="box" style="background: yellow"></div>
</div>
<script>
window.onload = function() {
const decounce = function(fn, delay) {
let timer = null
return function() {
const context = this
let args = arguments
clearTimeout(timer) // 每次調(diào)用debounce函數(shù)都會(huì)將前一次的timer清空,確保只執(zhí)行一次
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
let num = 0
function scrollTap() {
num++
console.log(看看num吧 ${num}
)
}
// 此處的觸發(fā)時(shí)間間隔設(shè)置的很小
document.addEventListener('scroll', decounce(scrollTap, 500))
// document.addEventListener('scroll', scrollTap)
}
</script>
</body>
此處的觸發(fā)時(shí)間間隔設(shè)置的很小,如果勻速不間斷的滾動(dòng),不斷觸發(fā)scroll事件,如果不用debounce處理,可以發(fā)現(xiàn)num改變了很多次,用了debounce函數(shù)防抖,num在一次上時(shí)間的滾動(dòng)中只改變了一次。
調(diào)用debouce使scrollTap防抖之后的結(jié)果:
直接調(diào)用scrollTap的結(jié)果:
補(bǔ)充:瀏覽器在處理setTimeout和setInterval時(shí),有最小時(shí)間間隔。
setTimeout的最短時(shí)間間隔是4毫秒;
setInterval的最短間隔時(shí)間是10毫秒,也就是說,小于10毫秒的時(shí)間間隔會(huì)被調(diào)整到10毫秒。
事實(shí)上,未優(yōu)化時(shí),scroll事件頻繁觸發(fā)的時(shí)間間隔也是這個(gè)最小時(shí)間間隔。
也就是說,當(dāng)我們?cè)赿ebounce函數(shù)中的間隔事件設(shè)置不恰當(dāng)(小于這個(gè)最小時(shí)間間隔),會(huì)使debounce無效。
函數(shù)節(jié)流 throttle
原理:當(dāng)達(dá)到了一定的時(shí)間間隔就會(huì)執(zhí)行一次;可以理解為是縮減執(zhí)行頻率
舉個(gè)栗子:還是以scroll滾動(dòng)事件來說吧,滾動(dòng)事件是及其消耗瀏覽器性能的,不停觸發(fā)。以我在項(xiàng)目中碰到的問題,移動(dòng)端通過scroll實(shí)現(xiàn)分頁,不斷滾動(dòng),我們不希望不斷發(fā)送請(qǐng)求,只有當(dāng)達(dá)到某個(gè)條件,比如,距離手機(jī)窗口底部150px才發(fā)送一個(gè)請(qǐng)求,接下來就是展示新頁面的請(qǐng)求,不停滾動(dòng),如此反復(fù);這個(gè)時(shí)候就得用到函數(shù)節(jié)流。
原理性代碼及實(shí)現(xiàn)
// 函數(shù)節(jié)流 throttle
// 方法一:定時(shí)器實(shí)現(xiàn)
const throttle = function(fn,delay) {
let timer = null
return function() {
const context = this
let args = arguments
if(!timer) {
timer = setTimeout(() => {
fn.apply(context,args)
clearTimeout(timer)
},delay)
}
}
}
// 方法二:時(shí)間戳
const throttle2 = function(fn, delay) {
let preTime = Date.now()
return function() {
const context = this
let args = arguments
let doTime = Date.now()
if (doTime - preTime >= delay) {
fn.apply(context, args)
preTime = Date.now()
}
}
}
需要注意的是定時(shí)器方法實(shí)現(xiàn)throttle方法和debounce方法的不同:
在debounce中:在執(zhí)行setTimeout函數(shù)之前總會(huì)將timer用setTimeout清除,取消延遲代碼塊,確保只執(zhí)行一次
在throttle中:只要timer存在就會(huì)執(zhí)行setTimeout,在setTimeout內(nèi)部每次清空這個(gè)timer,但是延遲代碼塊已經(jīng)執(zhí)行啦,確保一定頻率執(zhí)行一次
我們依舊可以在html頁面中進(jìn)行測試scroll事件,html和css代碼同debounce,此處不贅述,運(yùn)行結(jié)果是(可以說是一場漫長的滾輪滾動(dòng)了):
最后再來瞅瞅項(xiàng)目中封裝好的debounce和throttle函數(shù),可以說是很優(yōu)秀了,考慮的特別全面,希望自己以后封裝的函數(shù)也能考慮的這么全面吧,加油!
/*
空閑控制 返回函數(shù)連續(xù)調(diào)用時(shí),空閑時(shí)間必須大于或等于 wait,func 才會(huì)執(zhí)行
@param {function} func 傳入函數(shù),最后一個(gè)參數(shù)是額外增加的this對(duì)象,.apply(this, args) 這種方式,this無法傳遞進(jìn)函數(shù)
@param {number} wait 表示時(shí)間窗口的間隔
@param {boolean} immediate 設(shè)置為ture時(shí),調(diào)用觸發(fā)于開始邊界而不是結(jié)束邊界
@return {function} 返回客戶調(diào)用函數(shù)
/
const debounce = function(func, wait, immediate) {
let timeout, args, context, timestamp, result;
const later = function() {
// 據(jù)上一次觸發(fā)時(shí)間間隔
let last = Number(new Date()) - timestamp;
// 上次被包裝函數(shù)被調(diào)用時(shí)間間隔last小于設(shè)定時(shí)間間隔wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
// 如果設(shè)定為immediate===true,因?yàn)殚_始邊界已經(jīng)調(diào)用過了此處無需調(diào)用
if (!immediate) {
result = func.call(context, ...args, context);
if (!timeout) {
context = args = null;
}
}
}
};
return function(..._args) {
context = this;
args = _args;
timestamp = Number(new Date());
const callNow = immediate && !timeout;
// 如果延時(shí)不存在,重新設(shè)定延時(shí)
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) {
result = func.call(context, ...args, context);
context = args = null;
}
return result;
};
};
/*
頻率控制 返回函數(shù)連續(xù)調(diào)用時(shí),func 執(zhí)行頻率限定為 次 / wait
@param {function} func 傳入函數(shù)
@param {number} wait 表示時(shí)間窗口的間隔
@param {object} options 如果想忽略開始邊界上的調(diào)用,傳入{leading: false}。
如果想忽略結(jié)尾邊界上的調(diào)用,傳入{trailing: false}
@return {function} 返回客戶調(diào)用函數(shù)
*/
const throttle = function(func, wait, options) {
let context, args, result;
let timeout = null;
// 上次執(zhí)行時(shí)間點(diǎn)
let previous = 0;
if (!options) options = {};
// 延遲執(zhí)行函數(shù)
let later = function() {
// 若設(shè)定了開始邊界不執(zhí)行選項(xiàng),上次執(zhí)行時(shí)間始終為0
previous = options.leading === false ? 0 : Number(new Date());
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function(..._args) {
let now = Number(new Date());
// 首次執(zhí)行時(shí),如果設(shè)定了開始邊界不執(zhí)行選項(xiàng),將上次執(zhí)行時(shí)間設(shè)定為當(dāng)前時(shí)間。
if (!previous && options.leading === false) previous = now;
// 延遲執(zhí)行時(shí)間間隔
let remaining = wait - (now - previous);
context = this;
args = _args;
// 延遲時(shí)間間隔remaining小于等于0,表示上次執(zhí)行至此所間隔時(shí)間已經(jīng)超過一個(gè)時(shí)間窗口
// remaining大于時(shí)間窗口wait,表示客戶端系統(tǒng)時(shí)間被調(diào)整過
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
//如果延遲執(zhí)行不存在,且沒有設(shè)定結(jié)尾邊界不執(zhí)行選項(xiàng)
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
藍(lán)藍(lán)設(shè)計(jì)的小編 http://tweetduck.com