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
関数を返します。