Masalah dan Skop
Laksanakan debounce(fn, wait, options) dan throttle(fn, wait, options) untuk kod berorientasikan pelayar. Fungsi yang dikembalikan mesti mengekalkan argumen terkini dan this, mengembalikan hasil panggilan terkini, serta mendedahkan cancel() dan flush(). Pilihannya ialah leading, trailing, dan, untuk debounce, maxWait.
Secara lalai, debounce hanya dilaksanakan pada bahagian trailing (akhir). Panggilan leading dijalankan pada permulaan lonjakan (burst). Jika kedua-dua leading dan trailing diaktifkan, satu panggilan terpencil berjalan sekali pada bahagian leading; panggilan kedua dalam tempoh menunggu mencipta panggilan trailing dengan argumen terkini. maxWait menghalang aliran peristiwa yang berterusan daripada menangguhkan kerja selama-lamanya. Throttle membenarkan paling banyak satu panggilan bagi setiap selang bersaiz wait dan menyokong tingkah laku leading serta trailing.
Ini merupakan soalan frontend kerana kontraknya dipacu oleh input pelayar, scroll, resize, rendering, dan kitaran hayat komponen. Utiliti yang sama boleh dijalankan dalam Node.js, tetapi menukar persekitaran masa jalan (runtime) tidak mengubah kemahiran teras temu duga: menterjemah semantik pemasaan UI kepada mesin keadaan (state machine) yang kecil. Pelaksanaan ini menggunakan JavaScript dan keadaan tambahan O(1).
Perkara yang Dinilai oleh Penemu Duga
Isyarat pertama ialah sama ada calon mentakrifkan kontrak sebelum menulis pemasa. “Debounce menunggu; throttle mengehadkan” tidak menyelesaikan tingkah laku leading, sama ada panggilan leading tunggal juga mencetuskan trailing, argumen mana yang menang, atau apa yang dimaksudkan dengan pembersihan (cleanup). Jawapan yang kukuh menulis garis masa panggilan dan menyatakan pilihan tersebut dengan jelas.
Isyarat kedua ialah penaakulan keadaan (state reasoning). Satu pemboleh ubah pemasa tidak mencukupi apabila maxWait, pergerakan jam, nilai pulangan, dan flush() muncul. Pelaksanaan memerlukan masa panggilan terakhir, masa pelaksanaan sebenar terakhir, argumen dan penerima yang tergantung, pemegang pemasa, serta hasil terkini. Setiap bahagian harus sepadan dengan peraturan kontrak.
Isyarat ketiga ialah pertimbangan pelayar (browser judgment). Pemasa menyediakan kelewatan minimum, bukan had masa yang tepat; tugas panjang (long tasks) dan pendikit latar belakang (background throttling) boleh menjalankan panggilan balik lewat. Men-throttle kerja scroll dengan requestAnimationFrame() menyelaraskan kerja dengan proses paint tetapi tidak dengan sendirinya mengurangkan kadar peristiwa. Kadangkala scrollend, IntersectionObserver, atau cangkuk pembersihan rangka kerja menghapuskan keperluan untuk utiliti pemasa generik.
Akhir sekali, penemu duga menyemak ujian dan bukannya intuisi visual. Jeda jam dinding (wall-clock sleep) menjadikan ujian perlahan dan tidak stabil (flaky). Calon yang kukuh menyuntik atau menggantikan masa dan pemasa, memajukan jam palsu (fake clock), dan mengesahkan urutan panggilan yang tepat untuk sempadan, pembatalan, flushing, dan input berterusan.
Soalan untuk Dijelaskan Sebelum Menjawab
- Apakah nilai lalai? Jawapan ini menggunakan debounce
{ leading: false, trailing: true }dan throttle{ leading: true, trailing: true }. Nilai lalai yang berbeza mengubah tingkah laku panggilan terpencil dan ujian. - Apakah yang berlaku apabila kedua-dua bahagian diaktifkan? Panggilan tunggal hanya dipanggil pada bahagian leading. Panggilan trailing berlaku hanya apabila panggilan lain tiba dalam tempoh menunggu. Ini mengelakkan penduaan tindakan yang terpencil.
- Argumen dan penerima mana yang digunakan oleh panggilan tertunda? Ia menggunakan argumen dan
thisdaripada panggilan tertunda terkini. Menangkap peristiwa pertama akan menyebabkan kerja autolengkap atau saiz semula menjadi lapuk (stale). - Bolehkah input berterusan selama-lamanya? Jika ya, debounce trailing sahaja mungkin tidak akan dijalankan.
maxWaitmenetapkan kelewatan maksimum daripada panggilan sebenar sebelumnya atau permulaan lonjakan semasa. - Apakah yang patut dilakukan oleh
cancel()danflush()? Cancel mengalih keluar kerja yang tergantung dan menetapkan semula keadaan lonjakan. Flush serta-merta melaksanakan panggilan trailing yang layak dan mengembalikan hasilnya; ia tidak boleh menyebabkan penduaan kemudian. - Adakah masa berlalu yang tepat diperlukan? Tiada pemasa pelayar yang menjamin penjadualan yang tepat. Kontrak mengawal kelayakan terawal dan susunan; ujian menggunakan jam palsu untuk menghapuskan hingar penjadual.
Jawapan 30 Saat
“Saya akan terlebih dahulu mentakrifkan semantik leading dan trailing dengan garis masa. Debounce mengumpulkan lonjakan dan biasanya dipanggil selepas wait milisaat tanpa panggilan lain. Throttle menjamin kemajuan berkala semasa lonjakan berlaku. Saya mengekalkan argumen dan penerima terkini, masa panggilan terakhir, masa pelaksanaan sebenar terakhir, satu pemasa, dan hasil terkini.
Pada setiap panggilan, saya memutuskan sama ada kerja layak dilaksanakan sekarang. Jika tidak, saya menjadualkan baki masa menunggu. Pemasa menyemak semula kelayakan kerana panggilan yang lebih lewat mungkin telah menganjakkan tarikh akhir trailing. maxWait menghalang kebuluran (starvation); throttle ialah mesin keadaan yang sama dengan maxWait yang ditetapkan kepada wait. cancel() membersihkan keadaan tertunda, dan flush() melaksanakan satu panggilan trailing yang tertunda. Saya menguji dengan jam palsu, terutamanya panggilan tepat pada sempadan, leading campur trailing, input berterusan, pembatalan, flushing, dan pembersihan unmount.”
Penjelasan Mendalam Langkah demi Langkah
Langkah 1: Tukar perkataan kepada garis masa
Andaikan wait = 100 ms dan panggilan A@0, B@40, C@90, dan D@220. Debounce trailing sahaja menghasilkan C@190 dan D@320: setiap panggilan menganjakkan tarikh akhir tempoh tenang, dan argumen terkini yang menang. Debounce leading-dan-trailing menghasilkan A@0, C@190, dan D@220. D adalah terpencil, jadi ia tidak berjalan lagi pada 320.
Throttle leading-dan-trailing menghasilkan paling banyak satu panggilan bagi setiap tetingkap 100 ms sambil mengekalkan nilai tertunda yang terakhir. Bagi lonjakan pertama, ini bermakna A serta-merta dan nilai tertunda terkini, C, pada sempadan trailing. Cap masa yang tepat dalam pelayar sebenar mungkin lebih lewat daripada sempadan konsep, tetapi tidak pernah lebih awal.
Garis masa ini mendedahkan peralihan keadaan: panggilan melahu (idle) boleh memulakan lonjakan; panggilan kemudian menggantikan data tertunda; pemasa sama ada melaksanakan atau menjadualkan semula; pelaksanaan membersihkan data tertunda tetapi mengekalkan hasilnya. Menulis peralihan ini terlebih dahulu menghalang kebanyakan pepijat off-by-one dan penduaan trailing.
Langkah 2: Laksanakan satu mesin keadaan yang eksplisit
Pelaksanaan di bawah mengikut kontrak yang dinyatakan. shouldInvoke() mengendalikan panggilan pertama, sempadan tempoh tenang, jam yang bergerak ke belakang, dan maxWait. remainingWait() memilih yang lebih awal antara tarikh akhir trailing dan masa menunggu 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,
});
}Keadaannya ialah O(1), dan setiap panggilan pembungkus (wrapper) melakukan kerja O(1). Kos panggilan balik berada di luar kerumitan utiliti ini. Kod pengeluaran mungkin mengimport pelaksanaan yang diselenggara sebaliknya; nilai temu duga adalah keupayaan untuk menerangkan dan menguji kontraknya.
Langkah 3: Terangkan mengapa pemasa menyemak semula
Katakan panggilan pertama menjadualkan pemasa untuk 100 ms, kemudian panggilan kedua tiba pada 90 ms. Jika pemasa asal terus memanggil pada 100 secara membabi buta, tempoh tenang hanyalah 10 ms. Sebaliknya, timerExpired() mengira semula sinceCall, mendapati bahawa 100 ms tempoh senyap belum berlalu, dan menjadualkan baki 90 ms.
Mengosongkan dan mencipta pemasa pada setiap peristiwa ialah pelaksanaan trailing sahaja yang lebih mudah dan sah. Mesin keadaan yang menyemak semula berbaloi dengan kerumitannya kerana ia juga menyokong panggilan leading, maxWait, nilai pulangan, dan semantik throttle. Jika soalan hanya meminta debounce trailing asas, gunakan penyelesaian yang lebih kecil dan nyatakan bahawa kontrak yang lebih kaya memerlukan lebih banyak keadaan.
Langkah 4: Cegah starvation dengan maxWait
Pertanyaan autolengkap biasanya perlu menunggu jeda. Penimbal telemetri atau autosimpan tidak boleh menunggu selama-lamanya semasa input berterusan. Dengan wait = 300 ms dan maxWait = 1000 ms, panggilan berulang setiap 100 ms masih mencetuskan sekurang-kurangnya sekali di sekitar setiap sempadan maksimum 1000 ms, tertakluk kepada kelewatan penjadualan masa jalan.
maxWait juga merupakan jambatan kepada throttle. Menetapkannya sama dengan wait bermakna panggilan berterusan tidak boleh menangguhkan pelaksanaan melangkaui satu selang. Mengekalkan satu pelaksanaan mengelakkan dua mesin keadaan pemasa daripada menyimpang dalam tingkah laku sempadan. Penerbitan ini ialah pilihan pelaksanaan, bukan satu-satunya definisi throttle yang sah; kontrak dan ujian kekal berautoriti.
Langkah 5: Kendalikan kitaran hayat dan kesan sampingan
Kerja yang tertunda boleh kekal lebih lama daripada UI yang menjadualkannya. Komponen mesti memanggil cancel() semasa pembersihan supaya panggilan balik lama tidak mengemas kini keadaan yang telah dinyahlekap (unmounted), menggunakan props lapuk, atau menghantar permintaan selepas navigasi. Jika produk memerlukan komitmen teks yang tergantung sebelum penamatan (teardown), panggil flush() secara sengaja dan kemudian bersihkan; jangan jadikan setiap unmount menyerahkan data secara senyap.
Men-debounce carian tak segerak mengawal penciptaan permintaan, bukan susunan respons. Sebaik sahaja permintaan bermula, respons lama yang lebih perlahan masih boleh menimpa hasil yang lebih baharu. Gunakan AbortController, penjanaan permintaan, atau semakan respons terkini sebagai tambahan kepada debounce. Pengehadan kadar dan perlindungan respons lapuk menyelesaikan mod kegagalan yang berbeza.
Langkah 6: Pilih primitif pelayar mengikut tugasan sebenar
Gunakan debounce apabila hanya nilai akhir yang penting, seperti pengesahan selepas penaipan berhenti. Gunakan throttle apabila kemajuan perantaraan penting, seperti persampelan penunjuk atau keadaan scroll secara berkala. Gunakan requestAnimationFrame() untuk menyelaraskan penulisan visual dengan paint, tetapi jangan mendakwa ia mengurangkan kekerapan peristiwa scroll secara automatik. Gunakan IntersectionObserver untuk keterlihatan berasaskan ambang dan scrollend apabila peristiwa yang diperlukan secara khusus ialah penghujung penatalan.
Peraturan keputusan adalah berdasarkan tingkah laku: buang keadaan perantaraan, kekalkan kemajuan berkala, selaraskan kerja dengan paint, atau perhatikan ambang yang ditentukan pelayar. Memilih primitif mengikut tabiat boleh membazirkan kerja atau menyembunyikan kemas kini yang boleh dilihat oleh pengguna.
Langkah 7: Uji dengan masa maya
Gantikan Date.now, setTimeout, dan clearTimeout dengan jam palsu, atau gunakan pemasa palsu bagi pelari ujian. Rekod nilai dan cap masa maya. Majukan masa sehingga sebelum dan tepat pada setiap tarikh akhir. Jangan membuat penegasan bahawa tamat masa 100 ms sebenar dicetuskan tepat pada 100 ms.
Matriks minimum merangkumi lonjakan trailing sahaja, lonjakan leading sahaja, leading campur trailing dengan satu dan pelbagai panggilan, maxWait di bawah input berterusan, panggilan tepat pada wait, argumen dan penerima terkini, penggunaan semula hasil, pembatalan sebelum tarikh akhir, flush sebelum tarikh akhir, flush berulang, sifar tunggu, dan panggilan balik yang menjadualkan panggilan pembungkus yang lain. Untuk penyepaduan UI, uji juga pembersihan dan respons rangkaian yang lapuk.
Contoh Jawapan yang Kukuh
“Saya akan mentakrifkan garis masa sebelum mengekod. Dengan debounce trailing 100 ms, panggilan pada 0, 40, dan 90 menghasilkan satu panggilan pada masa terawal yang layak selepas 190 dengan argumen terakhir. Versi leading-dan-trailing memanggil pada 0 dan 190, tetapi satu panggilan leading yang terpencil tidak menduplikasi pada bahagian trailing.
Keadaan saya ialah satu pemasa, argumen dan penerima tertunda, masa panggilan terakhir, masa pelaksanaan sebenar terakhir, dan hasil terkini. Pemasa tidak pernah menganggap ia masih layak; ia menyemak semula kerana panggilan yang lebih baharu boleh menganjakkan tarikh akhir tenang. maxWait menambah tarikh akhir kedua supaya input berterusan tidak mengakibatkan panggilan balik mengalami kebuluran (starvation). Throttle menggunakan semula mesin keadaan yang sama dengan maxWait sama dengan wait.
Saya mengekalkan this, mengembalikan hasil panggilan terakhir, membersihkan keadaan tertunda dalam cancel(), dan memastikan flush() melaksanakan paling banyak satu panggilan trailing. Dalam komponen, saya membatalkannya semasa pembersihan. Untuk carian, saya membatalkan atau meletakkan versi pada permintaan secara berasingan kerana debounce tidak dapat menghalang respons lama daripada tiba lewat. Saya mengesahkan semua ini dengan masa palsu dan urutan panggilan yang tepat, memandangkan pemasa pelayar boleh berjalan lewat.”
Kesilapan Biasa
- Mengekod sebelum mentakrifkan tingkah laku bahagian (edge behavior) → pelaksanaan berbeza yang munasabah gagal dalam ujian berbeza → tulis nilai lalai dan urutan panggilan dengan cap masa terlebih dahulu.
- Mengekalkan argumen pertama → panggilan balik tertunda bertindak ke atas input yang lapuk → gantikan argumen dan penerima tertunda pada setiap panggilan.
- Sentiasa mencetuskan panggilan trailing selepas panggilan leading → satu klik menyebabkan dua tindakan → lakukan trailing hanya apabila panggilan lain menjadi tertunda semasa selang tersebut.
- Menetapkan semula selama-lamanya tanpa
maxWait→ aliran berterusan boleh menyebabkan autosimpan atau pengelompokan (batching) mengalami kebuluran → tambah tarikh akhir maksimum apabila kemajuan berkala diperlukan. - Menganggap kelewatan pemasa adalah tepat → tugas panjang dan pendikit masa jalan merosakkan penegasan cap masa → anggap ia sebagai kelayakan terawal dan uji dengan masa maya.
- Menggunakan
requestAnimationFrame()sebagai throttle generik → ia boleh berjalan pada irama yang sama seperti peristiwa scroll → gunakannya untuk penyelarasan paint dan ukur selang berasingan apabila pengurangan kadar diperlukan. - Melupakan pembersihan → kerja yang tertunda berjalan selepas navigasi atau unmount → batalkan semasa pembersihan kitaran hayat atau lakukan flush secara eksplisit apabila semantik produk memerlukannya.
- Menganggap debounce menghalang hasil carian lapuk → permintaan yang telah dimulakan boleh selesai tanpa mengikut urutan → gabungkannya dengan pembatalan atau pengesahan permintaan terkini.
- Membina keseluruhan mesin keadaan untuk gesaan asas → kod yang tidak perlu meningkatkan permukaan pepijat → laksanakan kontrak terkecil yang dinyatakan, kemudian terangkan lanjutan.
Soalan Susulan dan Jawapan
Susulan 1: Apakah yang berlaku apabila panggilan tiba tepat pada sempadan menunggu?
Takrifkan model susunan. Dalam ujian deterministik, jika tugas pemasa lama dilaksanakan sebelum tugas panggilan baharu pada cap masa maya yang sama, lonjakan lama boleh melakukan trailing dan panggilan baharu memulakan lonjakan lain. Jika panggilan baharu diproses terlebih dahulu, ia boleh mengemas kini keadaan tertunda sebelum pemasa menyemak semula. Giliran tugas pelayar tidak menjadikan peristiwa luaran yang serentak bersifat atom secara ajaib. Ujian mesti menjadualkan susunan tertentu dan pelaksanaan mestilah konsisten secara dalaman.
Susulan 2: Mengapa tidak melaksanakan throttle dengan setInterval?
Selang berdetik walaupun tiada kerja yang tertunda melainkan keadaan tambahan menyekatnya. Ia juga menjadikan leading, nilai trailing akhir, pembatalan, dan permulaan semula selepas melahu lebih sukar untuk dipertimbangkan. Pemasa satu kali (one-shot) yang dijadualkan daripada permintaan sebenar memberikan kawalan eksplisit ke atas sempadan seterusnya. setInterval boleh berfungsi dengan kontrak yang tepat, tetapi ia tidak lebih mudah apabila semua semantik yang diperlukan disertakan.
Susulan 3: Patutkah flush berjalan apabila trailing dinyahdayakan?
Tiada kerja trailing tertunda yang layak, jadi flush() mengembalikan hasil pelaksanaan terkini tanpa memanggil fn. Ini mengikut get trailing yang sama seperti tamat tempoh pemasa. Jika sesuatu produk memerlukan "paksa pelaksanaan tanpa mengira pilihan," itu ialah API yang berbeza dan harus menerima nama serta ujian yang berbeza.
Susulan 4: Bagaimanakah anda menguji pengunduran jam (clock rollback)?
Suntik jam dan ubah nilainya menjadi di bawah lastCallTime. Cabang sinceCall < 0 menganggap keadaan itu sebagai layak dan bukannya menjadualkan baki masa yang sangat besar atau negatif. Jam monotonik (monotonic clock) adalah lebih baik jika tersedia, tetapi kontrak utiliti pelayar sering menggunakan jam masa jalan secara tidak langsung; cabang pertahanan menghalang pembungkus daripada tersekat.
Susulan 5: Bilakah projek patut menggunakan Lodash dan bukannya pelaksanaan ini?
Gunakan pustaka yang diselenggara apabila semantik yang didokumenkan, strategi penggabungan (bundle), dan dasar kebergantungan projek adalah sesuai. Utiliti binaan sendiri memerlukan ujian keserasian, semakan, dan pemilikan sendiri. Melaksanakannya dalam temu duga menunjukkan kemahiran penaakulan; ia tidak membuktikan bahawa menduplikasi kebergantungan yang matang adalah keputusan pengeluaran yang terbaik.