JS中的debounce與throttle

簡介

debouncethrottle,用中文描述的話,就是 去抖節流javascript

它們有什麼用:css

針對一些 執行頻率很是高 的交互或事件,作性能優化。java

兩者的概念,網上的說法不少,這裏不描述了。我這裏主要分析下他們的相同點和不一樣點,和在何時用它們。segmentfault

debouncethrottle 的相同點:api

都是利用函數延遲執行來實現效果,咱們暫時能夠理解成用 setTimeout瀏覽器

注:lodash 中爲了優化性能,在沒有傳入 wait 參數的狀況下,優先使用 requestAnimationFrame,若是瀏覽器不支持,再降級使用 setTimeout性能優化

debouncethrottle 的不一樣點:閉包

debounce 有一個等待時長,若是在這個等待時長內,再次調用了函數,就取消上一個定時器,並新建一個定時器。因此 debounce 適用於 input, keyup, keydown 等事件, 亦或者 click 事件須要防止用戶在某個時間範圍內屢次點擊的時候,也能夠用。注:在lodash 的實現中 中並無取消新建定時器的作法,是用時間來判斷的。app

throttle 也有一個等待時長,每隔一段這個等待時長,函數必須執行一次。若是在這個等待時長內,當前延遲執行沒有完成,它會忽略接下來調用該函數的請求,不會去取消上一個定時器。因此 throttle 適用於 scroll, mousemove 等事件。在lodash 的實現中,還有一個等待的最大時長,這個咱們分析源碼時再討論。函數

resize 事件,使用 debouncethrottle 都行,看你的需求啦。

舉個栗子,使用 lodash 處理 resize 事件時,在 wait 參數不是很是小的狀況下:

debounce的話,會在用戶中止改變瀏覽器窗口大小時觸發,也就是隻是在最後觸發一次。

throttle的話,會在用戶改變瀏覽器窗口大小的過程當中,每隔一段時間觸發一次。

其實在 lodash 的實現中: throttle 就是一個定義了最大等待時長的 debounce

接下來,咱們先本身實現 簡易版的 debouncethrottle,而後再分析lodash 源碼中的對應方法

debounce

debounce 有個很重要的特性,就是在規定的等待時長內若是再次調用函數,會取消上一次函數執行。

因此咱們這裏能夠用 clearTimeout 先清除定時器,再從新 setTimeout 建一個新的定時器。

它的原理其實就是在閉包內維護一個 定時器

function debounce(fn, wait) {
    let callback = fn;    
    let timerId = null;

    function debounced() {
        // 保存做用域
        let context = this;
        // 保存參數,例如 event 對象
        let args = arguments;        

        clearTimeout(timerId);        
        timerId = setTimeout(function() {            
            callback.apply(context, args);
        }, wait);
    }
    
    // 返回一個閉包
    return debounced;         
}

// test
let resizeFun = function(e) {
    console.log('resize');
};
window.addEventListener('resize', debounce(resizeFun, 500));
複製代碼

throttle

throttle 相對於 debounce 的最大區別就是它不會取消上一次函數的執行。

因此咱們能夠基於 debounce 去調整一下。

function throttle(fn, wait) {
    let callback = fn;    
    let timerId = null;

    // 是不是第一次執行
    let firstInvoke = true;

    function throttled() {
        let context = this;
        let args = arguments;           

        // 若是是第一次觸發,直接執行
        if (firstInvoke) {
            callback.apply(context, args);
            firstInvoke = false;
            return ;
        }

        // 若是定時器已存在,直接返回。 
        if (timerId) {
            return ;
        }

        timerId = setTimeout(function() {  
            // 注意這裏 將 clearTimeout 放到 內部來執行了
            clearTimeout(timerId);
            timerId = null;

            callback.apply(context, args);
        }, wait);
    }

    // 返回一個閉包
    return throttled;
}

// test
let resizeFun = function(e) {
    console.log('resize');
};
window.addEventListener('resize', throttle(resizeFun, 500));
複製代碼

分析 lodash 中的 debounce

