手ぶれ防止と節流を探究します.


本論文は私のブログから来ました.GitHubに行ってください.
本文は手ぶれと節流を防ぐことから、それらの特性を分析し、特別な節流方式のrequest AnimationFrameを開拓し、最後にlodashの中のdebounceソースを分析します.
手ぶれ防止と節流は先端開発においてよく使われる最適化手段であり、それらは時間内の方法の実行回数を制御するために使われています.必要でない出費を多く節約できます.
手振れ防止(debounce)
ウィンドウのサイズの変化をタイムリーに知る必要がある時、私達はwindowにresize関数を結びつけます.
window.addEventListener('resize', () => {
    console.log('resize')
});
私たちは、極小のスケーリング操作でも、数十回のレシオをプリントすることができます.つまり、オンスリゼ関数で小さな動作をする必要があれば、何十回も繰り返し実行します.しかし、実際には、マウスの緩みだけに関心を持っています.ウィンドウが変化しているときには、debounceを使って最適化することができます.
const handleResize = debounce(() => {
    console.log('resize');
}, 500);
window.addEventListener('resize', handleResize);
上記のコードを実行します.(既製のdebounce関数が必要です.)500 msのスケーリングを停止した後、デフォルトのユーザーは操作を続けなくなりました.
これは振動防止の効果であり、一連の呼び出しを一つに変えて、最大限に効率を最適化しました.
もう一つの手ぶれ防止の一般的なシーンを挙げます.
検索バーはしばしば私たちの入力に従って、バックエンドに要求し、検索候補を取得して、検索バーの下に表示されます.手ぶれ防止を使用しない場合、「debounce」を入力すると、フロントエンドが順にバックエンドに「d」、「de」、「deb」、「debounce」の検索候補項目を要求します.ユーザの入力が速い場合、これらの要求は意味がなく、手ぶれ防止最適化を使用することができます.
上記の2つの例を見てみると、手ぶれ防止は結果だけを気にするのに非常に適しています.どのような状況であれ、大量の連続イベントを単一の私たちが必要とするイベントにうまく転じることができます.
よりよく理解するために、以下には最も簡単なdebounceの実現を提供します.このfunctionを初めて実行すると、一つのタイマーが起動します.次の実行は前回のタイマーをクリアして、一つのタイマーを再起動します.このfunctionが呼び出されなくなるまで、タイマーが成功的に走り終わって、コールバック関数を実行します.
const debounce = function(func, wait) {
    let timer;
    return function() {
        !!timer && clearTimeout(timer);
        timer = setTimeout(func, wait);
    };
};
もし私たちが結果に関心を持つだけでなく、過程にも関心を持ちますか?
スロットル
スロットルは、指定関数を所定の時間に一回以上実行させない、つまり連続高周波で動作を定期的に実行します.スロットルの主な目的は、元の動作の周波数を下げることである.
例:
無限スクロールのfeedフローをシミュレーションします.
html数:

css:

