Masalah dan Cakupan
Implementasikan debounce(fn, wait, options) dan throttle(fn, wait, options) untuk kode sisi peramban (browser). Fungsi yang dikembalikan harus mempertahankan argumen terbaru dan this, mengembalikan hasil pemanggilan terbaru, serta mengekspos cancel() dan flush(). Opsinya adalah leading, trailing, dan, untuk debounce, maxWait.
Secara default, debounce hanya dijalankan pada sisi trailing (akhir). Pemanggilan leading berjalan di awal lonjakan (burst). Jika leading dan trailing sama-sama diaktifkan, satu pemanggilan terisolasi berjalan sekali di sisi leading; pemanggilan kedua dalam periode tunggu menciptakan pemanggilan trailing dengan argumen terbaru. maxWait mencegah aliran event yang terus-menerus menunda pekerjaan selamanya. Throttle mengizinkan paling banyak satu pemanggilan per interval berukuran wait dan mendukung perilaku leading serta trailing.
Ini adalah pertanyaan frontend karena kontraknya didorong oleh input browser, scroll, resize, rendering, dan siklus hidup komponen. Utilitas yang sama dapat berjalan di Node.js, tetapi perubahan runtime tidak mengubah keahlian inti wawancara: menerjemahkan semantik timing UI ke dalam state machine kecil. Implementasi ini menggunakan JavaScript dan status tambahan O(1).
Apa yang Dievaluasi oleh Pewawancara
Sinyal pertama adalah apakah kandidat mendefinisikan kontrak sebelum menulis timer. "Debounce menunggu; throttle membatasi" tidak menyelesaikan perilaku leading, apakah pemanggilan leading tunggal juga memicu trailing, argumen mana yang menang, atau apa arti pembersihan (cleanup). Jawaban yang kuat menuliskan timeline pemanggilan dan memperjelas pilihan-pilihan tersebut.
Sinyal kedua adalah penalaran state. Satu variabel timer tidak cukup setelah maxWait, pergerakan jam, nilai kembalian, dan flush() muncul. Implementasi membutuhkan waktu pemanggilan terakhir, waktu eksekusi aktual terakhir, argumen dan receiver yang tertunda, handle timer, serta hasil terbaru. Setiap bagian harus sesuai dengan aturan kontrak.
Sinyal ketiga adalah penilaian peramban (browser judgment). Timer memberikan penundaan minimum, bukan batas waktu yang tepat; tugas panjang (long tasks) dan pembatasan latar belakang (background throttling) dapat menjalankan callback terlambat. Men-throttle pekerjaan scroll dengan requestAnimationFrame() menyelaraskan pekerjaan dengan proses paint tetapi tidak dengan sendirinya mengurangi laju event. Terkadang scrollend, IntersectionObserver, atau hook pembersihan framework menghilangkan kebutuhan akan utilitas timer generik.
Terakhir, pewawancara memeriksa pengujian daripada intuisi visual. Jeda berbasis wall-clock membuat pengujian menjadi lambat dan tidak stabil (flaky). Kandidat yang kuat menginjeksi atau mengganti waktu dan timer, memajukan fake clock, serta menegaskan urutan pemanggilan yang tepat untuk batasan (boundaries), pembatalan, flushing, dan input terus-menerus.
Pertanyaan untuk Diklarifikasi Sebelum Menjawab
- Apa nilai default-nya? Jawaban ini menggunakan debounce
{ leading: false, trailing: true }dan throttle{ leading: true, trailing: true }. Default yang berbeda mengubah perilaku pemanggilan terisolasi dan pengujian. - Apa yang terjadi jika kedua edge diaktifkan? Pemanggilan tunggal hanya dieksekusi pada sisi leading. Pemanggilan trailing terjadi hanya jika pemanggilan lain tiba selama periode tunggu. Ini menghindari duplikasi aksi yang terisolasi.
- Argumen dan receiver mana yang digunakan oleh pemanggilan yang tertunda? Fungsi menggunakan argumen dan
thisdari pemanggilan tertunda terbaru. Menangkap event pertama akan membuat pekerjaan autocomplete atau resize menjadi usang (stale). - Bisakah input berlanjut selamanya? Jika ya, debounce yang hanya menggunakan trailing mungkin tidak akan pernah berjalan.
maxWaitmenetapkan penundaan maksimum dari pemanggilan nyata sebelumnya atau awal dari burst saat ini. - Apa yang seharusnya dilakukan oleh
cancel()danflush()? Cancel menghapus pekerjaan yang tertunda dan mereset state burst. Flush segera melakukan pemanggilan trailing yang memenuhi syarat dan mengembalikan hasilnya; ini tidak boleh menyebabkan duplikat di kemudian hari. - Apakah waktu berlalu yang persis diperlukan? Tidak ada timer peramban yang menjamin penjadwalan tepat waktu. Kontrak mengontrol kelayakan paling awal dan pengurutan; pengujian menggunakan fake clock untuk menghilangkan derau scheduler.
Jawaban 30 Detik
“Pertama-tama saya akan mendefinisikan semantik leading dan trailing dengan sebuah timeline. Debounce mengelompokkan burst dan biasanya dijalankan setelah wait milidetik tanpa pemanggilan lain. Throttle menjamin progres berkala selama burst berlangsung. Saya menyimpan argumen dan receiver terbaru, waktu pemanggilan terakhir, waktu eksekusi aktual terakhir, satu timer, dan hasil terbaru.
Pada setiap pemanggilan, saya memutuskan apakah eksekusi memenuhi syarat sekarang. Jika tidak, saya menjadwalkan sisa waktu tunggu. Timer memeriksa ulang kelayakan karena pemanggilan berikutnya mungkin telah menggeser batas waktu trailing. maxWait mencegah kelaparan (starvation); throttle adalah state machine yang sama dengan maxWait yang dipatok ke wait. cancel() membersihkan state yang tertunda, dan flush() melakukan satu pemanggilan trailing yang tertunda. Saya menguji dengan fake clock, terutama pemanggilan tepat pada batas waktu, leading plus trailing, input terus-menerus, pembatalan, flushing, dan pembersihan saat unmount.”
Pembahasan Mendalam Langkah demi Langkah
Langkah 1: Ubah kata-kata menjadi timeline
Asumsikan wait = 100 ms dan pemanggilan A@0, B@40, C@90, dan D@220. Debounce yang hanya trailing menghasilkan C@190 dan D@320: setiap pemanggilan menggeser batas waktu periode hening, dan argumen terbaru yang menang. Debounce leading-dan-trailing menghasilkan A@0, C@190, dan D@220. D terisolasi, sehingga tidak berjalan lagi pada 320.
Throttle leading-dan-trailing menghasilkan paling banyak satu pemanggilan per jendela 100 ms sambil mempertahankan nilai tertunda terakhir. Untuk burst pertama, itu berarti A langsung dan nilai tertunda terbaru, C, pada batas trailing. Timestamp persis di peramban nyata mungkin lebih lambat dari batas konseptual, tetapi tidak pernah lebih awal.
Timeline ini mengungkap transisi state: pemanggilan saat idle dapat membuka burst; pemanggilan berikutnya menggantikan data yang tertunda; timer mengeksekusi atau menjadwalkan ulang; eksekusi menghapus data yang tertunda tetapi mempertahankan hasilnya. Menuliskan transisi ini terlebih dahulu mencegah sebagian besar bug off-by-one dan duplikasi trailing.
Langkah 2: Implementasikan satu state machine yang eksplisit
Implementasi di bawah ini mengikuti kontrak yang telah dinyatakan. shouldInvoke() menangani pemanggilan pertama, batas periode hening, jam yang mundur, dan maxWait. remainingWait() memilih batas waktu yang lebih awal antara trailing dan waktu tunggu maksimum.
function debounce(fn, wait, options = {}) {
wait = Math.max(0, Number(wait) || 0);
const leading = options.leading === true;
const trailing = options.trailing !== false;
const hasMaxWait = Number.isFinite(options.maxWait);
const maxWait = hasMaxWait
? Math.max(wait, options.maxWait)
: 0;
let timerId;
let lastArgs;
let lastThis;
let lastCallTime;
let lastInvokeTime = 0;
let result;
function invoke(time) {
const args = lastArgs;
const receiver = lastThis;
lastArgs = undefined;
lastThis = undefined;
lastInvokeTime = time;
result = fn.apply(receiver, args);
return result;
}
function shouldInvoke(time) {
const sinceCall = time - lastCallTime;
const sinceInvoke = time - lastInvokeTime;
return lastCallTime === undefined
|| sinceCall >= wait
|| sinceCall < 0
|| (hasMaxWait && sinceInvoke >= maxWait);
}
function remainingWait(time) {
const sinceCall = time - lastCallTime;
const trailingWait = wait - sinceCall;
if (!hasMaxWait) return trailingWait;
const sinceInvoke = time - lastInvokeTime;
return Math.min(trailingWait, maxWait - sinceInvoke);
}
function trailingEdge(time) {
timerId = undefined;
if (trailing && lastArgs) return invoke(time);
lastArgs = undefined;
lastThis = undefined;
return result;
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) return trailingEdge(time);
timerId = setTimeout(timerExpired, remainingWait(time));
}
function leadingEdge(time) {
lastInvokeTime = time;
timerId = setTimeout(timerExpired, wait);
return leading ? invoke(time) : result;
}
function cancel() {
if (timerId !== undefined) clearTimeout(timerId);
timerId = undefined;
lastArgs = undefined;
lastThis = undefined;
lastCallTime = undefined;
lastInvokeTime = 0;
}
function flush() {
if (timerId === undefined) return result;
clearTimeout(timerId);
return trailingEdge(Date.now());
}
function debounced(...args) {
const time = Date.now();
const invokeNow = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (invokeNow) {
if (timerId === undefined) return leadingEdge(time);
if (hasMaxWait) {
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invoke(time);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
function throttle(fn, wait, options = {}) {
return debounce(fn, wait, {
leading: options.leading !== false,
trailing: options.trailing !== false,
maxWait: wait,
});
}State-nya adalah O(1), dan setiap pemanggilan wrapper melakukan pekerjaan O(1). Biaya callback berada di luar kompleksitas utilitas ini. Kode produksi mungkin mengimpor implementasi yang terawat; nilai dalam wawancara adalah kemampuan untuk menjelaskan dan menguji kontraknya.
Langkah 3: Jelaskan mengapa timer memeriksa ulang
Misalkan pemanggilan pertama menjadwalkan timer untuk 100 ms, lalu pemanggilan kedua tiba pada 90 ms. Jika timer asli secara membabi buta mengeksekusi pada 100, periode hening hanya 10 ms. Sebaliknya, timerExpired() menghitung ulang sinceCall, melihat bahwa keheningan 100 ms belum tercapai, dan menjadwalkan sisa 90 ms.
Menghapus dan membuat timer pada setiap event adalah implementasi trailing-only sederhana yang valid. State machine yang memeriksa ulang sepadan dengan kompleksitasnya karena ia juga mendukung pemanggilan leading, maxWait, nilai kembalian, dan semantik throttle. Jika pertanyaan hanya meminta debounce trailing dasar, gunakan solusi yang lebih kecil dan nyatakan bahwa kontrak yang lebih kaya akan membutuhkan lebih banyak state.
Langkah 4: Mencegah starvation dengan maxWait
Kueri autocomplete biasanya harus menunggu adanya jeda. Buffer telemetri atau autosave tidak dapat menunggu selamanya sementara input terus berlanjut. Dengan wait = 300 ms dan maxWait = 1000 ms, pemanggilan berulang setiap 100 ms tetap terpicu setidaknya sekali di sekitar setiap batas maksimum 1000 ms, bergantung pada penundaan penjadwalan runtime.
maxWait juga merupakan jembatan ke throttle. Menyetelnya sama dengan wait berarti pemanggilan terus-menerus tidak dapat menunda eksekusi melampaui satu interval. Mempertahankan satu implementasi menghindari pergeseran perilaku edge antara dua timer state machine. Penurunan ini adalah pilihan implementasi, bukan satu-satunya definisi throttle yang valid; kontrak dan pengujian tetap menjadi acuan utama.
Langkah 5: Tangani siklus hidup dan efek samping
Pekerjaan yang tertunda dapat hidup lebih lama daripada UI yang menjadwalkannya. Komponen harus memanggil cancel() selama pembersihan agar callback lama tidak memperbarui state yang telah di-unmount, menggunakan props usang, atau mengirim permintaan setelah navigasi. Jika produk memerlukan penyimpanan teks yang tertunda sebelum teardown, panggil flush() secara sengaja lalu bersihkan; jangan membuat setiap unmount mengirimkan data secara diam-diam.
Mendebounce pencarian asinkron mengontrol pembuatan permintaan, bukan urutan respons. Setelah permintaan dimulai, respons lama yang lebih lambat masih dapat menimpa hasil yang lebih baru. Gunakan AbortController, pembuatan nomor generasi permintaan, atau pemeriksaan respons terbaru selain debounce. Rate limiting dan perlindungan respons usang menyelesaikan mode kegagalan yang berbeda.
Langkah 6: Pilih primitif peramban berdasarkan pekerjaan sebenarnya
Gunakan debounce ketika hanya nilai akhir (settled) yang penting, seperti validasi setelah pengetikan berhenti. Gunakan throttle ketika progres perantara penting, seperti mengambil sampel pointer atau status scroll secara berkala. Gunakan requestAnimationFrame() untuk menyelaraskan penulisan visual dengan paint, tetapi jangan mengklaim bahwa ini secara otomatis mengurangi frekuensi event scroll. Gunakan IntersectionObserver untuk visibilitas berbasis ambang batas dan scrollend ketika event yang dibutuhkan secara khusus adalah akhir dari scrolling.
Aturan keputusannya didasarkan pada perilaku: membuang state perantara, mempertahankan progres berkala, menyelaraskan pekerjaan dengan paint, atau mengamati ambang batas yang ditentukan browser. Memilih primitif berdasarkan kebiasaan dapat membuang-buang pemrosesan atau menyembunyikan pembaruan yang seharusnya terlihat oleh pengguna.
Langkah 7: Uji dengan waktu virtual
Ganti Date.now, setTimeout, dan clearTimeout dengan fake clock, atau gunakan fake timers dari test runner. Catat nilai dan timestamp virtual. Majukan waktu hingga tepat sebelum dan tepat pada setiap batas waktu. Jangan menegaskan bahwa timeout 100 ms di dunia nyata berjalan tepat pada 100 ms.
Matriks minimum mencakup burst trailing-only, burst leading-only, leading plus trailing dengan satu dan banyak pemanggilan, maxWait di bawah input terus-menerus, pemanggilan tepat pada wait, argumen dan receiver terbaru, penggunaan kembali hasil, cancel sebelum deadline, flush sebelum deadline, flush berulang, wait bernilai nol, dan callback yang menjadwalkan pemanggilan wrapper lainnya. Untuk integrasi UI, uji juga pembersihan dan respons jaringan yang usang.
Contoh Jawaban yang Kuat
“Saya akan mendefinisikan timeline sebelum menulis kode. Dengan debounce trailing 100 ms, pemanggilan pada 0, 40, dan 90 menghasilkan satu pemanggilan pada waktu pertama yang memenuhi syarat setelah 190 dengan argumen terakhir. Versi leading-dan-trailing memanggil pada 0 dan 190, tetapi satu pemanggilan leading yang terisolasi tidak menduplikasi pada sisi trailing.
State saya adalah satu timer, argumen dan receiver yang tertunda, waktu pemanggilan terakhir, waktu eksekusi aktual terakhir, dan hasil terbaru. Timer tidak pernah berasumsi bahwa ia masih memenuhi syarat; ia memeriksa ulang karena pemanggilan yang lebih baru dapat menggeser deadline hening. maxWait menambahkan batas waktu kedua sehingga input yang terus-menerus tidak membuat callback kelaparan (starvation). Throttle menggunakan kembali state machine yang sama dengan maxWait sama dengan wait.
Saya mempertahankan this, mengembalikan hasil eksekusi terakhir, membersihkan state tertunda di cancel(), dan membuat flush() melakukan paling banyak satu pemanggilan trailing. Di dalam komponen, saya membatalkannya saat pembersihan. Untuk pencarian, saya membatalkan atau memberi versi pada permintaan secara terpisah karena debounce tidak dapat mencegah respons lama tiba terlambat. Saya memverifikasi semua ini dengan waktu tiruan (fake time) dan urutan pemanggilan yang tepat, karena timer peramban dapat berjalan terlambat.”
Kesalahan Umum
- Menulis kode sebelum mendefinisikan perilaku edge → implementasi berbeda yang sama-sama masuk akal akan gagal dalam pengujian yang berbeda → tulis nilai default dan urutan pemanggilan ber-timestamp terlebih dahulu.
- Menyimpan argumen pertama → callback yang tertunda bertindak berdasarkan input yang usang → ganti argumen dan receiver yang tertunda pada setiap pemanggilan.
- Selalu memicu pemanggilan trailing setelah pemanggilan leading → satu klik menyebabkan dua tindakan → lakukan trailing hanya jika ada pemanggilan lain yang menjadi tertunda selama interval tersebut.
- Mereset selamanya tanpa
maxWait→ aliran terus-menerus dapat membuat autosave atau batching mengalami starvation → tambahkan batas waktu maksimum ketika progres berkala diperlukan. - Menganggap penundaan timer tepat secara presisi → long tasks dan throttling runtime merusak asersi timestamp → perlakukan ini sebagai kelayakan paling awal dan uji dengan waktu virtual.
- Menggunakan
requestAnimationFrame()sebagai throttle generik → ini dapat berjalan pada irama yang sama dengan event scroll → gunakan ini untuk penyelarasan paint dan ukur interval terpisah saat pengurangan laju diperlukan. - Melupakan pembersihan (cleanup) → pekerjaan yang tertunda berjalan setelah navigasi atau unmount → batalkan selama pembersihan siklus hidup atau lakukan flush secara eksplisit saat semantik produk memerlukannya.
- Mengasumsikan debounce mencegah hasil pencarian yang usang → permintaan yang sudah dimulai dapat selesai di luar urutan → gabungkan dengan pembatalan atau validasi permintaan terbaru.
- Membangun seluruh state machine untuk prompt dasar → kode yang tidak perlu meningkatkan kemungkinan bug → implementasikan kontrak terkecil yang diminta, lalu jelaskan ekstensinya.
Pertanyaan Lanjutan dan Jawaban
Lanjutan 1: Apa yang terjadi ketika pemanggilan tiba tepat pada batas waktu tunggu?
Definisikan model pengurutan. Dalam pengujian deterministik, jika tugas timer lama dieksekusi sebelum tugas pemanggilan baru pada timestamp virtual yang sama, burst lama dapat menjalankan trailing dan pemanggilan baru memulai burst lain. Jika pemanggilan baru diproses terlebih dahulu, ia dapat memperbarui state tertunda sebelum timer memeriksa ulang. Antrean tugas peramban tidak membuat event eksternal simultan menjadi atomik secara ajaib. Pengujian harus menjadwalkan urutan tertentu dan implementasinya harus konsisten secara internal.
Lanjutan 2: Mengapa tidak mengimplementasikan throttle dengan setInterval?
Interval berdetak bahkan ketika tidak ada pekerjaan yang tertunda kecuali ada state tambahan yang menekannya. Ini juga membuat penalaran tentang leading, nilai trailing akhir, pembatalan, dan memulai ulang setelah idle menjadi lebih sulit. Timer sekali jalan (one-shot) yang dijadwalkan dari permintaan nyata memberikan kontrol eksplisit atas batas waktu berikutnya. setInterval dapat bekerja dengan kontrak yang presisi, tetapi tidak lebih sederhana setelah semua semantik yang diperlukan disertakan.
Lanjutan 3: Haruskah flush berjalan ketika trailing dinonaktifkan?
Tidak ada pekerjaan trailing tertunda yang memenuhi syarat, sehingga flush() mengembalikan hasil eksekusi terbaru tanpa memanggil fn. Ini mengikuti gerbang trailing yang sama seperti kedaluwarsa timer. Jika suatu produk membutuhkan “paksa eksekusi terlepas dari opsi,” itu adalah API yang berbeda dan harus menerima nama serta pengujian yang berbeda.
Lanjutan 4: Bagaimana Anda menguji pemutaran mundur jam (clock rollback)?
Injeksi jam dan ubah nilainya menjadi di bawah lastCallTime. Percabangan sinceCall < 0 memperlakukan state tersebut sebagai memenuhi syarat daripada menjadwalkan sisa waktu yang sangat besar atau negatif. Jam monotonik (monotonic clock) lebih disukai jika tersedia, tetapi kontrak utilitas peramban sering kali menggunakan jam runtime secara tidak langsung; percabangan defensif mencegah wrapper macet.
Lanjutan 5: Kapan proyek harus menggunakan Lodash daripada implementasi ini?
Gunakan pustaka yang terawat ketika semantik yang didokumentasikan, strategi bundling, dan kebijakan dependensi proyek sesuai. Utilitas buatan sendiri membutuhkan pengujian kompatibilitas, peninjauan, dan kepemilikan sendiri. Mengimplementasikannya dalam wawancara mendemonstrasikan kemampuan penalaran; ini tidak membuktikan bahwa menduplikasi dependensi yang matang adalah keputusan produksi terbaik.