欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaScript 防抖和節(jié)流遇見的奇怪問題及解決

 更新時間:2020年11月20日 08:53:24   作者:rxliuli  
這篇文章主要介紹了JavaScript 防抖和節(jié)流遇見的奇怪問題及解決,幫助大家更好的理解和使用JavaScript,感興趣的朋友可以了解下

場景

網(wǎng)絡(luò)上已經(jīng)存在了大量的有關(guān) 防抖 和 節(jié)流 的文章,為何吾輩還要再寫一篇呢?事實上,防抖和節(jié)流,吾輩在使用中發(fā)現(xiàn)了一些奇怪的問題,并經(jīng)過了數(shù)次的修改,這里主要分享一下吾輩遇到的問題以及是如何解決的。

為什么要用防抖和節(jié)流?

因為某些函數(shù)觸發(fā)/調(diào)用的頻率過快,吾輩需要手動去限制其執(zhí)行的頻率。例如常見的監(jiān)聽滾動條的事件,如果沒有防抖處理的話,并且,每次函數(shù)執(zhí)行花費的時間超過了觸發(fā)的間隔時間的話 – 頁面就會卡頓。

演進(jìn)

初始實現(xiàn)

我們先實現(xiàn)一個簡單的去抖函數(shù)

function debounce(delay, action) {
 let tId
 return function(...args) {
  if (tId) clearTimeout(tId)
  tId = setTimeout(() => {
   action(...args)
  }, delay)
 }
}

測試一下

// 使用 Promise 簡單封裝 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
})()

好了,看來基本的效果是實現(xiàn)了的。包裝過的函數(shù) fn 調(diào)用了兩次,卻并沒有立刻執(zhí)行,而是等待時間間隔過去之后才最終執(zhí)行了一次。

this 怎么辦?

然而,上面的實現(xiàn)有一個致命的問題,沒有處理 this!當(dāng)你用在原生的事件處理時或許還不覺得,然而,當(dāng)你使用了 ES6 class 這類對 this 敏感的代碼時,就一定會遇到 this 帶來的問題。

例如下面使用 class 來聲明一個計數(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 綁定是有問題的,執(zhí)行以下代碼試試看

const counter = new Counter()
counter.fn() // Cannot read property 'i' of undefined

會拋出異常 Cannot read property 'i' of undefined,究其原因就是 this 沒有綁定,我們可以手動綁定 this .bind(this)

class Counter {
 constructor() {
  this.i = 0
  this.fn = debounce(10, this.add.bind(this))
 }
 add() {
  this.i++
 }
}

但更好的方式是修改 debounce,使其能夠自動綁定 this

function debounce(delay, action) {
 let tId
 return function(...args) {
  if (tId) clearTimeout(tId)
  tId = setTimeout(() => {
   action.apply(this, args)
  }, delay)
 }
}

然后,代碼將如同預(yù)期的運行

;(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
})()

返回值呢?

不知道你有沒有發(fā)現(xiàn),現(xiàn)在使用 debounce 包裝的函數(shù)都沒有返回值,是完全只有副作用的函數(shù)。然而,吾輩還是遇到了需要返回值的場景。
例如:輸入停止后,使用 Ajax 請求后臺數(shù)據(jù)判斷是否已存在相同的數(shù)據(jù)。

修改 debounce 成會緩存上一次執(zhí)行結(jié)果并且有初始結(jié)果參數(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
})()

看起來很完美?然而,沒有考慮到異步函數(shù)是個大失??!

嘗試以下測試代碼

;(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))
})()

會拋出異常 fn(...).then is not a function,因為我們包裝過后的函數(shù)是同步的,第一次返回的值并不是 Promise 類型。

除非我們修改默認(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ù)

支持異步有兩種思路

  1. 將異步函數(shù)包裝為同步函數(shù)
  2. 將包裝后的函數(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
})()

第二種思路實現(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
})()

可以看到,第一種思路帶來的問題是返回值永遠(yuǎn)會是 舊的 返回值,第二種思路主要問題是將同步函數(shù)也給包裝成了異步。利弊權(quán)衡之下,吾輩覺得第二種思路更加正確一些,畢竟使用場景本身不太可能必須是同步的操作。而且,原本 setTimeout 也是異步的,只是不需要返回值的時候并未意識到這點。

避免原函數(shù)信息丟失

后來,有人提出了一個問題,如果函數(shù)上面攜帶其他信息,例如類似于 jQuery 的 $,既是一個函數(shù),但也同時含有其他屬性,如果使用 debounce 就找不到了呀

一開始吾輩立刻想到了復(fù)制函數(shù)上面的所有可遍歷屬性,然后想起了 ES6 的 Proxy 特性 – 這實在是太魔法了。使用 Proxy 解決這個問題將異常的簡單 – 因為除了調(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)
   })
  },
 })
}

測試一下

;(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
})()

實現(xiàn)節(jié)流

以這種思路實現(xiàn)一個節(jié)流函數(shù) throttle

/**
 * 函數(shù)節(jié)流
 * 節(jié)流 (throttle) 讓一個函數(shù)不要執(zhí)行的太頻繁,減少執(zhí)行過快的調(diào)用,叫節(jié)流
 * 類似于上面而又不同于上面的函數(shù)去抖, 包裝后函數(shù)在上一次操作執(zhí)行過去了最小間隔時間后會直接執(zhí)行, 否則會忽略該次操作
 * 與上面函數(shù)去抖的明顯區(qū)別在連續(xù)操作時會按照最小間隔時間循環(huán)執(zhí)行操作, 而非僅執(zhí)行最后一次操作
 * 注: 該函數(shù)第一次調(diào)用一定會執(zhí)行,不需要擔(dān)心第一次拿不到緩存值,后面的連續(xù)調(diào)用都會拿到上一次的緩存值
 * 注: 返回函數(shù)結(jié)果的高階函數(shù)需要使用 {@link Proxy} 實現(xiàn),以避免原函數(shù)原型鏈上的信息丟失
 *
 * @param {Number} delay 最小間隔時間,單位為 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é)

嘛,實際上這里的防抖和節(jié)流仍然是簡單的實現(xiàn),其他的像 取消防抖/強(qiáng)制刷新緩存 等功能尚未實現(xiàn)。當(dāng)然,對于吾輩而言功能已然足夠了,也被放到了公共的函數(shù)庫 rx-util 中。

以上就是JavaScript 防抖和節(jié)流遇見的奇怪問題及解決的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 防抖和節(jié)流的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論