BFE.devでのAI解答例 - JavaScript コーディング問題
7. leading & trailingをサポートするdebounce()を実装する
function debounce(func, wait, option = { leading: false, trailing: true }) { let timeoutId; let lastArgs; let lastThis; let maxWait; let result; let lastCallTime = 0; let lastInvokeTime = 0; const leadingCall = () => { lastInvokeTime = Date.now(); timeoutId = setTimeout(timerExpired, wait); return option.leading ? invokeFunc(lastArgs, lastThis) : result; }; const timerExpired = () => { const timeSinceLastCall = Date.now() - lastCallTime; const timeSinceLastInvoke = Date.now() - lastInvokeTime; if (timeSinceLastCall < wait && timeSinceLastCall >= 0) { timeoutId = setTimeout(timerExpired, wait - timeSinceLastCall); } else { timeoutId = null; if (!option.leading) { result = invokeFunc(lastArgs, lastThis); } if (!timeoutId && maxWait !== wait) { result = invokeFunc(lastArgs, lastThis); } } }; const invokeFunc = (args, context) => { lastInvokeTime = Date.now(); lastThis = context; lastArgs = args; lastCallTime = lastInvokeTime; return func.apply(context, args); }; const leadingTimerExpired = () => { timeoutId = null; result = invokeFunc(lastArgs, lastThis); }; const debounced = (...args) => { lastArgs = args; const time = Date.now(); lastThis = this; if (!lastInvokeTime && option.leading === false) { lastInvokeTime = time; } const timeSinceLastCall = time - lastCallTime; if (timeSinceLastCall >= wait || timeSinceLastCall < 0) { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } lastCallTime = time; result = invokeFunc(args, this); } else if (!timeoutId && option.trailing !== false) { timeoutId = setTimeout( option.leading === false ? leadingTimerExpired : timerExpired, wait - timeSinceLastCall ); } if (option.maxWait !== undefined) { maxWait = Number(option.maxWait); const timeSinceLastInvoke = time - lastInvokeTime; if (timeSinceLastInvoke >= maxWait || timeSinceLastInvoke < 0) { invokeFunc(args, this); } else if (!timeoutId) { timeoutId = setTimeout(timerExpired, maxWait - timeSinceLastInvoke); } } return result; }; debounced.cancel = () => { clearTimeout(timeoutId); lastInvokeTime = 0; lastArgs = null; lastThis = null; lastCallTime = 0; timeoutId = null; }; debounced.flush = () => { if (timeoutId) { clearTimeout(timeoutId); timerExpired(); } }; return debounced;}
解説:
lodash.debounce()ではない手作りのdebounce()を実装する続きです。新しい変数と分岐がたくさんあるので、注意深く読んで実装を見てください。
Code Breakdown:
- 関数
debounce()を宣言し、以下の引数を受け取ります:func(Function): 処理したい関数。wait(Number): 待機する時間(ミリ秒)。option.leading(Boolean):funcの1回目を呼び出すかどうか。デフォルトはfalse。option.trailing(Boolean): 待機タイマーの後にfuncを呼び出すかどうか。デフォルトはtrue。
timeoutId変数を宣言し、nullに設定します。lastArgs変数を宣言し、空配列に設定します。これは、実行される関数に渡される引数を格納します。lastThis変数を宣言し、nullに設定します。これは、実行される関数のコンテキストを格納します。maxWait変数を宣言し、nullに設定します。result変数を宣言し、undefinedに設定します。lastCallTime変数を宣言し、0に設定します。lastInvokeTime変数を宣言し、0に設定します。leadingCall()関数を宣言し、以下の手順で行います。Date.now()を呼び出し、現在時刻を取得します。これはlastInvokeTimeに保存されます。setTimeout()を使用して、waitミリ秒後にtimerExpired()関数を呼び出します。これはtimeoutIdに保存されます。- もし
option.leadingがtrueなら、invokeFunc()を呼び出して、resultを返します。 そうでなければresultはundefinedのままです。
timerExpired()関数を宣言します。Date.now()を呼び出して、lastCallTimeから現在時刻までの経過時間を取得します。Date.now()を呼び出して、lastInvokeTimeから現在時刻までの経過時間を取得します。- もし
wait - timeSinceLastCallが0以上である場合で、timeoutIdがnullである場合は、setTimeout()を使ってwait - timeSinceLastCall時間後にtimerExpired()関数を呼び出します。 timeoutIdがnullである場合、option.leadingがfalseではない場合は、resultをinvokeFunc()から呼び出します。!timeoutIdかつmaxWaitがwaitと異なる場合、resultをinvokeFunc(lastArgs, lastThis)から呼び出します。
invokeFunc(args, context)関数を宣言します。Date.now()を呼び出して、現在時刻を取得します。これは、lastInvokeTimeに保存されます。lastThisにcontextをセットします。lastArgsにargsをセットします。lastCallTimeをlastInvokeTimeに設定し、それによってこれらの変数が一致するようにします。contextと同じthisでfuncを実行して、その結果を返します。
leadingTimerExpired()関数を宣言し、resultをinvokeFunc()から呼び出し、timeoutIdをnullに設定します。lastCallTime、lastArgs、lastThis、lastInvokeTime、timeoutIdの値を最初の状態にリセットするcancel()関数を宣言します。timeoutIdがある場合、timerExpired()関数を直ちに呼び出して、resultを返すflush()関数を宣言します。debounced関数を宣言し、以下の手順で行います。argsをlastArgsに設定しましょう。Date.now()を呼び出して現在時刻を取得します。これはtimeに保存されます。thisをlastThisに設定します。option.leadingがfalseでlastInvokeTimeが nullである場合は、lastInvokeTimeにtimeを保存します。timeSinceLastCallを計算し、waitよりも大きいまたは0未満である場合は、以下の順番で実行します。timeoutIdがある場合(前回のタイムアウトがまだ働いている場合)、clearTimeout()関数を呼び出してタイムアウトをキャンセルし、timeoutIdをnullに設定します。lastCallTimeにtimeを設定します。resultにinvokeFunc(args, this)を設定します。
timeoutIdがnullでかつoption.trailingがfalseではない場合、以下の手順で実行してください。setTimeout()を使用して、wait - timeSinceLastCall時間後にleadingTimerExpired()かtimerExpired()を呼び出します。前者はoption.leadingがfalseのときで、後者はoption.leadingがfalseでない場合です。それらのうち、最初の一回だけが実行されます。timeoutIdを設定します。
option.maxWaitが存在する場合は、以下の手順で実行してください。maxWaitにoption.maxWaitをNumber()関数を使って変換します。timeSinceLastInvokeを計算し、maxWait以上であるか、または0未満である場合、invokeFunc(args, this)を呼び出しします。timeoutIdがnullである場合は、以下の手順で実行してください。setTimeout()を使用して、maxWait -timeSinceLastInvoke時間後にtimerExpired()を呼び出します。timeoutIdを設定します。
resultを返します。
cancel()とflush()メソッドを、それぞれキャンセルと即座に呼び出すようにdebounced関数に追加します。debounced関数を返します。