性能優化之節流函數---throttle

防抖函數和節流函數本質是不同的。防抖函數是將屢次執行變爲最後一次執行或第一次執行,節流函數是將屢次執行變成每隔一段時間執行。javascript

節流函數

前言

防抖函數和節流函數本質是不同的。防抖函數是將屢次執行變爲最後一次執行或第一次執行,節流函數是將屢次執行變成每隔一段時間執行。html

好比說,噹噹咱們作圖片懶加載(lazyload)時,須要經過滾動位置,實時顯示圖片時,若是使用防抖函數,懶加載(lazyload)函數將會不斷被延時, 當咱們作圖片懶加載(lazyload)時,須要經過滾動位置,實時顯示圖片時,若是使用防抖函數,懶加載(lazyload)函數將會不斷被延時, 只有停下來的時候纔會被執行,對於這種須要週期性觸發事件的狀況,防抖函數就顯得不是很友好了,此時就應該使用節流函數來實現了。前端

例子

<div id="container"></div>
複製代碼
div{
    height: 200px;
    line-height: 200px;
    text-align: center; color: #fff;
    background-color: #444;
    font-size: 25px;
    border-radius: 3px;
}
複製代碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
    container.innerHTML = count ++ ;
}
container.addEventListener('mousemove',updateCount);
複製代碼

咱們來看一下效果:java

avatar

咱們能夠看到,鼠標從左側滑到右側,咱們綁定的事件執行了119次git

節流函數的實現

如今咱們來實現一個節流函數,使得鼠標移動過程當中每間隔一段時間事件觸發一次。github

使用時間戳來實現節流

首先咱們想到使用時間戳計時的方式,每次事件執行時獲取當前時間並進行比較判斷是否執行事件。app

/** * 節流函數 * @param func 用戶傳入的節流函數 * @param wait 間隔的時間 */
const throttle = function (func,wait = 50) {
    let preTime = 0;
    return function (...args) {
        let now = Date.now();
        if(now - preTime >= wait){
            func.apply(this,args);
            preTime = now;
        }
    }
};
複製代碼
let count = 1;
let container = document.getElementsByTagName('div')[0];
function updateCount() {
    container.innerHTML = count ++ ;
}
let action = throttle(updateCount,1000);

container.addEventListener('mousemove',action);
複製代碼

avatar

此時當鼠標移入的時候,事件當即執行,在鼠標移動的過程當中,每隔1000ms事件執行一次,旦在最後鼠標中止移動後,事件不會被執行
此時會有這樣的兩個問題:函數

  • 若是咱們但願鼠標剛進入的時候不當即觸發事件,此時該怎麼辦呢?
  • 若是咱們但願鼠標中止移動後,等到間隔時間到來的時候,事件依然執行,此時該怎麼辦呢?

使用定時器實現節流

爲知足上面的需求,咱們考慮使用定時器來實現節流函數
當事件觸發的時候,咱們設置一個定時器,再觸發的時候,定時器存在就不執行,等到定時器執行並執行函數,清空定時器,而後接着設置定時器優化

/**
 * 節流函數
 * @param func 用戶傳入的節流函數
 * @param wait 間隔的時間
 */
const throttle = function (func,wait = 50) {
    let timer = null;
    return function (...args) {
        if(!timer){
            timer = setTimeout(()=>{
                func.apply(this,args);
                timer = null;
            },wait);
        }
    }
};
複製代碼

使用這個定時器節流函數應用在最開始的例子上:ui

let action = throttle(updateCount,2000);

container.addEventListener('mousemove',action);
複製代碼

avatar

咱們能夠看到,當鼠標移入的時候,時間不會當即執行,等待2000ms後執行了一次,此後2000ms執行一次,當鼠標移除後,前一次觸發事件的時間2000ms後還會觸發一次事件。

比較時間戳節流與定時器節流

  • 時間戳節流
    • 開始時,事件當即執行
    • 中止觸發後,沒有辦法再執行事件
  • 定時器節流
    • 開始時,會在間隔時間後第一次執行
    • 中止觸發後,依然會再次執行一次事件

