JavaScript関数節流の実現


関数の流れは何ですか?
持続トリガされるイベントについては、間隔時間(n秒)が定められており、期間ごとに一回しか実行できない.
前のページの関数の手ぶれ防止は本編の関数節流と似ていますが、また違っています.
関数の手ぶれ防止とは、イベントがトリガされてからn秒後に実行されるコールバックのことで、このn秒以内に再度トリガされると、カウントダウンを再開します.
両方とも関数の頻繁な呼び出しを防ぐことができます.
違いは、イベントが継続的にトリガされる場合、トリガ時間間隔が所定の待ち時間(n秒)より短い場合、
  • 関数の手ぶれ防止の場合、関数はずっと実行を遅らせて、実行されない効果をもたらします.
  • 関数の節流の場合、関数はn秒ごとに一回実行します.
  • 関数の流れの実現
    関数の流れの実現には、タイムスタンプによって実現されたり、タイマーによって実現されたりします.
    タイムスタンプ
    考え方
    トリガがあれば、Dateで現在の時間を取得し、前回の時間と比較します.
  • 時間差が規定の待ち時間より大きい場合、一回実行できます.
  • ターゲット関数を実行した後、previous値を更新して、「前回」の時間を確保する.
  • でなければ、次のトリガが発生したら比較を続けます.
  • コード
        function throttle(func, wait) {
    
        let previous = 0;
    
        return function() {
            let now = +new Date();
            let context = this;
    
            if (now - previous >= wait) {
                func.apply(context, arguments);
                previous = now; //       previous  
            }
        }
    }
    合図
    container.onmousemove = throttle(doSomething, 1000);
    タイマー
    考え方
    タイマーで時間間隔を実現します.
  • タイマーが存在しない場合、関数が実行できると説明し、タスクキューにターゲット関数を登録するためのタイマーを定義します.
  • ターゲット関数の実行後、保存タイマID変数を空
  • に設定します.
  • は、タイマーが定義されているとき、待ち時間中であることを示している.次のトリガイベントを待ってから確認します.
  • 注①
  • 注①:手ぶれを防ぐためには、すぐにキャンセルして、タイマーを再定義して、時間を計測します.関数による手ぶれ防止(debounce)
    コード
    function throttle(func, wait) {
            let time, context
    
            return function(){
                context = this
                if(!time){
                    time = setTimeout(function(){
                        func.apply(context, arguments)
                        time = null
                    }, wait)
                }
    
            }
    
        }
    効果の違い
    期間:
  • タイムスタンプによって実現される:まずターゲット関数を実行し、後は所定の時間帯を待つ.
  • タイマが実現したのは、所定の時間を待ってから実行します.
  • は、トリガを停止した後、タイマーがすでにタスクキューにターゲット関数を登録していると、最後の時間も実行されます.
  • 最適化:両者の結合
    両者を結合して、一回のトリガを実現して、二回の実行(先に直ちに実行して、最後にも実行があります)
    コード
    function throttle (func, wait) {
        let previous = 0
        let context, args, time
    
        return function(){
            let now = +new Date()
            context = this;
            args = arguments
            if(now - previous >= wait){ //              ,      
                func.apply(context, args)
                previous = now
            } else { //       ,      
                if(time) clearTimeout(time)
                time = setTimeout(
                    () => {
                            func.apply(context, args)
                            time = null
                          }
                , wait)
            }
        }
    }
    問題
    もう一回の触発を実現しました.二回実行して、効果があります.
    問題は、前の周期の「尾」と次の周期の「頭」の間で、時間間隔の制御が失われていることです.
    修復
    よく見ると、問題はpreviousの設定にあります.
    「直接実行可能」の場合のみprevious値が更新され、タイマによりジョブキューに登録されて実行される場合はpreviousの更新は無視される.previousの値は、「前回の実行」ではなく、「前回の直接実行可能な場合に実行する」時間となる.
    同時に、導入変数remainingは、まだ待ち時間が必要であることを示し、最後の一回の実行も時間間隔に合致するようにする.
    完全後コード:
    function throttle(func, wait) {
      let previous = 0;
      let context, args, time, remaining;
    
      return function() {
        let now = +new Date();
        context = this;
        args = arguments;
        remaining = wait - (now - previous)//            
        if (remaining <= 0) {
          func.apply(context, args);
          previous = now //   “     ”   
        } else {
          if (time) {
            clearTimeout(time);
          }
          time = setTimeout(() => {
            func.apply(context, args);
            time = null;
            previous = +new Date() //   “     ”   
          }, remaining) //         
        }
      };
    }
    さらなる最適化
    undersscoreとmqyqingfengを参考にして、一回目/最後のタイムトライアルの実行を有効にするかどうかを実現します.optionsを第3のパラメータとして設定し、どの効果が伝達されているかを判断することで約束する.
  • leading:falseは、第1回実行禁止
  • を表しています.
  • triling:falseは停止トリガを無効にするコールバック
  • を表しています.
    参照コード:
    function throttle(func, wait, options) {
      let time, context, args, result;
      let previous = 0;
      if (!options) options = {};
    
      let later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        time = null;
        func.apply(context, args);
        if (!time) context = args = null;
      };
    
      let throttled = function() {
        let now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        let remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
          if (time) {
            clearTimeout(time);
            time = null;
          }
          previous = now;
          func.apply(context, args);
          if (!time) context = args = null;
        } else if (!time && options.trailing !== false) {
          time = setTimeout(later, remaining);
        }
      };
      return throttled;
    }
    
    キャンセル機能
    関数ブレ止めで、キャンセル方法を実現しました.同じ理屈:
    ...
    throttled.cancel = function() {
        clearTimeout(time);
        time = null;
        previous = 0;
    }
    ...
    
    参考:https://github.com/mqyqingfeng/Blog/issues/26