防抖就是指觸發事件後在 n 秒內函數只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。
說起防抖大家肯定會想到節流,著兩個就跟一對雙胞胎一樣,讓大家經常傻傻搞不清楚
我們先來看一下節流的概念
節流就是指連續觸發事件但是在 n 秒中只執行一次函數。節流會稀釋函數的執行頻率
我拿電梯關門舉個例子吧:
防抖
你按了電梯關門按鈕,電梯還有三秒要關閉,在你要關閉前的1.5s,按了一次開門按鈕電梯將會重新將要關閉時間重置為3秒
節流
你按了電梯關門按鈕,在電梯將要關閉的三秒內,你再怎麼按電梯按鈕都不會響應
防抖
在前端開發中會遇到一些頻繁的事件觸發,比如:
mousedown、mousemove
keyup、keydown
節流
在頁面的無限載入場景下,我們需要使用者在捲動頁面時,每隔一段時間發一次 Ajax 請求,而不是在使用者停下捲動頁面操作時才去請求資料。這樣的場景,就適合用節流技術來實現。
function debounce(func, wait) {
return function () {
let timer
clearTimeout(timer)
timer = setTimeout(() => {
func
}, wait)
}
}
這個是最簡單的一版debounce的實現方式,但是缺點有許多,我們先來看第一個
一,this 指向問題
我們可知在setTimeout 中this 指向windows 詳情可 MDN看詳細說明
function debounce(func, wait) {
return function () {
let timer
const contentThis = this
clearTImeout(timer)
timer = setTimeout(() => {
func.apply(contentThis)
}, wait)
}
}
第二版中,this指向是正常情況,但是如果func 有引數的話,就會導致引數的丟失,所以我們開啟第三版
function debounce(func, wait) {
return function () {
let timer
const contentThis = this
let arg = arguments
clearTImeout(timer)
timer = setTimeout(() => {
func.apply(contentThis, arg)
}, wait)
}
}
第三版完後,大家會發現還有一個問題,這個debounce 並不能立即執行,下面我們再改一下程式碼
function debounce(func, wait, immediate) {
return function () {
let timer = null
const _this = this
let arg = arguments
if (timer) clearTImeout(timer)
if (immediate) {
let runNow = !tiemr
timer = setTimeout(function () {
timer = null
}, wait)
if (runNow) {
func.apply(_this, arg)
}
} else {
timer = setTimeout(() => {
func.apply(contentThis, arg)
}, wait)
}
}
}
我們還需要注意一種情況,當func 函數有返回值時我們需要將返回值帶上,但是會有一個問題,當setTimeOut this指向的問題,導致再延時器中返回的資料只能是undefined,所以我們只能再immediate上新增
function debounce(func, wait, immediate) {
return function () {
let result
let timer = null
const _this = this
let arg = arguments
if (timer) clearTImeout(timer)
if (immediate) {
let runNow = !tiemr
timer = setTimeout(function () {
timer = null
}, wait)
if (runNow) {
result = func.apply(_this, arg)
}
} else {
timer = setTimeout(() => {
func.apply(contentThis, arg)
}, wait)
}
return result
}
}
_.debounce = function (func, wait, immediate) {
var timeout, result;
var later = function (context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArgs(function (args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
節流的原理我們在上面已經說過了,每隔一段時間就只執行一次事件
我們先看第一種方式
function throttle(func, wait) {
let previous = 0
return function () {
let now = +new Date()
let _this = this
let arg = arguments
if (now - previous > wait) {
func.apply(_this, arg)
previous = now
}
}
}
使用方式如下
content.onmousemove = throttle(count,1000);
function throttle(func, wait) {
return function () {
let timer = null
let arg = arguments
if (!timer) {
timer = setTimeout(() => {
timer = null
func.apply(context, arg)
}, wait)
}
}
}
現在我們分析一下,上面兩版
第一種事件會立刻執行,第二種事件會在 n 秒後第一次執行
第一種事件停止觸發後沒有辦法再執行事件,第二種事件停止觸發後依然會再執行一次事件
所以我們將這兩版總結一下,實現一版,滑鼠移入能立即執行,但是停止觸發的時候還能再次執行一次,程式碼如下:
function throttle(func, wait) {
let previous = 0
let later = function () {
previous = +new Date()
tiemr = null
func.apply(_this, arg)
}
let throttled = function () {
let now = +new Date()
let remaining = wait - (now - previous)
let _this = this
let arg = arguments
let tiemr = null
if (remaining <= 0 || remaining > wait) {
if (tiemr) {
clearTimeout(tiemr)
timer = null
}
previous = now
func.apply(_this, arg)
} else if (!tiemr) {
tiemr = setTimeout(later, remaining)
}
}
return throttled
}
上面的程式碼還有許多侷限,比如說,我想要實現,滑鼠移入不執行,但是停止觸發的時候再執行一次的效果,或者滑鼠移入立即執行,但是停止觸發的時候不再執行的效果。對於這種情況我們應該如何去做呢?
那我們設定個 options 作為第三個引數,然後根據傳的值判斷到底哪種效果,我們約定:
leading:false 表示禁用第一次執行
trailing: false 表示禁用停止觸發的回撥
我們來優化一下程式碼
function throttle(func, wait, options) {
let previous = 0
if (!options) {
options = {}
}
let later = function () {
previous = options.leading === false ? 0 : new Date().getTime()
tiemr = null
func.apply(_this, arg)
if (!timeout) _this = args = null
}
let throttled = function () {
let now = new Date().getTime()
if (!previous && options.leading === false) previous = now
let remaining = wait - (now - previous)
let _this = this
arg = arguments
if (remaining <= 0 || remaining > wait) {
if (tiemr) {
clearTimeout(timer)
timer = null
}
previous = now
func.apply(_this, arg)
if (!tiemr) _this = arg = null
} else if (!tiemr && options.trailing !== false) {
timer = setTimeout(later, remaining)
}
}
return throttled
}
我們要注意有這樣一個問題:
那就是 leading:false 和 trailing: false 不能同時設定。
如果同時設定的話,比如當你將滑鼠移出的時候,因為 trailing 設定為 false,停止觸發的時候不會設定定時器,所以只要再過了設定的時間,再移入的話,就會立刻執行,就違反了 leading: false,bug 就出來了,所以,這個 throttle 只有三種用法:
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});
以上就完成了函數防抖和節流的實現
最近我在react hook 中使用防抖和節流發現不起作用,最後發現是自己沒有掌握react hook 的機制導致的
當元件元件重新渲染的時候,hooks 會重新執行一遍,這樣debounce高階函數裡面的timer就不能起到快取的作用(每次重渲染都被置空)。timer不可靠,debounce的核心就被破壞了。
所以我們可以利用React元件的快取機制重新實現一個react hook 版的防抖和節流
import * as React from 'react'
export function useDebounce(fn, delay, dep = []) {
const { current } = React.useRef({ fn, timer: null })
React.useEffect(
function () {
current.fn = fn
},
[fn]
)
return React.useCallback(function f(...args) {
if (current.timer) {
clearTimeout(current.timer)
}
current.timer = setTimeout(() => {
current.fn.call(this, ...args)
}, delay)
}, dep)
}
import * as React from 'react'
export function useThrottle(fn, delay, dep = []) {
const { current } = React.useRef({ fn, timer: null })
React.useEffect(
function () {
current.fn = fn
},
[fn]
)
return React.useCallback(function f(...args) {
if (!current.timer) {
current.timer = setTimeout(() => {
delete current.timer
}, delay)
current.fn.call(this, ...args)
}
}, dep)
}
我在實現防抖的時候還遇到遇到一個問題,我再這一起記錄一下吧
當我在實現react中觸發input 元件中的onChange 事件時去傳送一個非同步請求,但是發現一直獲取不到value 值,程式碼如下
const onSearchChange = (e) => {
e.persist()
e.preventDefault()
e.stopPropagation()
setSearchValue(e.currentTarget.value)
useDebounce((e) => {
setFilterValue(e.currentTarget.value)
}, 500)
}
就是setFilterValue 一直獲取不到值,最後經過調查發現了原由,
首先,在 onSearchChange 中,事件觸發,就能獲取到 event 物件,其中主要就是 event.target 就是當前觸發事件的 dom 物件,由於 useDebounce延遲執行,導致了 onSearchChange 函數已經執行完了,進入了 react-dom 中相關一系列操作(進行了一系列複雜的操作),下面給出最關鍵的 executeDispatchesAndRelease,executeDispatchesAndRelease 方法釋放 event.target 的值
/**
* Dispatches an event and releases it back into the pool, unless persistent.
*
* @param {?object} event Synthetic event to be dispatched.
* @private
*/
var executeDispatchesAndRelease = function(event) {
if (event) {
executeDispatchesInOrder(event);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
由於 event 在 useDebounce中作為了引數,記憶體中沒有清除,執行上面的方法 event.target = null; event 為參照型別,一處改變,所有用到的地方都會改變。導致了 useDebounce中 event 也發生了變化。
解決辦法:
const onSearchChange = (e) => {
e.persist()
e.preventDefault()
e.stopPropagation()
setSearchValue(e.currentTarget.value)
handleSearchChange(e.currentTarget.value)
}
const handleSearchChange = useDebounce((value) => {
setFilterValue(value)
}, 500)