對於咱們平常的工做需求來講,可能出現的需求是,既須要開始時當即執行,也須要結束時還能再執行一次的節流函數。

綜合時間戳節流和定時器節流

/** * 節流函數 * @param func 用戶傳入的節流函數 * @param wait 間隔的時間 */
const throttle = function (func,wait = 50) {
    let preTime = 0,
        timer = null;
    return function (...args) {
        let now = Date.now();
        // 沒有剩餘時間 || 修改了系統時間
        if(now - preTime >= wait || preTime > now){
            if(timer){
                clearTimeout(timer);
                timer = null;
            }
            preTime = now;
            func.apply(this,args);
        }else if(!timer){
            timer = setTimeout(()=>{
                preTime = Date.now();
                timer = null;
                func.apply(this,args)
            },wait - now + preTime);
        }
    }
};
複製代碼

使用這個定時器節流函數應用在最開始的例子上:

let action = throttle(updateCount,2000);

container.addEventListener('mousemove',action);
複製代碼

avatar

咱們能夠看到,當鼠標移入時,事件當即執行,以後每間隔2000ms後,事件均會執行,當鼠標離開時,前一次事件觸發2000ms後,事件最後會再一次執行

咱們繼續考慮下面的場景

  • 有時候咱們的需求變成鼠標移入時當即執行,鼠標移除後事件不在執行呢?
  • 有時候咱們的需求變成鼠標移入時不當即執行,鼠標移除後事件還會執行呢?

繼續優化

咱們設置 opts 做爲 throttle 函數的第三個參數,而後根據 opts 所攜帶的值來判斷實現那種效果,約定以下:

  • leading : Boolean 是否使用第一次執行
  • trailing : Boolean 是否使用中止觸發的回調執行

修改代碼以下:

/** * 節流函數 * @param func 用戶傳入的節流函數 * @param wait 間隔的時間 * @param opts leading 是否第一次執行 trailing 是否中止觸發後執行 */
const throttle = function (func,wait = 50,opts = {}) {
    let preTime = 0,
        timer = null,
        { leading = true, trailing = true } = opts;
    return function (...args) {
        let now = Date.now();
        if(!leading && !preTime){
            preTime = now;
        }
        // 沒有剩餘時間 || 修改了系統時間
        if(now - preTime >= wait || preTime > now){
            if(timer){
                clearTimeout(timer);
                timer = null;
            }
            preTime = now;
            func.apply(this,args);
        }else if(!timer && trailing){
            timer = setTimeout(()=>{
                preTime = Date.now();
                timer = null;
                func.apply(this,args)
            },wait - now + preTime);
        }
    }
};
複製代碼

這裏須要注意的是,leading:false 和 trailing: false 不能同時設置。 由於若是同時設置的時候,當鼠標移除的時候,中止觸發的時候不會設置定時器,也就是說,等到過了設置的時間,preTime不會被更新,此後再次移入的話就會當即執行,就違反了 leading: false

取消

在 debounce 的實現中,咱們加了一個 cancel 方法,throttle 咱們也加個 cancel 方法:

/**
 * 節流函數
 * @param func 用戶傳入的節流函數
 * @param wait 間隔的時間
 * @param opts leading 是否第一次執行 trailing 是否中止觸發後執行
 */
const throttle = function (func,wait = 50,opts = {}) {
    let preTime = 0,
        timer = null,
        { leading = false, trailing = true } = opts,
        throttled = function (...args) {
            let now = Date.now();
            if(!leading && !preTime){
                preTime = now;
            }
            // 沒有剩餘時間 || 修改了系統時間
            if(now - preTime >= wait || preTime > now){
                if(timer){
                    clearTimeout(timer);
                    timer = null;
                }
                preTime = now;
                func.apply(this,args);
            }else if(!timer && trailing){
                timer = setTimeout(()=>{
                    preTime = Date.now();
                    timer = null;
                    func.apply(this,args)
                },wait - now + preTime);
            }
        };
    throttled.cancel = function () {
        clearTimeout(timer);
        timer = null;
        preTime = 0;
    };
    return throttled;
};
複製代碼

至此咱們完成了一個節流函數。

來自我豐哥,前端大神級開發

相關文章
相關標籤/搜索