天天看點

節流

節流的原理很簡單:

如果你持續觸發事件,每隔一段時間,隻執行一次事件。

根據首次是否執行以及結束後是否執行,效果有所不同,實作的方式也有所不同。

我們用 leading 代表首次是否執行,trailing 代表結束後是否再執行一次。

關于節流的實作,有兩種主流的實作方式,一種是使用時間戳,一種是設定定時器。

第一種方法:使用時間戳,當觸發事件的時候,我們取出目前的時間戳,然後減去之前的時間戳(最一開始值設為 0 ),如果大于設定的時間周期,就執行函數,然後更新時間戳為目前的時間戳,如果小于,就不執行。

// 第一版
function throttle(func, wait) {
    var context, args;
    var previous = 0;

    return function() {
        var now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}
container.onmousemove = throttle(getUserAction, 1000);      

第二種實作方式,使用定時器。

當觸發事件的時候,我們設定一個定時器,再觸發事件的時候,如果定時器存在,就不執行,直到定時器執行,然後執行函數,清空定時器,這樣就可以設定下個定時器。

// 第二版
function throttle(func, wait) {
    var timeout;
    var previous = 0;

    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                func.apply(context, args)
            }, wait)
        }

    }
}      

為了讓效果更加明顯,我們設定 wait 的時間為 3s,效果示範如下:

節流

我們可以看到:當滑鼠移入的時候,事件不會立刻執行,晃了 3s 後終于執行了一次,此後每 3s 執行一次,當數字顯示為 3 的時候,立刻移出滑鼠,相當于大約 9.2s 的時候停止觸發,但是依然會在第 12s 的時候執行一次事件。

是以比較兩個方法:

  1. 第一種事件會立刻執行,第二種事件會在 n 秒後第一次執行
  2. 第一種事件停止觸發後沒有辦法再執行事件,第二種事件停止觸發後依然會再執行一次事件

我想要一個有頭有尾的!就是滑鼠移入能立刻執行,停止觸發的時候還能再執行一次!

是以我們綜合兩者的優勢,然後雙劍合璧,寫一版代碼:

// 第三版
function throttle(func, wait) {
    var timeout, context, args, result;
    var previous = 0;

    var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
    };

    var throttled = function() {
        var now = +new Date();
        //下次觸發 func 剩餘的時間
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
         // 如果沒有剩餘的時間了或者你改了系統時間
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}      

但是我有時也希望無頭有尾,或者有頭無尾,這個咋辦?

那我們設定個 options 作為第三個參數,然後根據傳的值判斷到底哪種效果,我們約定:

leading:false 表示禁用第一次執行

trailing: false 表示禁用停止觸發的回調

我們來改一下代碼:

// 第四版
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}      

取消

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

// 第五版 非完整代碼,完整代碼請檢視最後的示範代碼連結
...
throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = null;
}
...      

注意

我們要注意 underscore 的實作中有這樣一個問題:

container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
    leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
    trailing: false
});