#wrapper {
    height: 500px;
    overflow: auto;
}
.feed {
    height: 200px;
    background: #ededed;
    margin: 20px;
}
js:
const wrapper = document.getElementById("wrapper");
const loadContent = () => {
    const {
        scrollHeight,
        clientHeight,
        scrollTop
    } = wrapper;
    const heightFromBottom = scrollHeight - scrollTop - clientHeight;
    if (heightFromBottom < 200) {
        const wrapperCopy = wrapper.cloneNode(true);
        const children = [].slice.call(wrapperCopy.children);
        children.forEach(item => {
            wrapper.appendChild(item);
        })
    }
}
const handleScroll = throttle(loadContent, 200);
wrapper.addEventListener("scroll", handleScroll);
この例では、新しいコンテンツを追加する必要があるかどうかを判断するために、スクロールバーの距離の底の高さを継続的に取得する必要があることが見られます.srcolも高周波トリガのイベントであることを知っています.有効トリガの回数を減らす必要があります.手ぶれを防ぐために使うなら、スクロールを停止してからしばらくの間、新しいコンテンツをロードしなければなりません.無限スクロールのようななめらかさはありません.この時、私達は節流を使って、イベントの有効トリガの周波数を下げると同時に、ユーザーに流暢な閲覧体験を与えることができます.この例では、私たちはthrottleのwait値を200 msと指定します.つまり、あなたがずっとページをスクロールしているなら、loadCotent関数も200 msごとに一回だけ実行します.
同様に、ここではthrottleの最も簡単な実現があります.もちろん、この実現は粗いです.多くの欠陥があります.
const throttle = function (func, wait) {
    let lastTime;
    return function () {
        const curTime = Date.now();
        if (!lastTime || curTime - lastTime >= wait) {
            lastTime = curTime;
            return func();
        }
    }
}
request Animation Frame(rAF)
rAFはある程度throttle(func,16)と似た役割をしていますが、ブラウザが持っているアプリなので、スロットル関数よりもスムーズに機能します.window.request Animation Frame()を呼び出して、ブラウザは次回更新する時指定のコールバック関数を実行します.通常、画面のリフレッシュ周波数は60 hzであるため、この関数は約16.7 msに1回実行される.もしあなたの動画をもっと滑らかにしたいなら、rAFを使えばいいです.スクリーンのリフレッシュ頻度に合わせて来たのです.
rAFの書き方はdebounceやthrottleと違っていますが、動画を描きたい場合は、常にコールバック関数で自身を呼び出す必要があります.具体的な書き方はmdnを参照してください.
rAFはie 10及び以上のブラウザをサポートしていますが、ブラウザが持っているアプリなので、nodeでは使えません.
締め括りをつける
debounceは一つのイベントの実行を最後のイベントの実行に変えます.結果だけに注目すれば、debounceはもっと適しています.
プロセスに注目しながら、throttleを使用すれば、高周波イベントの実行頻度を下げることができます.
あなたのコードがブラウザで実行されている場合は、ie 10との互換性を考慮せず、できるだけスムーズにページの変化を要求します.rAFを使用することができます.
参考:https://css-tricks.com/debouncing-throttling-explained-examples/
付:lodashソース解析
lodashのdebounce機能は非常に強大で、debounce、throttleとrAFを一身に集めていますので、わざわざ読んでみます.以下は私の解析です.
function debounce(func, wait, options) {
    /**
     * lastCallTime      debounced     
     * lastInvokeTime      func   
     */
    let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;

    let lastInvokeTime = 0;
    let leading = false;
    let maxing = false;
    let trailing = true;

    /**
     *      wait raf        raf
     */
    const useRAF =
        !wait && wait !== 0 && typeof root.requestAnimationFrame === "function";

    if (typeof func !== "function") {
        throw new TypeError("Expected a function");
    }
    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;
    }

    /**
     *   func
     */
    function invokeFunc(time) {
        const args = lastArgs;
        const thisArg = lastThis;

        lastArgs = lastThis = undefined;
        /**
         *   lastInvokeTime
         */
        lastInvokeTime = time;
        result = func.apply(thisArg, args);
        return result;
    }

    /**
     *      
     */
    function startTimer(pendingFunc, wait) {
        if (useRAF) {
            root.cancelAnimationFrame(timerId);
            return root.requestAnimationFrame(pendingFunc);
        }
        return setTimeout(pendingFunc, wait);
    }

    /**
     *    debounce    
     */
    function leadingEdge(time) {
        lastInvokeTime = time;
        timerId = startTimer(timerExpired, wait);
        return leading ? invokeFunc(time) : result;
    }

    /**
     *       
     * 1  wait          debounced  (lastCallTime)
     * 2  maxWait          func  (lastInvokeTime)
     * 1 2    
     */
    function remainingWait(time) {
        const timeSinceLastCall = time - lastCallTime;
        const timeSinceLastInvoke = time - lastInvokeTime;
        const timeWaiting = wait - timeSinceLastCall;

        return maxing
            ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
            : timeWaiting;
    }

    /**
     *         
     */
    function shouldInvoke(time) {
        const timeSinceLastCall = time - lastCallTime;
        const timeSinceLastInvoke = time - lastInvokeTime;
        /**
         * 4     true,    false
         * 1.     
         * 2.      debounced  (lastCallTime)>=wait
         * 3.      
         * 4.   maxWait,      func  (lastInvokeTime)>=maxWait
         */
        return (
            lastCallTime === undefined ||
            timeSinceLastCall >= wait ||
            timeSinceLastCall < 0 ||
            (maxing && timeSinceLastInvoke >= maxWait)
        );
    }

    /**
     *   shouldInvoke        
     *   :  trailingEdge  
     *    :  startTimer      timer,wait   remainingWait    
     */
    function timerExpired() {
        const time = Date.now();
        if (shouldInvoke(time)) {
            return trailingEdge(time);
        }
        // Restart the timer.
        timerId = startTimer(timerExpired, remainingWait(time));
    }

    /**
     *    debounce    
     */
    function trailingEdge(time) {
        timerId = undefined;

        /**
         * trailing true lastArgs  undefined   
         */
        if (trailing && lastArgs) {
            return invokeFunc(time);
        }
        lastArgs = lastThis = undefined;
        return result;
    }

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

        lastArgs = args;
        lastThis = this;
        /**
         *   lastCallTime
         */
        lastCallTime = time;

        if (isInvoking) {
            /**
             *      
             */
            if (timerId === undefined) {
                return leadingEdge(lastCallTime);
            }
            /**
             * 【 1】
             */
            if (maxing) {
                timerId = startTimer(timerExpired, wait);
                return invokeFunc(lastCallTime);
            }
        }
        /**
         * 【 2】
         */
        if (timerId === undefined) {
            timerId = startTimer(timerExpired, wait);
        }
        return result;
    }
    return debounced;
}
オススメは戻り方debouncedから、実行順に読むと分かりやすくなります.
【注1】最初はif(maxing)の中のこのコードの役割が分かりませんでしたが、本来はこのコードは実行されません.その後、私はlodashの倉庫に行ってtestファイルを見ました.このコードに対して、caseのテストがあります.いくつかのコードを除いて、テスト用の例を修正して展示しました.
var limit = 320,
    withCount = 0