function debounce(func, wait, options) {
    let lastArgs,   // 存儲 func 函數執行時的參數, 執行 debounced 函數的時候,被賦值
        lastThis,     // 存儲 func 函數執行時的做用域, 執行 debounced 函數的時候,被賦值
        maxWait,      // 最長等待時間
        result,       // 存儲 func 函數的返回值
        timerId,      // 定時器 id
        lastCallTime  // 最近一次 執行 debounced 函數時的時間

    // 最近一次執行 func 時的時間戳
    // 正常狀況下,lastCallTime 與 lastInvokeTime 是相差無幾的。
    let lastInvokeTime = 0
    // 是否 在延遲開始前 調用函數 - 它的做用,相似我上面實現的 throttle 方法中的 firstInvoke
    let leading = false
    // options 是否 傳入了 maxWait 
    let maxing = false
    // 是否 在延遲結束後 調用函數
    let trailing = true
    // 能夠看到, debounce 函數,默認是 leading = false, trailing = true。也就意味着,默認在延遲結束後調用 func 函數

    // 若是 沒有傳入 wait, 且 wait 不等於 0, 且瀏覽器支持 requestAnimationFrame時
    // useRAF 會等於 true, 表示啓用 requestAnimationFrame 
    const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')

    // 若是 沒有傳入 func, 直接拋出錯誤
    if (typeof func != 'function') {
        throw new TypeError('Expected a function')
    }

    // 設置 wait 的默認值爲 0
    wait = +wait || 0

    // 初始化 options 選項的 默認參數
    if (isObject(options)) {
        leading = !!options.leading
        maxing = 'maxWait' in options
        // 處理 maxWait 參數,若是用戶本身定義了 maxWait, 則和 wait 參數比較,取他們的最大值
        // 這裏是爲了防止,用戶 傳入的 wait 大於 maxWait 的狀況
        maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
        trailing = 'trailing' in options ? !!options.trailing : trailing
    }

    // 封裝執行函數, 用於當即執行 func
    function invokeFunc(time) {
        // 參數
        const args = lastArgs
        // 做用域
        const thisArg = lastThis
        // 重置
        lastArgs = lastThis = undefined
        // 記錄 func 函數執行時的時間戳
        lastInvokeTime = time
        // 執行函數
        result = func.apply(thisArg, args)
        return result
    }

    // 開啓定時器
    function startTimer(pendingFunc, wait) {
        if (useRAF) {
            return root.requestAnimationFrame(pendingFunc)
        }
        return setTimeout(pendingFunc, wait)
    }

    // 清除定時器
    function cancelTimer(id) {
        if (useRAF) {
            return root.cancelAnimationFrame(id)
        }
        clearTimeout(id)
    }

    // 在延遲開始前調用
    // 對 invokeFunc 的封裝,返回 invokeFunc 函數的返回值
    function leadingEdge(time) {
        // 記錄 函數被調用時 的時間戳
        lastInvokeTime = time
        // 開啓一個定時器
        timerId = startTimer(timerExpired, wait)
        // 若是 leading 爲 true,則表示須要在延遲開始前 先執行一次 func 函數
        return leading ? invokeFunc(time) : result
    }

    // 在延遲結束後調用
    // 對 invokeFunc 的封裝,返回 invokeFunc 函數的返回值
    function trailingEdge(time) {
        timerId = undefined

        // 這裏加了個 lastArgs 的判斷,lastArgs 會在 debounced 函數執行時賦值
        if (trailing && lastArgs) {
            return invokeFunc(time)
        }

        // 重置 參數和 做用域
        lastArgs = lastThis = undefined
        return result
    }

    // 
    function remainingWait(time) {
        // 計算 time 與最近一次調用 debounced 函數的時間差
        const timeSinceLastCall = time - lastCallTime
        // 計算 time 與最近一次調用 func 函數的時間差
        const timeSinceLastInvoke = time - lastInvokeTime
        // 用 wait 減去已經等待的時間
        const timeWaiting = wait - timeSinceLastCall

        return maxing
            // 若是設置了最大等待時長,
            // 則須要 比較 ( wait 減去已經等待的時間 ) 和 ( maxWait 減去已經等待的時間 ),取最小值
            ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
            // 不然直接返回 wait 減去已經等待的時間
            : timeWaiting
    }

    // 根據時間判斷是否能夠執行函數
    function shouldInvoke(time) {
        // 計算 time 與最近一次調用 debounced 函數的時間差
        const timeSinceLastCall = time - lastCallTime
        // 計算 time 與最近一次調用 func 函數的時間差
        const timeSinceLastInvoke = time - lastInvokeTime

        return (
            // 判斷是否是第一次執行 debouned 函數,若是是第一次執行,確定能夠調用 func 函數
            lastCallTime === undefined
            // 判斷距離最近一次調用 debounced 函數的時間差,是否大於等於 wait,若是是的話,也就意味着能夠調用 func 函數
            || (timeSinceLastCall >= wait)
            // 正常狀況 timeSinceLastCall 不會小於 0, 除非手動調整了系統時間
            || (timeSinceLastCall < 0)
            // 若是設置了 maxWait,判斷距離上一次調用 func 函數的時間差,是否超過了最大等待時長
            || (maxing && timeSinceLastInvoke >= maxWait))
    }

    // 封裝執行函數,用於 wait 延遲結束後執行
    function timerExpired() {
        const time = Date.now()
        // 根據時間來判斷是否能夠執行 func 函數
        if (shouldInvoke(time)) {
            return trailingEdge(time)
        }
        // 從新計算時間,從新建一個定時器
        timerId = startTimer(timerExpired, remainingWait(time))
    }

    function debounced(...args) {
        const time = Date.now()
        const isInvoking = shouldInvoke(time)

        lastArgs = args
        lastThis = this
        lastCallTime = time

        // 若是能夠執行 func。第一次執行的時候,isInvoking 確定是 true
        if (isInvoking) {
            // 第一次執行時
            if (timerId === undefined) {
                return leadingEdge(lastCallTime)
            }
            // 若是能夠執行 func,且又有 timerId 。說明 func 能夠執行了,但是又沒有執行 trailingEdge
            // 若是又已經設置了 maxWait,就當即執行 func
            // 何時會出現這種狀況, 我還不明白。
            if (maxing) {
                // 開啓一個定時器
                timerId = startTimer(timerExpired, wait)
                // 當即執行 func 函數
                // 爲何設置 maxWait 就須要理解執行 func ,下面分析 throttle 的時候就明白了
                return invokeFunc(lastCallTime)
            }
        }

        // 什麼狀況下 不是第一次執行, 卻又沒有 timerId 呢?
        // 由於 trailingEdge 函數內部會執行 timerId = undefined
        // 若是恰好 trailingEdge 函數執行以後,又觸發了 debounced ,就會出現這種狀況
        if (timerId === undefined) {
            timerId = startTimer(timerExpired, wait)
        }

        return result
    }

    // 返回一個閉包
    return debounced
}
複製代碼

