blog

関数underscore lodashのデバウンス理解の実装方法

デバウンスの標準的な定義がないので、理解するしかないのですが...。 一定時間内に同じイベントが複数回発生した場合、一度だけ発生したイベントだけが実行されます。 これはどういう意味ですか?まず、地下、...

Nov 16, 2020 · 14 min. read
Share this

目次

  • コンセプト
  • 実装方法
  • 改善点
  • Q&A
  • 要約すると

コンセプト

スタビライゼーションに標準的な定義はありません。

一定期間内に同じイベントが複数回トリガーされた場合は、1回トリガーされたイベントのみが実行されます。

それはどういう意味ですか?まず地下鉄やバスの日常生活を見てください。

地下やバスが混んでいると、次から次へと人が乗り込んできて、運転手は最後の一人が乗り込むのを待ってドアを閉め、幼稚園方面へバスを発車させます。

シナリオの内訳は以下の通り。

  • :ドアを閉めて発進
  • イベントをトリガーするタイミング:各ベビーがロードされてから5秒後。

注:1番の赤ちゃんがバスに乗り、ドアが閉まるまで5秒待ち、その5秒の間に2番と3番の赤ちゃんがバスに乗り込んだ場合、3番の赤ちゃんがバスに乗り込んでからドアが閉まるまで5秒待つか、最後の赤ちゃんがバスに乗り込んでからドアが閉まるまで5秒待ちます。

また、開発でそれを使う必要があるシナリオとは?

ブラウザウィンドウの拡大縮小リサイズイベント

ブラウザのスクロールイベント

これを3回言うことが重要です。

覚えておいてください:アンチシェイクは一度しか機能しません!

覚えておいてください:アンチシェイクは一度しか機能しません!

覚えておいてください:アンチシェイクは一度しか機能しません!

上記の導入のいくつかによると、それは、アンチシェイクは、パフォーマンスの最適化を行うには、一定の期間内に同じイベントをトリガする高頻度の問題を解決するために使用されることが知られている必要がありますので、特定のシナリオは完全にビジネスのニーズに基づいていません。

実装方法

その概念を紹介した後、安定化の実施方法を見てみましょう。

ブラウザ・ウィンドウのズーム・サイズ変更イベントをシナリオとし、必要なステップのリストを見てみましょう。

  1. イベントハンドラを実装します。

  2. setTimeout を使ってタイマーを作成し、時間になったらハンドラをトリガーします。

  3. タイマーが作成されるたびに、まずキャンセルされます。

<html>
 <head>
 <title>resize</title>
 <script type="text/javascript">
 //最適化されていない通常の動作では、ブラウザウィンドウのドラッグやズームは常にイベントハンドラ関数をトリガーする。.
 function resize() {
 console.log('拡大縮小されたブラウザウィンドウでresizeイベントをトリガーする.');
 }
 window.onresize = resize;
 </script>
 </head>
 <body>
 <p>ブラウザウィンドウの大きさを変えるとresizeイベントが発生する。.</p>
 </body>
</html>

イベントハンドラを実装します。

<html>
 <head>
 <title>resize</title>
 <script type="text/javascript">
 function resize() {
 console.log('拡大縮小されたブラウザウィンドウでresizeイベントをトリガーする.');
 }
 /**
 *
 * デバウンス関数
 * @param {Function} fn 関数を実行する
 * @param {Number} wait 待ち時間
 * @returns Function 実行可能関数
 */
 function debounce(fn, wait) {
 return fn;
 }
 //ここでは、ウィンドウを強調したい。.onresize受け取った関数は実行可能関数である.リサイズイベントを発生させるたびに実行関数を呼び出す必要があるので、debounce()関数は実行関数を返す。.
 window.onresize = debounce(resize, 3000);
 </script>
 </head>
 <body>
 <p>ブラウザウィンドウのサイズを変えるとresizeイベントが発生する。.</p>
 </body>
</html>

上記は、一般的な構造を作成するためのステップバイステップの分析に過ぎません。次のステップは、ハンドラ関数をトリガーする時間であるタイマーを作成するためにsetTimeoutを使用し始めることです。

