前端性能優化之節流-throttle

上次介紹了前端性能優化之防抖-debounce,此次來聊聊它的兄弟-節流。javascript

再拿乘電梯的例子來講:坐過電梯的都知道,在電梯關門但未上升或降低的一小段時間內,若是有人從外面按開門按鈕,電梯是會再開門的。要是電梯空間沒有限制的話,那裏面的人就一直在等。。。後來電梯工程師收到了好多投訴,因而他們就改變了方案,設定每隔必定時間,好比30秒,電梯就會關門,下一節電梯會繼續等待30秒。前端

專業術語歸納就是:每隔必定時間,執行一次函數。java

最簡易版的代碼實現:性能優化

function throttle(fn, delay) {
    let timer = null;

    return function() {
        const context = this;
        const args = arguments;

        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(context, args);
                timer = null;
            }, delay);
        }
    };
}
複製代碼

很好理解,返回一個匿名函數造成閉包,並維護了一個局部變量timer。只有在timer不爲null纔開啓定時器,而timer爲null的時機則是定時器執行完畢。閉包

除了定時器,還能夠用時間戳實現:app

function throttle(fn, delay) {
    let last = 0;

    return function() {
        const context = this;
        const args = arguments;

        const now = +new Date();
        const offset = now - last;

        if (offset > delay) {
            last = now;
            fn.apply(context, args);
        }
    };
}
複製代碼

last表明上次執行fn的時刻,每次執行匿名函數都會計算當前時刻與last的間隔,是否比咱們設定的時間間隔大,若大於,則執行fn,並更新last的值。前端性能

比較上述兩種實現方式,實際上是有區別的: 定時器方式,第一次觸發並不會執行fn,但中止觸發以後,還會再次執行一次fn 時間戳方式,第一次觸發會執行fn,中止觸發後,不會再次執行一次fn函數

兩種方式是能夠互補的,能夠將其結合起來,即能第一次觸發會執行fn,又能在中止觸發後,再次執行一次fn:post

function throttle(fn, delay) {
    let last = 0;
    let timer = null;

    return function() {
        const context = this;
        const args = arguments;

        const now = +new Date();
        const offset = now - last;

        if (offset > delay) {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }

            last = now;
            fn.apply(context, args);
        }
        else if (!timer) {
            timer = setTimeout(() => {
                last = +new Date();
                timer = null;
                fn.apply(context, args);
            }, delay - offset);
        }
    };
}
複製代碼

匿名函數內有個if...else,第一個是判斷時間戳,第二個是判判定時器,對比下前面兩種實現方式。 首先是時間戳方式的簡易版:性能

if (offset > delay) {
  last = now;
  fn.apply(context, args);
}
複製代碼

混合版:

if (offset > delay) {
  if (timer) {      // 注意這裏
    clearTimeout(timer);
    timer = null;
  }

  last = now;
  fn.apply(context, args);
}
複製代碼

能夠發現,混合版比簡易版多了對timer不爲null的判斷,並清除了定時器、將timer置爲null。 再是定時器實現方式的簡易版:

if (!timer) {
  timer = setTimeout(() => {
    fn.apply(context, args);
    timer = null;
  }, delay);
}
複製代碼

混合版:

else if (!timer) {
  timer = setTimeout(() => {
    last = +new Date();   // 注意這裏
    timer = null;
    fn.apply(context, args);
  }, delay - offset);
}
複製代碼

能夠看到,混合版比簡易版多了對last變量的重置,而last變量是時間戳實現方式中判斷的重要因素。這裏要注意下,由於是在定時器的回調中,因此last的重置值要從新獲取當前時間戳,而不能使用變量now。

經過以上對比,咱們能夠發現,混合版是綜合了兩種不一樣實現方式的做用,但除去開始和結束階段的不一樣,二者的共同做用是一致的--執行fn函數。因此,同一個時刻,執行fn函數的語句只能存在一個!在混合版的實現中,時間戳判斷裏,去除了定時器的影響,定時器判斷裏,去除了時間戳的影響。

對於當即執行和中止觸發後的再次執行,咱們能夠經過參數來控制,適應需求的變化。 假設規定{ immediate: false } 阻止當即執行,{ trailing: false } 阻止中止觸發後的再次觸發:

function throttle(fn, delay, options = {}) {
    let timer = null;
    let last = 0;

    return function() {
        const context = this;
        const args = arguments;

        const now = +new Date();
        
        if (last === 0 && options.immediate === false) {    // 這個條件語句是新增的
            last = now;
        }

        const offset = now - last;

        if (offset > delay) {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }

            last = now;
            fn.apply(context, args);
        }
        else if (!timer && options.trailing !== false) {  // options.trailing !== false 是新增的
            timer = setTimeout(() => {
                last = options.immediate === false ? 0 : +new Date();;
                timer = null;
                fn.apply(context, args);
            }, delay - offset);
        }
    };
}
複製代碼

相對於混合版,除了新增了一個參數options,其它不一樣之處已在代碼中標明。 思考下,當即執行是時間戳方式實現的,那麼想要阻止當即執行的話,只要阻止第一次觸發時,offset > delay 條件的成立就好了!如何判斷是第一次觸發?last變量只有初始化時,值纔會是0,再加上咱們手動傳入的參數,阻止當即執行的條件就知足了:

if (last === 0 && options.immediate === false) {    
  last = now;
}
複製代碼

條件知足後,咱們重置last變量的初始值爲當前時間戳,那麼第一次 offset > delay 就不會成立了! 而後想阻止中止觸發後的再次執行,仔細一想,要是不須要這個功能的話,時間戳的實現不就能夠知足了?對!咱們只要變相地去除定時器就行了:

!timer && options.trailing !== false
複製代碼

若是咱們不手動傳入{ trailing: false } ,這個條件是永遠不會成立的,即定時器永遠不會開啓。

不過有個問題在於,immediate和trailing不能同時設置爲false,緣由在於,{ trailing: false } 的話,中止觸發後不會再次執行,而後關鍵的last變量也就不會被重置爲0,下一次再次觸發又會當即執行,這樣就有衝突了。

相關文章
相關標籤/搜索