注意:

lastCallTime 存儲的是 debounced 函數執行時的時間戳。

lastInvokeTime 存儲的是 func 函數執行時的時間戳。

lodashdebounced 還提供了3個api,供外部調用

// 取消 debounce
function cancel() {
    if (timerId !== undefined) {
        cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
}

// 執行 func
function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now())
}

// 判斷是否正在等待中
function pending() {
    return timerId !== undefined
}

// 暴露出三個方法 
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
複製代碼

分析 lodash 中的 throttle

throttle 其實就是設置了 leadingmaxWaitdebounce

function throttle(func, wait, options) {
    let leading = true
    let trailing = true

    if (typeof func != 'function') {
        throw new TypeError('Expected a function')
    }

    // 初始化 leading 和 trailing 的默認值
    if (isObject(options)) {
        leading = 'leading' in options ? !!options.leading : leading
        trailing = 'trailing' in options ? !!options.trailing : trailing
    }

    // 默認狀況下,leading 爲 true, trailing 爲 true。
    // 表示 在延遲開始前,和延遲結束後,都須要調用 func
    // 還傳入了一個 maxWait
    return debounce(func, wait, {
        'leading': leading,
        'maxWait': wait,
        'trailing': trailing
    })
}
複製代碼

debounce 的源碼,我沒有徹底看明白,有幾個地方是個人猜想。好比 debounced 函數內的 if (maxing),不明白爲何要這樣判斷。

很是但願有明白的道友,告訴下我。

若是有錯誤的地方,還請指出。

謝謝閱讀。

參考

lodash文檔

「淺入淺出」函數防抖(debounce)與節流(throttle)

從lodash源碼學習節流與防抖

聊聊lodash的debounce實現

相關文章
相關標籤/搜索