<html>
 <head>
 <title>resize</title>
 <script type="text/javascript">
 function resize() {
 console.log('拡大縮小されたブラウザウィンドウでresizeイベントをトリガーする.');
 }
 /**
 *
 * デバウンス関数
 * @param {Function} fn 関数を実行する
 * @param {Number} wait 待ち時間
 * @returns Function 実行可能関数
 */
 function debounce(fn, wait) {
 //returnfnを直接返すので、タイマが付加されず、イベントハンドラが高頻度でトリガされる。.
 //ここでreturnは、onresizeイベントによってトリガーされる無名関数を返す。 この無名関数も頻繁にトリガーされるが、無名関数内部でタイマーを使うことで、fnイベントハンドラの頻度を減らしている、信じられない?実行してみてほしい。.
 return function () {
 console.log('----匿名関数'); //高い頻度でコンソールに出力される。
 setTimeout(fn, wait); //タイマーを使ってfn関数を適切なタイミングで実行する
 };
 }
 //ここでは、ウィンドウを強調したい。.onresize受け取った関数は実行可能関数である.リサイズイベントを発生させるたびに実行関数を呼び出す必要があるので、debounce()関数は実行関数を返す。.
 window.onresize = debounce(resize, 3000);
 </script>
 </head>
 <body>
 <p>ブラウザウィンドウの大きさを変えるとresizeイベントが発生する。.</p>
 </body>
</html>

上記のコードを実行した場合の効果は、次のイメージに似ている必要があります---匿名関数---と比較して、出力の頻度は、ブラウザウィンドウのリサイズイベントをスケーリングすることによってトリガされる関数の出力を実行するためにタイマーを使用することがわかります。頻度ははるかに低いです...

これまで、一定時間内に同じイベントを複数回トリガーしても、トリガーされたイベントは1回しか実行されないというアンチシェイクのコンセプトを確認してきましたタイマーを作成するたびに、まずそのタイマーをキャンセルします。

<html>
 <head>
 <title>resize</title>
 <script type="text/javascript">
 function resize() {
 console.log('拡大縮小されたブラウザウィンドウでresizeイベントをトリガーする.');
 }
 /**
 *
 * デバウンス関数
 * @param {Function} fn 関数を実行する
 * @param {Number} wait 待ち時間
 * @returns Function 実行可能関数
 */
 function debounce(fn, wait) {
 var timerId = null;
 //returnfnを直接返すので、タイマが付加されず、イベントハンドラが高頻度でトリガされる。.
 //ここでreturnは、onresizeイベントによってトリガーされる無名関数を返す。 この無名関数も頻繁にトリガーされるが、無名関数内部でタイマーを使うことで、fnイベントハンドラの頻度を減らしている、信じられない?実行してみてほしい。.
 return function () {
 console.log('----匿名関数'); //高い頻度でコンソールに出力される。
 if (timerId !== null) {
 clearTimeout(timerId); //作成したタイマーをキャンセルする
 }
 timerId = setTimeout(fn, wait); //タイマーを設定する
 };
 }
 //ここでは、ウィンドウを強調したい。.onresize受け取った関数は実行可能関数である.リサイズイベントを発生させるたびに実行関数を呼び出す必要があるので、debounce()関数は実行関数を返す。.
 window.onresize = debounce(resize, 3000);
 </script>
 </head>
 <body>
 <p>ブラウザウィンドウのサイズを変えるとresizeイベントが発生する。.</p>
 </body>
</html>

以上の3つのステップが全て実装されているため、ブラウザウィンドウが常にスケーリングしている時に作成されるタイマー機能は低い頻度で実行されるのではなく、ブラウザウィンドウのスケーリングが3000ms(3秒)停止した後にのみスケーリングイベントが実行されるため、3つ目のステップが存在します。

debounce アンダースコアの部分