var withMaxWait = debounce(function () {
    console.log('invoke');
    withCount++;
}, 64, {
    'maxWait': 128
});

var start = +new Date;
while ((new Date - start) < limit) {
    withMaxWait();
}
コードを実行して、3回のinvokeを印刷しました.私はまたifのこのコードをコメントしてコードを実行しましたが、結果は1回だけ印刷されました.ソースの英語注釈Handle invocations in a tight loopに合わせて、本来の理想的な実行手順はwithMaxWait-timer->withMaxWait->timerという交互に行われていますが、set Timeoutはメインスレッドのコードの実行が完了するまで待つ必要があるので、このような短い時間でのクイック呼び出しがwithMaxWait-withMaxWait-timer-timer-timer-timer-timer-timer 2から開始されます.lastArgsはundefinedとして設定されていますので、invokeFunnc関数を呼び出さないので、一回だけinvokeを印刷します.
また、invokeFunncを実行するたびにlastArgsをundefinedに設定していますので、trilingEdgeを実行する際にlastArgsを判断します.if(´){}を実行したinvokeFun関数が現れないようにします.また、timerのinvokeFunct関数を実行しました.
このコードはmaxWaitパラメータ設定後の正確性と時効性を保証しています.
【注2】trilingEdgeを一回実行した後、debounced関数を実行すると、shuldInvokeがfalseに戻る場合があります.単独で処理します.
【注3】lodashのdebounceにとって、throttleはleadingがtrueであり、maxWaitがwaitに等しい特殊なdebounceである.