防抖、節(jié)流
開發(fā)中經(jīng)常會有一些持續(xù)觸發(fā)的事件拘荡,比如scroll臼节、resize、mousemove、input等网缝。頻繁的執(zhí)行回調(diào)巨税,不僅對性能有很大影響,甚至?xí)邢鄳?yīng)跟不上粉臊,造成頁面卡死等現(xiàn)象草添。
針對這種問題有兩種解決方案,防抖和節(jié)流扼仲。
防抖
事件觸發(fā)后的time時(shí)間內(nèi)只執(zhí)行一次果元。原理是維護(hù)一個(gè)延時(shí)器,規(guī)定在time時(shí)間后執(zhí)行函數(shù)犀盟,如果在time時(shí)間內(nèi)再次觸發(fā),則取消之前的延時(shí)器重新設(shè)置蝇狼。所以回調(diào)只在最后執(zhí)行一次阅畴。
- 方式一:
function debounce(func, time = 0) {
if (typeof func !== 'function') {
throw new TypeError('Expect a function')
}
let timer
return function (e) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
func.call(this, e)
clearTimeout(timer)
}, time)
}
}
- 方式二:
const debounce = (func, wait = 0) => {
let timeout = null
let args
function debounced(...arg) {
args = arg
if(timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函數(shù)執(zhí)行結(jié)果
return new Promise((res, rej) => {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch(e) {
rej(e)
}
}, wait)
})
}
// 允許取消
function cancel() {
clearTimeout(timeout)
timeout = null
}
// 允許立即執(zhí)行
function flush() {
cancel()
return func.apply(this, args)
}
debounced.cancel = cancel
debounced.flush = flush
return debounced
}
節(jié)流
在事件觸發(fā)過程中,每隔wait執(zhí)行一次回調(diào)迅耘。
可選參數(shù)三 trailing
贱枣,事件第一次觸發(fā)是否執(zhí)行一次回調(diào)。默認(rèn)true颤专,即第一次觸發(fā)先執(zhí)行一次纽哥。
- 方式一:
function throttle(func, wait = 0, trailing = true) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
let timer
let start = +new Date
return function (e) {
const now = +new Date
const distance = start + wait - now
if (timer) {
clearTimeout(timer)
}
if (now - start >= wait || trailing) { // 每個(gè)wait時(shí)間執(zhí)行一次或第一次觸發(fā)就執(zhí)行一次
func.apply(this, rest)
start = now
trailing = false
} else {
// 事件結(jié)束wait時(shí)間后再執(zhí)行一次
timer = setTimeout(() => {
func.call(this, e)
}, distance )
}
}
}
- 方式二:
function throttlew(fn, wait) {
let start = 0
return function(e) {
const now = Date.now()
if (now - start > wait) {
fn.call(this, e)
start = now
}
}
- 方式三:
const throttle = (func, wait = 0, execFirstCall) => {
let timeout = null
let args
let firstCallTimestamp
function throttled(...arg) {
if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
if (!execFirstCall || !args) {
// console.log('set args:', arg)
args = arg
}
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函數(shù)執(zhí)行結(jié)果
return new Promise(async (res, rej) => {
if (new Date().getTime() - firstCallTimestamp >= wait) {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
} else {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
}, firstCallTimestamp + wait - new Date().getTime())
}
})
}
// 允許取消
function cancel() {
clearTimeout(timeout)
args = null
timeout = null
firstCallTimestamp = null // 這里很關(guān)鍵。每次執(zhí)行完成后都要初始化
}
// 允許立即執(zhí)行
function flush() {
cancel()
return func.apply(this, args)
}
throttled.cancel = cancel
throttled.flush = flush
return throttled
}
區(qū)別: 不管事件觸發(fā)有多頻繁栖秕,節(jié)流都會保證在規(guī)定時(shí)間內(nèi)一定會執(zhí)行一次真正的事件處理函數(shù)春塌,而防抖只是在最后一次事件觸發(fā)后才執(zhí)行一次函數(shù)。
場景:
防抖:比如監(jiān)聽頁面滾動簇捍,滾動結(jié)束并且到達(dá)一定距離時(shí)顯示返回頂部按鈕只壳,適合使用防抖。
節(jié)流:比如在頁面的無限加載場景下暑塑,需要用戶在滾動頁面時(shí)過程中吼句,每隔一段時(shí)間發(fā)一次 Ajax 請求,而不是在用戶停下滾動頁面操作時(shí)才去請求數(shù)據(jù)事格。此場景適合用節(jié)流來實(shí)現(xiàn)惕艳。