Prompt dan Konteks yang Berlaku
Daftar tugas dapat menampilkan ribuan baris. Hasil paginasi, pembaruan langsung (live updates), dan tindakan pengguna menyisipkan atau menghapus baris secara dinamis. Setiap baris memiliki tombol edit, hapus, dan pengalih status, serta tombol dapat berisi elemen ikon atau teks. Daftar tersebut mungkin juga berisi daftar subtugas bertingkat yang dimiliki secara independen. Daftarkan hanya satu listener click pada daftar luar dan kirimkan (dispatch) tindakan menggunakan data-action milik kontrol dan data-task-id milik barisnya.
Jawaban harus menjelaskan bagaimana sebuah event berjalan dari leluhur ke targetnya dan kembali menuju leluhur, membedakan event.target dari event.currentTarget, dan mencegah daftar luar menangani tindakan yang dimiliki oleh daftar bertingkat. Jawaban juga harus mencakup event yang tidak dapat menggunakan delegasi bubbling biasa, efek dari keturunan yang menghentikan propagasi, bagaimana Shadow DOM mengubah jalur yang terlihat, dan bagaimana listener dihapus saat komponen di-unmount.
Pertanyaan ini cocok untuk wawancara frontend tingkat menengah, platform Web, dan full-stack. Keterampilan intinya adalah pengiriman event DOM browser dan batas kepemilikan UI, jadi ini termasuk dalam frontend. Pertanyaan ini tidak menguji wadah publish-subscribe userland atau penjadwalan tugas, mikrotugas, dan rendering.
Apa yang Dievaluasi oleh Pewawancara
Jawaban yang kuat dimulai dengan model yang akurat. Pengiriman mengikuti jalur event melalui fase capture, fase target, dan, saat event dapat melakukan bubble, fase bubble. addEventListener mendaftarkan listener bubbling secara default; capture: true membuatnya berjalan saat event bergerak menuju target. Hanya mengatakan bahwa “event menggelembung ke atas (bubble up)” tidak dapat menjelaskan listener capture, event non-bubbling, atau batas Shadow DOM.
Kualitas implementasi terlihat dalam logika perutean. target mengidentifikasi target pengiriman event; currentTarget mengidentifikasi objek yang listener-nya sedang berjalan. Dalam handler daftar induk, yang pertama mungkin berupa ikon di dalam tombol sedangkan yang terakhir adalah daftarnya. Perbandingan nama tag (tag-name) akan melewatkan markup bertingkat. Jawaban yang kuat dimulai dari target, menggunakan closest() untuk menemukan kontrol tindakan, dan kemudian membuktikan bahwa kontrol tersebut milik akar (root) delegasi yang aktif.
Batas kepemilikan memisahkan jawaban dasar dari jawaban yang siap untuk produksi. contains() dapat menolak kecocokan di luar kontainer, tetapi kontainer itu sendiri mungkin berisi root delegasi independen lainnya. Mewajibkan root delegasi terdekat agar sama dengan daftar saat ini mengekspresikan kepemilikan dan mencegah daftar luar dan daftar bertingkat mengeksekusi satu tindakan secara bersamaan. Nama tindakan juga harus melewati daftar izin (allowlist) alih-alih menjadi nama fungsi arbitrer dari string DOM.
Rencana verifikasi harus menargetkan kegagalan yang dapat diamati: markup bertingkat, baris yang ditambahkan secara dinamis, tombol dinonaktifkan (disabled), root bertingkat, propagasi yang dihentikan, event fokus, Shadow DOM, panggilan balik asinkron, dan pembersihan saat unmount. Delegasi mengurangi jumlah listener dan secara alami mencakup keturunan dinamis, tetapi antarmuka kecil yang stabil mungkin lebih jelas dengan listener langsung. Delegasi adalah pilihan desain, bukan optimalisasi wajib.
Pertanyaan yang Perlu Diklarifikasi Sebelum Menjawab
- Event mana yang didelegasikan?
clickbiasanya melakukan bubble dan cocok dengan prompt ini. Mendelegasikanfocus,
blur, mouseenter, atau mouseleave membutuhkan capture, alternatif bubbling, atau listener langsung; pilihan tersebut mengubah implementasi dan semantik.
- Bisakah daftar berisi komponen bertingkat atau Shadow DOM?
contains()sering kali cukup untuk
daftar datar. Kepemilikan bertingkat membutuhkan penanda root terpisah, sementara Shadow DOM membutuhkan kontrak event komponen publik.
- Apakah markup tombol stabil? Jika target bisa berupa ikon atau node teks, handler membutuhkan
closest(). matches() hanya cukup jika target dijamin merupakan elemen tindakan.
- Siapa yang boleh memanggil
stopPropagation()? Jika komponen anak diizinkan untuk menghentikan propagasi,
listener bubble leluhur tidak dapat menjamin untuk melihat event tersebut. Klarifikasi kepemilikan alih-alih diam-diam memindahkan tindakan bisnis ke fase capture.
- Bagaimana status disabled direpresentasikan? Tombol native menggunakan
disabled. Kontrol kustom juga membutuhkan
perilaku aksesibel dan keyboard; kelas CSS saja bukan kontrak yang andal untuk handler.
- Siapa yang memiliki masa pakai (lifetime) listener? Root tingkat halaman mungkin bersifat permanen. Komponen yang dapat di-mount harus mempertahankan
referensi fungsi yang dapat dilepas atau menggunakan AbortSignal, jika tidak, mounting ulang akan memproses satu klik beberapa kali.
- Apakah suatu tindakan perlu membatalkan perilaku default? Panggil
preventDefault()hanya jika navigasi,
pengiriman formulir, atau tindakan default lainnya bertentangan dengan tujuan produk. Pembatalan default dan kontrol propagasi adalah keputusan independen.
Kerangka Jawaban 30 Detik
“Sebuah event di-capture di sepanjang jalurnya, ditangani pada target, dan menggelembung kembali (bubble) melalui leluhur ketika bubbles bernilai true. Listener default induk berjalan di fase bubble, sehingga satu listener dapat mencakup keturunan dinamis. Di dalam handler, saya pertama-tama memverifikasi bahwa target adalah sebuah Element, lalu memanggil closest('[data-action]'). Saya memeriksa contains dan mewajibkan root delegasi terdekat adalah daftar saat ini sehingga daftar bertingkat tidak membocorkan tindakan ke luar. currentTarget adalah daftar dengan listener dan hanya andal saat handler berjalan. Saya mengirimkan tindakan melalui allowlist tindakan dan membersihkannya dengan AbortController. Untuk event non-bubbling, stopPropagation, dan Shadow DOM, saya memilih capture, alternatif bubbling, listener langsung, atau custom event tingkat komponen sesuai batasannya, kemudian menguji target bertingkat, baris dinamis, dan unmount/remount.”
Pembahasan Mendalam Langkah demi Langkah
Mulailah dengan jalur event. Misalkan sebuah klik dimulai pada ikon di dalam tombol. Jalur tersebut mencakup document, daftar luar, baris tugas, tombol, dan ikon. Listener capture berjalan dari bagian luar jalur menuju target. Listener target yang berlaku berjalan saat event mencapai target. Jika bubbles bernilai true, listener bubble leluhur kemudian berjalan dari dalam ke luar. Algoritma pengiriman membangun jalur ini, dan baik struktur DOM maupun batas Shadow DOM memengaruhinya.
target dan currentTarget menjawab pertanyaan yang berbeda. target menanyakan di mana interaksi ini dikirimkan dan biasanya tetap stabil selama bubbling DOM biasa. currentTarget menanyakan objek mana yang listener-nya sedang berjalan sekarang; nilainya berubah antar handler dan menjadi null setelah handler kembali (returns). Simpan referensi atau data daftar yang diperlukan secara sinkron. Jangan mengandalkan event.currentTarget setelah sebuah await.
Implementasi berikut memisahkan pencocokan, kepemilikan, status, dan pengiriman tindakan. Asumsikan root daftar memiliki data-delegation-root, setiap baris memiliki data-task-id, dan tindakan menggunakan tombol native dengan data-action. openEditor, deleteTask, dan toggleTask mewakili fungsi produk yang ada:
const list = document.querySelector("#task-list");
if (!(list instanceof HTMLElement)) {
throw new Error("task list was not found");
}
const handlers = new Map([
["edit", (taskId) => openEditor(taskId)],
["delete", (taskId) => deleteTask(taskId)],
["toggle", (taskId) => toggleTask(taskId)],
]);
const controller = new AbortController();
list.addEventListener(
"click",
(event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const action = target.closest("[data-action]");
if (!(action instanceof HTMLButtonElement)) return;
if (!list.contains(action)) return;
if (action.closest("[data-delegation-root]") !== list) return;
if (action.disabled) return;
const row = action.closest("[data-task-id]");
if (!(row instanceof HTMLElement) || !list.contains(row)) return;
const actionName = action.dataset.action;
const taskId = row.dataset.taskId;
if (!actionName || !taskId) return;
const handler = handlers.get(actionName);
if (!handler) return;
handler(taskId);
},
{ signal: controller.signal },
);
function cleanupTaskListDelegation() {
controller.abort();
}target.closest() menelusuri dari ikon aktual ke tombolnya, sehingga perubahan markup internal tidak merusak perutean. list.contains(action) membuktikan bahwa kecocokan masih berada di dalam daftar. Pemeriksaan root terdekat kemudian menolak tombol yang dimiliki oleh daftar bertingkat. Map hanya mengizinkan tiga tindakan yang diketahui; tindakan data-action yang tidak dikenal akan keluar dengan aman. Pemeriksaan disabled eksplisit mendokumentasikan kontrak handler, sehingga tindakan yang dinonaktifkan tetap ditolak jika kode di kemudian hari menggunakan dispatch terprogram atau mengubah markup-nya.
Kontrol propagasi memerlukan penjelasan terpisah. stopPropagation() menghentikan perjalanan lebih lanjut melalui jalur capture atau bubble. Ini tidak menghentikan tindakan default atau listener lain pada elemen yang sama; yang terakhir memerlukan stopImmediatePropagation(). preventDefault() membatalkan tindakan default dari event yang dapat dibatalkan tetapi tidak menghentikan propagasi. Memperlakukan ketiga API ini sebagai hal yang setara menyebabkan tautan tetap bernavigasi, leluhur kehilangan event, atau handler node yang sama terus berlanjut secara tidak terduga.
Pilih strategi untuk setiap event non-bubbling. Leluhur dapat mengamati focus dan blur selama capture, atau menggunakan event bubbling focusin dan focusout. mouseenter dan mouseleave dapat diikat secara langsung. Menggantinya dengan event bubbling mouseover dan mouseout juga membutuhkan pemfilteran relatedTarget karena perpindahan antar keturunan menciptakan event tambahan. Elemen yang dapat di-scroll sering kali paling jelas dengan listener langsung. Nama event yang serupa tidak menjamin semantik yang identik.
Shadow DOM adalah batas komponen; delegasi tingkat dokumen tidak boleh bergantung pada selektor di dalam komponen. Event UI user-agent seperti click umumnya tersusun (composed) melintasi batas shadow, tetapi listener luar dapat melihat host yang ditargetkan ulang (retargeted) sebagai target. composedPath() dapat mengekspos jalur melalui open shadow root tetapi tidak mengekspos node di dalam closed root. Web Component yang perlu mempublikasikan tindakan harus menangani bagian internalnya sendiri dan memancarkan event komponen terdokumentasi yang diizinkan untuk melakukan bubble dan melintasi batas tersebut. Konsumen kemudian bergantung pada kontrak publik yang stabil.
Verifikasi perilaku daripada hanya menghitung listener. Catat urutan capture, target, dan bubble; klik teks tombol dan ikonnya; tambahkan baris dan klik tanpa mengikat ulang; hapus baris dan pastikan node lamanya tidak lagi bertindak; klik daftar bertingkat dan pastikan hanya pemiliknya yang menanganinya; uji tindakan yang dinonaktifkan dan tidak dikenal; biarkan elemen anak menghentikan propagasi dan amati event leluhur yang hilang; cakup strategi fokus dan Shadow DOM; panggil cleanupTaskListDelegation() dan buktikan klik berhenti, lalu mount ulang sekali dan buktikan satu klik ditangani tepat satu kali.
Contoh Jawaban Berkualitas Tinggi
“Saya pertama-tama akan mengonfirmasi bahwa kita mendelegasikan event click yang melakukan bubble. Browser meng-capture di sepanjang jalur event ke target, menangani target, dan kemudian kembali melalui leluhur ketika bubbling diizinkan. Listener default induk berjalan selama bubbling. event.target adalah target pengiriman dan bisa berupa ikon di dalam tombol; event.currentTarget adalah daftar dengan handler ini dan hanya valid saat handler berjalan.
Saya mulai dari target dengan closest('[data-action]'), lalu memverifikasi bahwa hasilnya adalah tombol di dalam daftar saat ini. Karena daftar ini mungkin berisi komponen didelegasikan lainnya, saya juga mewajibkan data-delegation-root terdekat dari tindakan tersebut adalah daftar ini. Saya menemukan baris data-task-id-nya dan menggunakan allowlist Map untuk mengirimkan edit, delete, atau toggle ke fungsi yang ada. Baris yang baru disisipkan secara alami dicakup oleh listener leluhur yang sama, dan AbortController menghapusnya saat unmount.
Saya tidak akan memaksakan setiap event ke dalam delegasi bubble. Focus dapat menggunakan capture atau focusin, dan mouseenter tidak memiliki semantik yang sama dengan mouseover. Anak yang memanggil stopPropagation mencegah handler bubble leluhur melihat event tersebut. Di seluruh Shadow DOM, target mungkin mengalami retargeting dan closed root tidak mengekspos jalur internalnya, jadi sebuah komponen harus memublikasikan event eksternal yang stabil alih-alih membuat halaman bergantung pada selektor internal.
Pengujian saya mencakup ikon internal, baris dinamis, root bertingkat, tindakan yang dinonaktifkan dan tidak dikenal, penghentian propagasi, Shadow DOM, dan unmount/remount. Jika hanya ada beberapa tombol stabil dengan perilaku yang tidak terkait, saya menggunakan listener langsung. Nilai utama delegasi di sini adalah batas kepemilikan yang jelas, dukungan keturunan dinamis, dan satu siklus hidup—bukan klaim kinerja tanpa konteks.”
Kesalahan Umum
- Hanya memeriksa
event.target.matches('button')→ klik ikon menjadikan ikon sebagai target →
telusuri ke elemen tindakan dengan closest().
- Mengeksekusi kecocokan
closest()pertama → mungkin milik kontainer lain atau root bertingkat →
periksa contains() dan root kepemilikan terdekat.
- Memperlakukan
currentTargetsebagai elemen yang diklik → dalam delegasi induk itu adalah daftar dengan
handler yang sedang berjalan → gunakan target untuk sumber dan currentTarget untuk batas penanganan.
- Membaca
event.currentTargetsetelahawait→ handler telah kembali dan nilainya adalahnull→
simpan referensi elemen dan data yang diperlukan secara sinkron.
- Mengasumsikan
preventDefault()menghentikan bubbling → default dapat dibatalkan sementara leluhur tetap berjalan →
putuskan pembatalan default dan propagasi secara terpisah.
- Menerapkan satu template delegasi untuk setiap event → focus dan mouseenter tidak melakukan bubble seperti click →
pilih capture, event alternatif, atau pengikatan langsung dari semantik event.
- Memeriksa internal Shadow DOM dari root dokumen → target mengalami retargeting dan closed root menyembunyikan
jalur internalnya → biarkan komponen memiliki internalnya sendiri dan publikasikan event yang stabil.
- Menambahkan listener anonim pada setiap mount tanpa pembersihan → satu klik berjalan berulang kali dengan state
yang basi → pertahankan referensi yang dapat dilepas atau kelola masa pakai dengan AbortSignal.
- Mengklaim delegasi selalu lebih cepat → antarmuka kecil yang stabil menambah kompleksitas perutean dan pemfilteran →
pilih berdasarkan dinamika node, kepemilikan, dan biaya pemeliharaan.
Tindak Lanjut dan Tanggapan
Tindak Lanjut 1: Tombol anak memanggil stopPropagation, tetapi delegasi luar harus tetap berfungsi. Apa yang Anda lakukan?
Pertama tentukan siapa yang memiliki semantik klik. Jika anak tidak berhak memblokir event, hapus panggilan tersebut dan perbaiki kontrak komponen. Listener capture dapat mengamati event sebelum mencapai tombol, tetapi mengeksekusi penghapusan atau tindakan bisnis lainnya selama capture berjalan sebelum handler target dan perilaku default, mengubah urutan dan peluang pembatalan. Gunakan pengambilalihan capture hanya jika produk secara eksplisit membutuhkannya, dengan pengujian urutan, pembatalan, dan tindakan duplikat.
Tindak Lanjut 2: Sebuah formulir membutuhkan penanganan fokus terpusat. Bagaimana Anda mendelegasikannya?
focus tidak melakukan bubble, tetapi leluhur dapat mengamatinya dengan capture: true; atau gunakan event bubbling focusin. Jika tujuannya adalah satu handler bantuan bidang (field-help), model focusin sering kali lebih mudah dipahami. Uji fokus keyboard, penunjuk (pointer), dan terprogram, serta hindari melaporkan setiap perpindahan di dalam widget komposit sebagai tindakan meninggalkan seluruh widget tersebut.
Tindak Lanjut 3: Tombol tindakan berada di dalam closed shadow root. Bagaimana daftar tingkat halaman mengidentifikasinya?
Halaman tidak boleh mengidentifikasi tombol di dalam closed root. Komponen menangani klik secara internal dan mempublikasikan event semantik dari host-nya, hanya dengan tindakan stabil dan pengenal tugas dalam datanya serta kontrak eksplisit yang memungkinkan bubbling dan melintasi batas shadow. Halaman mendelegasikan event komponen tersebut. Enkapsulasi tetap utuh, dan halaman tidak bergantung pada node internal yang tidak akan diungkapkan oleh composedPath().
Tindak Lanjut 4: Handler menunggu konfirmasi. Bagaimana Anda menghindari bertindak pada baris yang salah setelah perubahan DOM?
Baca dan validasi taskId dan actionName yang tidak dapat diubah (immutable) secara sinkron sebelum memulai alur asinkron. Setelah konfirmasi, cari status bisnis saat ini berdasarkan taskId; jangan menyimpan referensi baris lama dan menganggapnya masih terhubung. Jika duplikasi menjadi masalah, tandai juga tugas/tindakan sebagai pending atau gunakan kunci idempoten (idempotency key) untuk permintaan tersebut.
Tindak Lanjut 5: Kapan Anda harus meninggalkan delegasi?
Listener langsung lebih mudah diaudit ketika node berjumlah sedikit dan stabil, kontrol memiliki perilaku yang tidak terkait, event tidak melakukan bubble, atau batas komponen membutuhkan penanganan lokal. Untuk event frekuensi tinggi, jika satu root berulang kali menjalankan selektor kompleks untuk banyak event yang tidak relevan, persempit root atau ikat secara langsung. Ukur jalur kinerja aktual sebelum mengubah struktur demi kinerja.