有以下代碼git
let n = 1
window.onmousemove = () => {
console.log(`第${n}次觸發回調`)
n++
}
複製代碼
當咱們在PC端頁面上滑動鼠標時,一秒能夠能夠觸發約60次事件。你們也能夠訪問下面的在線例子進行測試。github
查看在線例子: 函數節流-監聽鼠標移動觸發次數測試 by Logan (@logan70) on CodePen.瀏覽器
這裏的回調函數只是打印字符串,若是回調函數更加複雜,可想而知瀏覽器的壓力會很是大,可能下降用戶體驗。app
resize
、scroll
或mousemove
等事件的監聽回調會被頻繁觸發,所以咱們要對其進行限制。async
函數節流簡單來講就是對於連續的函數調用,每間隔一段時間,只讓其執行一次。初步的實現思路有兩種:函數
設置一個對比時間戳,觸發事件時,使用當前時間戳減去對比時間戳,若是差值大於設定的間隔時間,則執行函數,並用當前時間戳替換對比時間戳;若是差值小於設定的間隔時間,則不執行函數。post
function throttle(method, wait) {
// 對比時間戳,初始化爲0則首次觸發當即執行,初始化爲當前時間戳則wait毫秒後觸發纔會執行
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// 間隔大於wait則執行method並更新對比時間戳
if (now - previous > wait) {
method.apply(context, args)
previous = now
}
}
}
複製代碼
查看在線例子: 函數節流-初步實現之時間戳 by Logan (@logan70) on CodePen.測試
當首次觸發事件時,設置定時器,wait毫秒後執行函數並將定時器置爲null
,以後觸發事件時,若是定時器存在則不執行,若是定時器不存在則再次設置定時器。優化
function throttle(method, wait) {
let timeout
return function(...args) {
let context = this
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
method.apply(context, args)
}, wait)
}
}
}
複製代碼
查看在線例子: 函數節流-初步實現之定時器 by Logan (@logan70) on CodePen.ui
mousemove
)mousedown/keydown
事件(單位時間只能發射一顆子彈)mousemove
)mousemove
)keyup
)scroll
加了 debounce
後,只有用戶中止滾動後,纔會判斷是否到了頁面底部;若是是 throttle
的話,只要頁面滾動就會間隔一段時間判斷一次代碼說話,有錯懇請指出
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result 記錄method的執行返回值
let timeout, result
// 記錄上次原函數執行的時間(非每次更新)
let methodPrevious = 0
// 記錄上次回調觸發時間(每次都更新)
let throttledPrevious = 0
let throttled = function(...args) {
let context = this
// 使用Promise,能夠在觸發回調時拿到原函數執行的返回值
return new Promise(resolve => {
let now = new Date().getTime()
// 兩次相鄰觸發的間隔
let interval = now - throttledPrevious
// 更新本次觸發時間供下次使用
throttledPrevious = now
// 重置methodPrevious爲now,remaining = wait > 0,僞裝剛執行過,實現禁止當即執行
// 統一條件:leading爲false
// 加上如下條件之一
// 1. 首次觸發(此時methodPrevious爲0)
// 2. trailing爲true時,中止觸發時間超過wait,定時器內函數執行(methodPrevious被置爲0),而後再次觸發
// 3. trailing爲false時(不設定時器,methodPrevious不會被置爲0),中止觸發時間超過wait後再次觸發(interval > wait)
if (leading === false && (!methodPrevious || interval > wait)) {
methodPrevious = now
// 保險起見,清除定時器並置爲null
// 僞裝剛執行過要僞裝的完全XD
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
// 距離下次執行原函數的間隔
let remaining = wait - (now - methodPrevious)
// 1. leading爲true時,首次觸發就當即執行
// 2. 到達下次執行原函數時間
// 3. 修改了系統時間
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 更新對比時間戳,執行函數並記錄返回值,傳給resolve
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// 解除引用,防止內存泄漏
if (!timeout) context = args = null
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
// leading爲false時將methodPrevious設爲0的目的在於
// 若不將methodPrevious設爲0,若是定時器觸發後很長時間沒有觸發回調
// 下次觸發時的remaining爲負,原函數會當即執行,違反了leading爲false的設定
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// 解除引用,防止內存泄漏
if (!timeout) context = args = null
}, remaining)
}
})
}
// 加入取消功能,使用方法以下
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
複製代碼
調用節流後的函數的外層函數也須要使用Async/Await語法等待執行結果返回
使用方法見代碼:
function square(num) {
return Math.pow(num, 2)
}
// let throttledFn = throttle(square, 1000)
// let throttledFn = throttle(square, 1000, {leading: false})
// let throttledFn = throttle(square, 1000, {trailing: false})
let throttledFn = throttle(square, 1000, {leading: false, trailing: false})
window.onmousemove = async () => {
try {
let val = await throttledFn(4)
// 原函數不執行時val爲undefined
if (typeof val !== 'undefined') {
console.log(`原函數返回值爲${val}`)
}
} catch (err) {
console.error(err)
}
}
// 鼠標移動時,每間隔1S輸出:
// 原函數的返回值爲:16
複製代碼
查看在線例子: 函數節流-最終版 by Logan (@logan70) on CodePen.
具體的實現步驟請往下看
這樣實現的效果是首次觸發當即執行,中止觸發後會再執行一次
function throttle(method, wait) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// 距離下次函數執行的剩餘時間
let remaining = wait - (now - previous)
// 若是無剩餘時間或系統時間被修改
if (remaining <= 0 || remaining > wait) {
// 若是定時器還存在則清除並置爲null
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 更新對比時間戳並執行函數
previous = now
method.apply(context, args)
} else if (!timeout) {
// 若是有剩餘時間但定時器不存在,則設置定時器
// remaining毫秒後執行函數、更新對比時間戳
// 並將定時器置爲null
timeout = setTimeout(() => {
previous = new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
複製代碼
咱們來捋一捋,假設連續觸發回調:
查看在線例子: 函數節流-優化初版:融合兩種實現方式 by Logan (@logan70) on CodePen.
// leading爲控制首次觸發時是否當即執行函數的配置項
function throttle(method, wait, leading = true) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// !previous表明首次觸發或定時器觸發後的首次觸發,若不須要當即執行則將previous更新爲now
// 這樣remaining = wait > 0,則不會當即執行,而是設定定時器
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout) {
timeout = setTimeout(() => {
// 若是leading爲false,則將previous設爲0,
// 下次觸發時會與下次觸發時的now同步,達到首次觸發(對於用戶來講)不當即執行
// 若是直接設爲當前時間戳,若中止觸發一段時間,下次觸發時的remaining爲負值,會當即執行
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
複製代碼
查看在線例子: 函數節流-優化第二版:提供首次觸發時是否當即執行的配置項 by Logan (@logan70) on CodePen.
// trailing爲控制中止觸發後是否還執行一次的配置項
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout && trailing !== false) {
// 若是有剩餘時間但定時器不存在,且trailing不爲false,則設置定時器
// trailing爲false時等同於只使用時間戳來實現節流
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
複製代碼
查看在線例子: 函數節流-優化第三版:提供中止觸發後是否還執行一次的配置項 by Logan (@logan70) on CodePen.
有些時候咱們須要在不可觸發的這段時間內可以手動取消節流,代碼實現以下:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
// 將返回的匿名函數賦值給throttled,以便在其上添加取消方法
let throttled = function(...args) {
let context = this
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
// 加入取消功能,使用方法以下
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
// 將節流後函數返回
return throttled
}
複製代碼
查看在線例子: 函數節流-優化第四版:提供取消功能 by Logan (@logan70) on CodePen.
須要節流的函數多是存在返回值的,咱們要對這種狀況進行處理,underscore
的處理方法是將函數返回值在返回的debounced
函數內再次返回,可是這樣實際上是有問題的。若是原函數執行在setTimeout
內,則沒法同步拿到返回值,咱們使用Promise處理原函數返回值。
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result記錄原函數執行結果
let timeout, result
let previous = 0
let throttled = function(...args) {
let context = this
// 返回一個Promise,以即可以使用then或者Async/Await語法拿到原函數返回值
return new Promise(resolve => {
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = method.apply(context, args)
// 將函數執行返回值傳給resolve
resolve(result)
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
// 將函數執行返回值傳給resolve
resolve(result)
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
複製代碼
使用方法一:在調用節流後的函數時,使用then
拿到原函數的返回值
function square(num) {
return Math.pow(num, 2)
}
let throttledFn = throttle(square, 1000, false)
window.onmousemove = () => {
throttledFn(4).then(val => {
console.log(`原函數的返回值爲:${val}`)
})
}
// 鼠標移動時,每間隔1S後輸出:
// 原函數的返回值爲:16
複製代碼
使用方法二:調用節流後的函數的外層函數使用Async/Await語法等待執行結果返回
使用方法見代碼:
function square(num) {
return Math.pow(num, 2)
}
let throttledFn = throttle(square, 1000)
window.onmousemove = async () => {
try {
let val = await throttledFn(4)
// 原函數不執行時val爲undefined
if (typeof val !== 'undefined') {
console.log(`原函數返回值爲${val}`)
}
} catch (err) {
console.error(err)
}
}
// 鼠標移動時,每間隔1S輸出:
// 原函數的返回值爲:16
複製代碼
查看在線例子: 函數節流-優化第五版:處理原函數返回值 by Logan (@logan70) on CodePen.
模仿underscore
實現的函數節流有一點美中不足,那就是 leading:false
和 trailing: false
不能同時設置。
若是同時設置的話,好比當你將鼠標移出的時候,由於 trailing
設置爲 false
,中止觸發的時候不會設置定時器,因此只要再過了設置的時間,再移入的話,remaining
爲負數,就會馬上執行,就違反了 leading: false
,這裏咱們優化的思路以下:
計算連續兩次觸發回調的時間間隔,若是大於設定的間隔值時,重置對比時間戳爲當前時間戳,這樣就至關於回到了首次觸發,達到禁止首次觸發(僞)當即執行的效果,代碼以下,有錯懇請指出:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout, result
let methodPrevious = 0
// 記錄上次回調觸發時間(每次都更新)
let throttledPrevious = 0
let throttled = function(...args) {
let context = this
return new Promise(resolve => {
let now = new Date().getTime()
// 兩次觸發的間隔
let interval = now - throttledPrevious
// 更新本次觸發時間供下次使用
throttledPrevious = now
// 更改條件,兩次間隔時間大於wait且leading爲false時也重置methodPrevious,實現禁止當即執行
if (leading === false && (!methodPrevious || interval > wait)) {
methodPrevious = now
}
let remaining = wait - (now - methodPrevious)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// 解除引用,防止內存泄漏
if (!timeout) context = args = null
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// 解除引用,防止內存泄漏
if (!timeout) context = args = null
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
methodPrevious = 0
timeout = null
}
return throttled
}
複製代碼
查看在線例子: 函數節流-優化第六版:可同時禁用當即執行和後置執行 by Logan (@logan70) on CodePen.
JavaScript專題之跟着 underscore 學節流
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。