js函式節流與防抖
阿新 • • 發佈:2020-12-22
一、節流(throttle)與防抖(debounce)的含義
節流:
指連續觸發事件但是在 n 秒中只執行一次函式。即 2n 秒內執行 2 次,3n秒內執行3次… 。節流如字面意思,會稀釋函式的執行頻率。
使用場景:dom元素拖拽,搜尋聯想等等
防抖:
連續的事件響應我們在n秒內只執行一次回撥。如n秒內觸發6次,只在最後一次再執行回撥。
使用場景:文字輸入驗證
二、簡單實現
節流:
function throttle(fn,wait){
// 首先獲取呼叫throttle時的一個時間戳作為觸發時時間,實現用閉包儲存 pre 變數。
var pre = Date.now();
return function(){
var context = this;
var args = arguments;
var now = Date.now();
if( now - pre >= wait){ // 噹噹前時間-出發時時間大於等待時間後,觸發fn函式執行
fn.apply(context,args);
pre = Date.now(); // 更新觸發時間
} else{
//讓方法在脫離事件後也能執行一次
timeout = setTimeout(function(){
fn.apply(context, args)
}, wait);
}
}
}
由此可以實現,在wait時間範圍內,只執行一次,下一個時間窗內,會再次觸發。呼叫方式:比如在2秒後呼叫handleSth()方法: throttle(handleSth, 2000)
防抖:
// 1、非立即執行版:一開始不觸發delay秒後才會執行
function debounce(func, delay) {
let timer = null;
return function() {
clearTimeout (timer);
timer = setTimeout(() => {
func.apply(this, arguments)
// 具體分析可見:https://blog.csdn.net/weixin_44494811/article/details/103486637
}, delay)
}
}
// 2、立即執行版:一開始就觸發,後面再觸發不執行,delay秒後可以再觸發
function debounce (func, delay) {
let timer;
return function(){
clearTimeout(timer);
let callNow = !timer
timer = setTimeout(() => {
timer = null;
}, delay)
if (callNow) {
func.apply(this, arguments);
}
}
}
// 3、綜合版
// 合成版
/**
* @desc 函式防抖
* @param func 目標函式
* @param wait 延遲執行毫秒數
* @param immediate true - 立即執行, false - 延遲執行
*/
function debounce(func, wait, immediate) {
let timer;
return function() {
let context = this,
args = arguments;
// 根據immediate引數配置是否立即執行
if (timer) clearTimeout(timer);
if (immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timer = setTimeout(() => {
func.apply
}, wait)
}
}
}
拓展延伸
以上涉及的函式設計和思想可參考更詳細的分解:節流函式的原理和設計、關於閉包中變數的儲存
三、Lodash原始碼中的實現
lodash庫中關於節流函式的實現,比簡單的方法要成熟得多,首先,1、對引入的函式方法增加了型別判斷:typeof function, 對配置也增加了物件型別判斷:isObject.js 2、對函式節流函式增加了更多的可配置化引數,比如設定個最長等待時間,不管如何先響應一次再防抖等等,避免使用者看上次頁面假死。
1、節流:
_.throttle(func, [wait=0], [options=])
引數
func
(Function): 要節流的函式。[wait=0]
(number): 需要節流的毫秒。- [options=] (Object): 選項物件。
[options.leading=true]
(boolean): 指定呼叫在節流開始前。[options.trailing=true]
(boolean): 指定呼叫在節流結束後。
主方法如下:
function throttle(func, wait, options) {
let leading = true
let trailing = true
// 首先判斷傳入的func是否是function型別
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 判斷配置引數是否是物件,並轉換options.leading的型別為boolean
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 執行debouce方法,並傳遞引數
return debounce(func, wait, {
leading,
trailing,
'maxWait': wait
})
}
2、防抖:
_.debounce(func, [wait=0], [options=])#
引數
func
(Function): 要防抖動的函式。[wait=0]
(number): 需要延遲的毫秒數。- [options=] (Object): 選項物件。
[options.leading=false]
(boolean): 指定在延遲開始前呼叫。[options.maxWait]
(number): 設定func
允許被延遲的最大值。[options.trailing=true]
(boolean): 指定在延遲結束後呼叫。
在lodash庫中,對於節流和防抖的處理,核心內容是debouce方法,下面對這個debounce方法進行簡單的分析
可配置化的debounce方法
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait, // 最大等待時間
result, // 執行func
timerId, // 定時器控制代碼
lastCallTime // 上次觸發的時間,比如不斷scroll,為上次scroll的時間
let lastInvokeTime = 0 // 上次執行func的時間
let leading = false // 配置引數,是否第一次觸發立即執行
let maxing = false // 是否有最長等待時間
let trailing = true // 是否在等待週期結束後執行傳入的func函式
// 如果wait沒傳,呼叫window.requestAnimationFrame()
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
// 判斷func是否是函式型別
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 從傳入的options中取出引數並做一些型別轉換
wait = +wait || 0
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 定義函式invokeFunc:使用者傳入的func方法的執行函式,傳入引數time,並更新lastInvokeTime記錄上次執行invokeFunc的時間
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
// 定義函式startTimer: 建立一個定時器,傳參pendingFunc待執行函式,wait延遲多久後執行
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 定義函式cancelTimer: 清除建立的這個定時器:id控制代碼
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
// 定義leadingEdge:防抖開始執行的函式,根據leading判斷,如果true,立即執行
function leadingEdge(time) {
// 開始執行,則記錄這個time為上次執行func的時間:lastInvokeTime
lastInvokeTime = time
// 開始建立定時器執行
timerId = startTimer(timerExpired, wait)
// 根據leading引數判斷是否立即執行
return leading ? invokeFunc(time) : result
}
// 根據傳入的time,計算還需要等待的時間
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime // 現在距離上次觸發scroll的時間
const timeSinceLastInvoke = time - lastInvokeTime // 現在距離上次執行func的時間
const timeWaiting = wait - timeSinceLastCall // wait延遲時間 - 距離上次觸發scroll的時間
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
// 如果設定了最大等待時間,則還需等待:(延遲時間wait - 已經等候時間,最大等待時間-上次執行func剩餘的時間)兩者取較小值
// 否則,還需等待 wait - 已經等候時間
}
// 根據傳入的time判斷是否應該執行func函式
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime // 上次觸發
const timeSinceLastInvoke = time - lastInvokeTime // 上次執行
// 四種情況執行:
// 1、第一次觸發,lastCallTime為undefined
// 2、距離上次觸發已經大於延遲時間了
// 3、當前-上次觸發<0,特殊情況,比如原本是2020,修改了系統時間為2018
// 4、距離上次執行的時間> 最長等待時間了
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
// 防抖核心,判斷是執行函式,還是繼續設定定時器
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) { // 根據當前時間,判斷是否應該執行,如果是,執行func
return trailingEdge(time)
}
// 否則,重置定時器,將剩餘的時間傳入
timerId = startTimer(timerExpired, remainingWait(time))
}
// 執行func的判斷函式
function trailingEdge(time) {
timerId = undefined
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
// 取消防抖
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
// 直接執行
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
// 判斷是否在等待中
function pending() {
return timerId !== undefined
}
// 入口函式
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time) // 根據當前時間判斷是否應該執行func函式
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
// 如果定時器還未建立,建立定時器按照所設定的是否立即執行去執行
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
// 如果設定了最長等待時間,建立定時器,返回func的執行方法
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
// 如果還沒有建立定時器,建立定時器
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
export default debounce
基於以上debounce的基礎,其實lodash中,對節流的的實現,就是傳了一個maxWait引數( func
允許被延遲的最大值)為wait (延遲數),它的結果是如果連續不斷觸發則每隔 wait 秒執行一次func。