JavaScript 防抖和節(jié)流遇見(jiàn)的奇怪問(wèn)題及解決
場(chǎng)景
網(wǎng)絡(luò)上已經(jīng)存在了大量的有關(guān) 防抖 和 節(jié)流 的文章,為何吾輩還要再寫(xiě)一篇呢?事實(shí)上,防抖和節(jié)流,吾輩在使用中發(fā)現(xiàn)了一些奇怪的問(wèn)題,并經(jīng)過(guò)了數(shù)次的修改,這里主要分享一下吾輩遇到的問(wèn)題以及是如何解決的。
為什么要用防抖和節(jié)流?
因?yàn)槟承┖瘮?shù)觸發(fā)/調(diào)用的頻率過(guò)快,吾輩需要手動(dòng)去限制其執(zhí)行的頻率。例如常見(jiàn)的監(jiān)聽(tīng)滾動(dòng)條的事件,如果沒(méi)有防抖處理的話,并且,每次函數(shù)執(zhí)行花費(fèi)的時(shí)間超過(guò)了觸發(fā)的間隔時(shí)間的話 – 頁(yè)面就會(huì)卡頓。
演進(jìn)
初始實(shí)現(xiàn)
我們先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的去抖函數(shù)
function debounce(delay, action) {
let tId
return function(...args) {
if (tId) clearTimeout(tId)
tId = setTimeout(() => {
action(...args)
}, delay)
}
}
測(cè)試一下
// 使用 Promise 簡(jiǎn)單封裝 setTimeout,下同
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
好了,看來(lái)基本的效果是實(shí)現(xiàn)了的。包裝過(guò)的函數(shù) fn 調(diào)用了兩次,卻并沒(méi)有立刻執(zhí)行,而是等待時(shí)間間隔過(guò)去之后才最終執(zhí)行了一次。
this 怎么辦?
然而,上面的實(shí)現(xiàn)有一個(gè)致命的問(wèn)題,沒(méi)有處理 this!當(dāng)你用在原生的事件處理時(shí)或許還不覺(jué)得,然而,當(dāng)你使用了 ES6 class 這類(lèi)對(duì) this 敏感的代碼時(shí),就一定會(huì)遇到 this 帶來(lái)的問(wèn)題。
例如下面使用 class 來(lái)聲明一個(gè)計(jì)數(shù)器
class Counter {
constructor() {
this.i = 0
}
add() {
this.i++
}
}
我們可能想在 constructor 中添加新的屬性 fn
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add)
}
add() {
this.i++
}
}
但很遺憾,這里的 this 綁定是有問(wèn)題的,執(zhí)行以下代碼試試看
const counter = new Counter() counter.fn() // Cannot read property 'i' of undefined
會(huì)拋出異常 Cannot read property 'i' of undefined,究其原因就是 this 沒(méi)有綁定,我們可以手動(dòng)綁定 this .bind(this)
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add.bind(this))
}
add() {
this.i++
}
}
但更好的方式是修改 debounce,使其能夠自動(dòng)綁定 this
function debounce(delay, action) {
let tId
return function(...args) {
if (tId) clearTimeout(tId)
tId = setTimeout(() => {
action.apply(this, args)
}, delay)
}
}
然后,代碼將如同預(yù)期的運(yùn)行
;(async () => {
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add)
}
add() {
this.i++
}
}
const counter = new Counter()
counter.add()
counter.add()
console.log(counter.i) // 2
counter.fn()
counter.fn()
console.log(counter.i) // 2
await wait(20)
console.log(counter.i) // 3
})()
返回值呢?
不知道你有沒(méi)有發(fā)現(xiàn),現(xiàn)在使用 debounce 包裝的函數(shù)都沒(méi)有返回值,是完全只有副作用的函數(shù)。然而,吾輩還是遇到了需要返回值的場(chǎng)景。
例如:輸入停止后,使用 Ajax 請(qǐng)求后臺(tái)數(shù)據(jù)判斷是否已存在相同的數(shù)據(jù)。
修改 debounce 成會(huì)緩存上一次執(zhí)行結(jié)果并且有初始結(jié)果參數(shù)的實(shí)現(xiàn)
function debounce(delay, action, init = undefined) {
let flag
let result = init
return function(...args) {
if (flag) clearTimeout(flag)
flag = setTimeout(() => {
result = action.apply(this, args)
}, delay)
return result
}
}
調(diào)用代碼變成了
;(async () => {
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add, 0)
}
add() {
return ++this.i
}
}
const counter = new Counter()
console.log(counter.add()) // 1
console.log(counter.add()) // 2
console.log(counter.fn()) // 0
console.log(counter.fn()) // 0
await wait(20)
console.log(counter.fn()) // 3
})()
看起來(lái)很完美?然而,沒(méi)有考慮到異步函數(shù)是個(gè)大失敗!
嘗試以下測(cè)試代碼
;(async () => {
const get = async i => i
console.log(await get(1))
console.log(await get(2))
const fn = debounce(10, get, 0)
fn(3).then(i => console.log(i)) // fn(...).then is not a function
fn(4).then(i => console.log(i))
await wait(20)
fn(5).then(i => console.log(i))
})()
會(huì)拋出異常 fn(...).then is not a function,因?yàn)槲覀儼b過(guò)后的函數(shù)是同步的,第一次返回的值并不是 Promise 類(lèi)型。
除非我們修改默認(rèn)值
;(async () => {
const get = async i => i
console.log(await get(1))
console.log(await get(2))
// 注意,修改默認(rèn)值為 Promise
const fn = debounce(10, get, new Promise(resolve => resolve(0)))
fn(3).then(i => console.log(i)) // 0
fn(4).then(i => console.log(i)) // 0
await wait(20)
fn(5).then(i => console.log(i)) // 4
})()
支持有返回值的異步函數(shù)
支持異步有兩種思路
- 將異步函數(shù)包裝為同步函數(shù)
- 將包裝后的函數(shù)異步化
第一種思路實(shí)現(xiàn)
function debounce(delay, action, init = undefined) {
let flag
let result = init
return function(...args) {
if (flag) clearTimeout(flag)
flag = setTimeout(() => {
const temp = action.apply(this, args)
if (temp instanceof Promise) {
temp.then(res => (result = res))
} else {
result = temp
}
}, delay)
return result
}
}
調(diào)用方式和同步函數(shù)完全一樣,當(dāng)然,是支持異步函數(shù)的
;(async () => {
const get = async i => i
console.log(await get(1))
console.log(await get(2))
// 注意,修改默認(rèn)值為 Promise
const fn = debounce(10, get, 0)
console.log(fn(3)) // 0
console.log(fn(4)) // 0
await wait(20)
console.log(fn(5)) // 4
})()
第二種思路實(shí)現(xiàn)
const debounce = (delay, action, init = undefined) => {
let flag
let result = init
return function(...args) {
return new Promise(resolve => {
if (flag) clearTimeout(flag)
flag = setTimeout(() => {
result = action.apply(this, args)
resolve(result)
}, delay)
setTimeout(() => {
resolve(result)
}, delay)
})
}
}
調(diào)用方式支持異步的方式
;(async () => {
const get = async i => i
console.log(await get(1))
console.log(await get(2))
// 注意,修改默認(rèn)值為 Promise
const fn = debounce(10, get, 0)
fn(3).then(i => console.log(i)) // 0
fn(4).then(i => console.log(i)) // 4
await wait(20)
fn(5).then(i => console.log(i)) // 5
})()
可以看到,第一種思路帶來(lái)的問(wèn)題是返回值永遠(yuǎn)會(huì)是 舊的 返回值,第二種思路主要問(wèn)題是將同步函數(shù)也給包裝成了異步。利弊權(quán)衡之下,吾輩覺(jué)得第二種思路更加正確一些,畢竟使用場(chǎng)景本身不太可能必須是同步的操作。而且,原本 setTimeout 也是異步的,只是不需要返回值的時(shí)候并未意識(shí)到這點(diǎn)。
避免原函數(shù)信息丟失
后來(lái),有人提出了一個(gè)問(wèn)題,如果函數(shù)上面攜帶其他信息,例如類(lèi)似于 jQuery 的 $,既是一個(gè)函數(shù),但也同時(shí)含有其他屬性,如果使用 debounce 就找不到了呀
一開(kāi)始吾輩立刻想到了復(fù)制函數(shù)上面的所有可遍歷屬性,然后想起了 ES6 的 Proxy 特性 – 這實(shí)在是太魔法了。使用 Proxy 解決這個(gè)問(wèn)題將異常的簡(jiǎn)單 – 因?yàn)槌苏{(diào)用函數(shù),其他的一切操作仍然指向原函數(shù)!
const debounce = (delay, action, init = undefined) => {
let flag
let result = init
return new Proxy(action, {
apply(target, thisArg, args) {
return new Promise(resolve => {
if (flag) clearTimeout(flag)
flag = setTimeout(() => {
resolve((result = Reflect.apply(target, thisArg, args)))
}, delay)
setTimeout(() => {
resolve(result)
}, delay)
})
},
})
}
測(cè)試一下
;(async () => {
const get = async i => i
get.rx = 'rx'
console.log(get.rx) // rx
const fn = debounce(10, get, 0)
console.log(fn.rx) // rx
})()
實(shí)現(xiàn)節(jié)流
以這種思路實(shí)現(xiàn)一個(gè)節(jié)流函數(shù) throttle
/**
* 函數(shù)節(jié)流
* 節(jié)流 (throttle) 讓一個(gè)函數(shù)不要執(zhí)行的太頻繁,減少執(zhí)行過(guò)快的調(diào)用,叫節(jié)流
* 類(lèi)似于上面而又不同于上面的函數(shù)去抖, 包裝后函數(shù)在上一次操作執(zhí)行過(guò)去了最小間隔時(shí)間后會(huì)直接執(zhí)行, 否則會(huì)忽略該次操作
* 與上面函數(shù)去抖的明顯區(qū)別在連續(xù)操作時(shí)會(huì)按照最小間隔時(shí)間循環(huán)執(zhí)行操作, 而非僅執(zhí)行最后一次操作
* 注: 該函數(shù)第一次調(diào)用一定會(huì)執(zhí)行,不需要擔(dān)心第一次拿不到緩存值,后面的連續(xù)調(diào)用都會(huì)拿到上一次的緩存值
* 注: 返回函數(shù)結(jié)果的高階函數(shù)需要使用 {@link Proxy} 實(shí)現(xiàn),以避免原函數(shù)原型鏈上的信息丟失
*
* @param {Number} delay 最小間隔時(shí)間,單位為 ms
* @param {Function} action 真正需要執(zhí)行的操作
* @return {Function} 包裝后有節(jié)流功能的函數(shù)。該函數(shù)是異步的,與需要包裝的函數(shù) {@link action} 是否異步?jīng)]有太大關(guān)聯(lián)
*/
const throttle = (delay, action) => {
let last = 0
let result
return new Proxy(action, {
apply(target, thisArg, args) {
return new Promise(resolve => {
const curr = Date.now()
if (curr - last > delay) {
result = Reflect.apply(target, thisArg, args)
last = curr
resolve(result)
return
}
resolve(result)
})
},
})
}
總結(jié)
嘛,實(shí)際上這里的防抖和節(jié)流仍然是簡(jiǎn)單的實(shí)現(xiàn),其他的像 取消防抖/強(qiáng)制刷新緩存 等功能尚未實(shí)現(xiàn)。當(dāng)然,對(duì)于吾輩而言功能已然足夠了,也被放到了公共的函數(shù)庫(kù) rx-util 中。
以上就是JavaScript 防抖和節(jié)流遇見(jiàn)的奇怪問(wèn)題及解決的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 防抖和節(jié)流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- JavaScript防抖與節(jié)流詳解
- JavaScript 防抖和節(jié)流詳解
- web項(xiàng)目開(kāi)發(fā)之JS函數(shù)防抖與節(jié)流示例代碼
- JS防抖節(jié)流函數(shù)的實(shí)現(xiàn)與使用場(chǎng)景
- JavaScript的防抖和節(jié)流案例
- 如何理解JS函數(shù)防抖和函數(shù)節(jié)流
- Javascript節(jié)流函數(shù)throttle和防抖函數(shù)debounce
- 如何在面試中手寫(xiě)出javascript節(jié)流和防抖函數(shù)
- js節(jié)流防抖應(yīng)用場(chǎng)景,以及在vue中節(jié)流防抖的具體實(shí)現(xiàn)操作
- 關(guān)于JavaScript防抖與節(jié)流的區(qū)別與實(shí)現(xiàn)
相關(guān)文章
原生微信小程序/uniapp使用空格占位符無(wú)效的解決辦法
最近需要在字體中間加空白占位符,在嘗試使用 之后,還是不能使用,下面這篇文章主要給大家介紹了關(guān)于原生微信小程序/uniapp使用空格占位符無(wú)效的解決辦法,需要的朋友可以參考下2023-02-02
jquery實(shí)現(xiàn)動(dòng)靜態(tài)條形統(tǒng)計(jì)圖
這篇文章主要介紹了jquery實(shí)現(xiàn)動(dòng)靜態(tài)條形統(tǒng)計(jì)圖,需要的朋友可以參考下2015-08-08
angular bootstrap timepicker TypeError提示怎么辦
這篇文章主要介紹了angular bootstrap timepicker TypeError提示的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
JS實(shí)現(xiàn)滑動(dòng)門(mén)效果的方法詳解
這篇文章主要介紹了JS實(shí)現(xiàn)滑動(dòng)門(mén)效果的方法,結(jié)合實(shí)例形式分析了滑動(dòng)門(mén)效果的實(shí)現(xiàn)原理、步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-12-12
使用ECharts進(jìn)行數(shù)據(jù)可視化的代碼詳解
ECharts 是一個(gè)由百度開(kāi)源的強(qiáng)大、靈活的 JavaScript 圖表庫(kù),用于在 Web 頁(yè)面上創(chuàng)建各種類(lèi)型的數(shù)據(jù)可視化圖表,它具有豐富的圖表類(lèi)型、強(qiáng)大的配置選項(xiàng)和良好的跨平臺(tái)兼容性,本文介紹了如何使用ECharts進(jìn)行數(shù)據(jù)可視化,需要的朋友可以參考下2024-08-08
基于JavaScript實(shí)現(xiàn)表格滾動(dòng)分頁(yè)
這篇文章主要為大家詳細(xì)介紹了基于JavaScript實(shí)現(xiàn)表格滾動(dòng)分頁(yè),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11