/** * * underscore. debounce 関数を理解する * @param {Function } イベント処理関数 * @param {Number} wait トリガー時間 * @param {Boolean} immediate イベントハンドラを即座に実行するかどうか * @returns Function 実行可能関数 */ function debounce(func, wait, immediate) { //timeoutタイマーが作られたかどうかを記録する //resultfunc関数の戻り値を記録する var timeout, result; var later = function (context, args) { timeout = null; if (args) result = func.apply(context, args); }; // restArguments関数を理解するために、まずrest引数のES6構文を見てみよう。,https://../-//////_rs // restArgumentsこれはunderscoreがES6ライクなrestパラメータ構文を実装するために提供する関数で、実行可能な関数を返す。 // restArguments関数は引数を処理するためだけに呼ばれるか、無名関数として扱われる。. // var debounced=function(){} var debounced = restArguments(function (args) { //タイマーが存在する場合はキャンセルする。. if (timeout) clearTimeout(timeout); //上記で紹介したデバウンスの概念によれば、デバウンスは一度しか実行されず、混雑したバスや地下のシーンでは、一度しか実行されないことがわかるだろう。'この'一般的にはイベントが最後にトリガーされたときに実行されるが、ビジネスシナリオによってはイベントが最初にトリガーされることもある。.つまり、即時パラメータは以下の2つのケースを判断するために使われる。 //true:1.最初にイベントが発生すると、即座にイベントハンドラが実行される。; //false:2.最後にトリガーされたイベントはイベントハンドラを実行する; //どちらの場合でも、"debounce only once "という表現は変わらない。!" //ifの最初のトリガーイベントをどのように行うかを直接見て、すぐにイベントハンドラ関数を実行し、すべてのキャンセル後に. if (immediate) { //さて、IMMEDIATEというif文が入った。=trueすぐに実行する必要がある //まず、タイマーの存在を知る。 var callNow = !timeout; //ハイライト - ハイライト - ハイライト //次に、タイマーを作成し、イベント処理関数をトリガする時間まで待って、タイマーがここでトリガされますが、後の関数は、常にfalseの場合、要するに、タイマーによってトリガされることに注意してください、タイマー関数は、トリガされませんので、後の関数をトリガするsetTimeoutこのフォームを介してパラメータに渡すことはできませんので、後の関数がある未定義;これと以下のifを併用することで、最初のトリガーの後、それ以降のトリガーは全てキャンセルされるようになる。. timeout = setTimeout(later, wait); //if(タイマーが存在しない場合)、setTimeoutを通してタイマーを設定する必要はなく、すぐにfunc関数を実行することで、初めてイベントがトリガーされたときにすぐに if (callNow) result = func.apply(this, args); } else { //delayアンダースコアのユーティリティ関数として、2つのことを行う。 //1.引数でrestArgumentsを呼ぶ。 //2.戻り値 setTimeout //あるいは直感的にタイムアウトと理解する=setTimeout(later,wait);これだけが後にパラメータを渡すことができないので、ここでは this内の遅延関数は、restArgumentsを介してargsパラメータは、後の関数を渡すために timeout = delay(later, wait, this, args); } //戻り値についての注意点は、もし即座に=trueif関数を入力するとresultが返ってくる。;即座に=false,result常に未定義 //なぜこのようなことが起こるかというと、ifを入力した直後に関数の結果が実行されるからである。= func.apply(this, args),そこで、戻り値を得る;ディレイ関数は、実際にはsetTimeoutタイマであり、コードの非同期実行のために、トリガ時間まで待機していないため、それ以外のコードが実行された結果を返す。;で、結果は=undefined . return result; }); //タイマーをキャンセルするstaticメソッドを追加する。 debounced.cancel = function () { clearTimeout(timeout); timeout = null; }; return debounced; }

ソースコードの理解はもう少し味わう必要が...。ということで、上記の理解をまとめると以下のようになります。

  • イベントハンドラ関数の実装

  • 即時実施、最終実施の達成

  • パラメータの取り扱い

  • 戻り値の扱い

  • キャンセルタイマーの実装

debounce ロダッシュ

/** * * lodash. debounce 関数を理解する * @param {Function } イベント処理関数 * @param {Number} wait トリガー時間 * @param {Object} options オプション * @returns Function 実行可能関数 */ function debounce(func, wait, options) { var lastArgs, lastThis, maxWait, //最大待ち時間、このパラメータは実際にスロットリングに使われる。 result, timerId, lastCallTime, //前回イベントが発生した lastInvokeTime = 0, //最後にイベントハンドラが実行された leading = false, //最初にイベントが発生すると、即座にイベントハンドラが実行される。 maxing = false, // 開くかどうかは、パラメータによると、実際にはスロットリングモードを開始するかどうかを決定することである trailing = true; //イベントをトリガーしたら、イベントハンドラ関数を実行するまで待つ。 if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } wait = toNumber(wait) || 0; //待ち時間 if (isObject(options)) { leading = !!options.leading; //ブール値に変換する maxing = 'maxWait' in options; //最大待ち時間を設定すると、断続的にイベントを発生させる最大間隔に応じて、スロットリングの効果を得たい。 maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; //最大間隔の設定 trailing = 'trailing' in options ? !!options.trailing : trailing; } /** * * イベントハンドラの即時実行 * @param {Number} time 呼び出し時間 * @returns */ function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; // lastInvokeTime = time; //イベントハンドラの最終実行時刻をリセットする。=今回は result = func.apply(thisArg, args); //関数を実行する return result; } /** * ハイライト - ハイライト - ハイライト * 関数名からすると、この関数はイベントが最初にトリガーされたときにinvokeFunc関数を呼び出す必要があるかどうかを判断する役割だけを担っている。 * しかし、タイマーも作っているのに、なぜ(1)と(2)をやるのか?.イベント関数を即座に実行するかどうかの判定;2.タイマーを作る)??? * 最初のことをするのが本来やるべきことで、2番目のことはその後の操作の道を開くためのタイマーを作ることである。 * デバウンス関数の第3パラメータは設定可能であることをお忘れなく。{leading:true,trailing:true}両引数がtrueの場合も存在する。 * つまり:trueイベントが最初にトリガーされると、イベントハンドラは即座に実行されるが、末尾の:trueトリガーイベント後にイベントハンドラを実行する * ということで、末尾の関数にタイマーを作成する。:trueの場合の準備. * なぜtrueが許されるのかについては、記事末尾のQ&Aコーナーの最初の質問を参照のこと。 * @param {Number} time 呼び出し時間 * @returns */ function leadingEdge(time) { lastInvokeTime = time; //イベントハンドラの最終実行時刻をリセットする。=今回は timerId = setTimeout(timerExpired, wait); //タイマーを作る //先頭のオプションに従ってinvokeFuncを呼び出す必要がある場合。 return leading ? invokeFunc(time) : result; } /** * * setTimoutトリガーまでの時間を計算する * @param {*} time * @returns */ function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } /** * * 時間差に応じて計算を開始する必要がある。 * @param {Number} time * @returns Boolean */ function shouldInvoke(time) { //2つのデバウンス関数の時間差=デバウンスをトリガーする時間 - デバウンスをトリガーする時間。 //(スロットリングモードは)イベント処理関数の時間差を利用する=デバウンスがトリガーされる時間 - イベントハンドラが実行される時間。 var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; //以下はそれぞれの場合の判定で、いずれかの項目がtrueを返す限り、呼び出しが開始される。 //1. lastCallTime === undefined 値がundefinedの場合は、初めて関数が起動されたことを意味する。 //2. timeSinceLastCall >= wait 2つのデバウンス関数の時間差が、デバウンス関数の待ち時間より大きい場合は、次の例のように、デバウンス関数とデバウンス関数が同じバッチではなく、再度スタートすることを意味する。 // タイマーが5秒後に実行される必要があり、その5秒間にデバウンスイベントが連続してトリガーされた場合、その間にトリガーされた各デバウンスイベントの時間差を計算する。, //時間差が5秒未満の場合、5秒以内にイベントが発生した場合、falseを返し、一方、時間差が5秒以上の場合、1〜5秒以内にイベントが発生しなかった場合、tureを返す。 //3. timeSinceLastCall < 0 次の秒は必ず前の秒より大きくなるのではないか?手動でシステム時刻をある日にさかのぼるように調整すると、その差は0(過去のある時刻-現在のある時刻)以下にならない。<0) //4. maxing && timeSinceLastInvoke >= maxWait maxingtrue値はインターバルが設定されていることを意味する。, // timeSinceLastInvoke >= maxWait イベント処理関数間の時間差が、イベントに設定された最大間隔より大きいかどうかを判定する。. return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait) ); } /** * * setTimeout(timerExpired)関数はタイマーが切れた後に実行する必要がある。, wait) * 残り時間を常に再計算する * @returns */ function timerExpired() { var time = now(); if (shouldInvoke(time)) { return trailingEdge(time); } // 時間を再計算する timerId = setTimeout(timerExpired, remainingWait(time)); } /** * * トリガーイベントが終わった後にinvokeFunc関数を呼ぶかどうかを決める。 * デフォルトの末尾:true イベントハンドラはトリガーイベントの終了後に実行される。 * @param {Number} time 呼び出し時間 * @returns */ function trailingEdge(time) { timerId = undefined; //末尾のオプションによって、lastArgsがinvokeFuncを呼び出すかどうかを決める。 if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } /** * * タイマーをキャンセルする */ function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } /** * * 即時トリガハンドラ * @returns */ function flush() { return timerId === undefined ? result : trailingEdge(now()); } /** * * 実行可能な関数を返す * var debounced=function(){} * @returns */ function debounced() { var time = now(), isInvoking = shouldInvoke(time); lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } //maxingインターバルの設定 if (maxing) { clearTimeout(timerId); timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; }

改善点

lodashのこのコードを見て、言葉が足りなくなったらお許しを...。underscoreやlodashには及ばないので、少なくとも以下の点を自分のブレ対策に加えるべきです。

  • 最初のトリガーの直後、または最後のトリガーイベントの直後に実行されるように関数を設定します。

  • パラメータの扱い

  • 戻り値

<html>
 <head>
 <title>resize</title>
 <script type="text/javascript">
 function resize() {
 console.log('拡大縮小されたブラウザウィンドウでresizeイベントをトリガーする.');
 return 'resizeこの関数は.';
 }
 /**
 *
 * デバウンス関数
 * @param {Function} fn 関数を実行する
 * @param {Number} wait 待ち時間
 * @param {Boolean} immediate 即時実行
 * @returns Function 実行可能関数
 */
 function debounce(fn, wait, immediate) {
 var timerId = null;
 var result;
 //returnfnを直接返すので、タイマが付加されず、イベントハンドラが高頻度でトリガされる。.
 //ここでreturnは、onresizeイベントによってトリガーされる無名関数を返す。 この無名関数も頻繁にトリガーされるが、無名関数内部でタイマーを使うことで、fnイベントハンドラの頻度を減らしている、信じられない?実行してみてほしい。.
 return function () {
 var that = this;
 var args = arguments;
 if (timerId !== null) {
 clearTimeout(timerId); //作成したタイマーをキャンセルする
 }
 if (immediate) {
 if (timerId === null) {
 //即時実行
 result = fn.apply(that, args);
 }
 //上記イベントハンドラ関数の実行直後に、timerIdを設定した時間にタイマーを作成する=null,
 //待機期間が常にイベントが実行されないトリガであることを保証するために、そうでない場合は、上記のtimerIdの状態を決定することができない場合は、トリガを続行するように設定する
 timerId = setTimeout(function () {
 timerId = null;
 }, wait);
 } else {
 timerId = setTimeout(function () {
 //result経由で戻り値を受け取っても、外部からは受け取らない。
 result = fn.apply(that, args);
 }, wait); //タイマーを設定する
 }
 return result;
 };
 }
 //ここでは、ウィンドウを強調したい。.onresize受け取った関数は実行可能関数である.リサイズイベントを発生させるたびに実行関数を呼び出す必要があるので、debounce()関数は実行関数を返す。.
 window.onresize = debounce(resize, 3000, false);
 </script>
 </head>
 <body>
 <p>ブラウザウィンドウのサイズを変えるとresizeイベントが発生する。.</p>
 </body>
</html>

Q&A

前述したように、debounceは一度しか実行されません。そのため、lodash版のdebounceは、最初にトリガーされたイベントのイベントハンドラと最後にトリガーされたイベントのイベントハンドラを実行するように、パラメータ

重要:手ぶれ補正は一度しか機能しません!

それはイベントがイベントハンドラ関数の実装をトリガした最初の時間であるかどうか、イベントがイベントハンドラ関数の実装をトリガした最後の時間は、ちょうど実行時間が異なっている、あなたはアンチシェイク関数のアンダースコアバージョンを参照してください信じていないデバウンス第3パラメータ即時の実装または最後の実装の実装を判断するために使用され、2回の実装を許可していません状況の存在。

{leading:false,trailing :true}{leading:true,trailing :true}{leading:true,trailing :true,maxWait:1000}debounceのlodashバージョンに戻ると、注意深く見れば、あなたはdebounceの第3パラメータのデフォルトの設定を見つけるでしょう 、つまり、アンチシェイクの認識は一度だけ実行され、状況の構成が存在することを許可する理由は、debounce関数の実装を完了するためにdebounce関数を呼び出すことによって、lodashのスロットル関数の実装によるものですので、それは理解することができます ここでは、2つのtrueとmaxWait引数は、スロットル関数のために提供されています。つのtrueとmaxWait引数はスロットル関数のために用意されています。

{leading:false,trailing :true}従って、lodashのアンチディザリングやスロットリングを使うには、ビジネスシナリオにマッチさせる必要があり、アンチディザリングを使う場合のコンフィギュレーションを混ぜるなどして、そのようなシナリオの存在を最小限にするようにする必要があります。

アンダースコアとロダッシュの安定化の違いは何ですか。

lodashは時間差を計算してタイマーをセットします。

underscoreはディザリングとスロットリングを2つの別々の関数に分割し、それぞれが独自の処理を行います。一方、lodashのスロットリング関数はディザリング関数を内部的に呼び出し、設定項目によって区別されます。

要約すると

以上、デバウンスの理解について、アンダースコアのロダッシュを振る機能を実現する方法について、もう少しご指導ください。

Read next

No articles found.

No articles found.