Bab 15 dari Eloquent JavaScript. Sebelumnya: 14 Document Object Model. Berikutnya: 16 Proyek Game Platform.
Kamu punya kuasa atas pikiranmu, bukan atas peristiwa di luar. Sadari ini, dan kamu bakal nemuin kekuatan.
— Marcus Aurelius, Meditations

Beberapa program bekerja dengan input pengguna langsung, kaya aksi mouse dan keyboard. Jenis input itu gak tersedia di awal, sebagai struktur data yang terorganisir dengan baik, dia datang potongan per potongan, secara real-time, dan program harus merespons ke dia seiring dia terjadi.
Event Handler
Bayangin interface di mana satu-satunya cara buat nyari tahu apakah tombol keyboard lagi ditekan adalah baca keadaan tombol itu saat ini. Biar bisa bereaksi ke penekanan tombol, kamu harus terus-menerus baca keadaan tombolnya buat nangkepnya sebelum dia dilepas lagi. Bakal berbahaya buat ngelakuin perhitungan lain yang makan waktu, karena kamu mungkin ketinggalan penekanan tombol.
Beberapa mesin primitif nanganin input kaya gini. Selangkah lebih maju dari ini adalah buat hardware atau sistem operasi nyadar penekanan tombolnya dan naruhnya di antrean. Program lalu bisa secara berkala ngecek antrean buat event baru dan bereaksi ke apa yang dia temuin di sana.
Tentu aja, programnya harus ingat buat ngelihat antreannya, dan ngelakuinnya sering karena waktu apa pun antara tombol ditekan dan program nyadar event-nya bakal bikin software terasa gak responsif. Pendekatan ini disebut polling. Kebanyakan programmer lebih suka ngehindarinnya.
Mekanisme yang lebih baik adalah buat sistem secara aktif ngasih tahu kodenya waktu event terjadi. Browser ngelakuin ini dengan ngebolehin kita mendaftarkan fungsi sebagai handler buat event tertentu.
<p>Click this document to activate the handler.</p>
<script>
window.addEventListener("click", () => {
console.log("You knocked?");
});
</script>Binding window ngerujuk ke objek bawaan yang disediain browser. Dia mewakili jendela browser yang berisi dokumennya. Manggil method addEventListener-nya mendaftarkan argumen keduanya buat dipanggil setiap kali event yang digambarkan argumen pertamanya terjadi.
Event dan Node DOM
Setiap event handler browser didaftarkan dalam konteks. Di contoh sebelumnya, kita manggil addEventListener di objek window buat mendaftarkan handler buat seluruh jendela. Method kaya gitu juga bisa ditemuin di elemen DOM dan beberapa jenis objek lain. Event listener cuma dipanggil waktu event-nya terjadi dalam konteks objek tempat mereka didaftarkan.
<button>Click me</button>
<p>No handler here.</p>
<script>
let button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked.");
});
</script>Contoh itu nempelin handler ke node button. Klik di button-nya bikin handler itu jalan, tapi klik di sisa dokumen gak.
Ngasih node atribut onclick punya efek serupa. Ini jalan buat kebanyakan jenis event, kamu bisa nempelin handler lewat atribut yang namanya nama event dengan on di depannya.
Tapi node cuma bisa punya satu atribut onclick, jadi kamu cuma bisa mendaftarkan satu handler per node dengan cara itu. Method addEventListener ngebolehin kamu nambahin berapa pun handler, artinya aman buat nambahin handler bahkan kalau udah ada handler lain di elemennya.
Method removeEventListener, dipanggil dengan argumen mirip addEventListener, ngehapus handler.
<button>Act-once button</button>
<script>
let button = document.querySelector("button");
function once() {
console.log("Done.");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>Fungsi yang dikasih ke removeEventListener harus nilai fungsi yang sama yang dikasih ke addEventListener. Waktu kamu perlu ngebatalin pendaftaran handler, kamu bakal mau ngasih fungsi handler-nya nama (once, di contoh) biar bisa ngasih nilai fungsi yang sama ke kedua method.
Event Object
Walaupun kita udah ngabaikannya sejauh ini, fungsi event handler dikasih argumen: event object. Objek ini megang informasi tambahan soal event-nya. Misalnya, kalau kita mau tahu tombol mouse mana yang ditekan, kita bisa ngelihat properti button dari event object.
<button>Click me any way you want</button>
<script>
let button = document.querySelector("button");
button.addEventListener("mousedown", event => {
if (event.button == 0) {
console.log("Left button");
} else if (event.button == 1) {
console.log("Middle button");
} else if (event.button == 2) {
console.log("Right button");
}
});
</script>Informasi yang disimpan di event object beda per jenis event. (Kita bakal ngebahas jenis yang berbeda nanti di bab.) Properti type objeknya selalu megang string yang ngidentifikasi event-nya (kaya "click" atau "mousedown").
Propagation (Penyebaran)
Buat kebanyakan jenis event, handler yang didaftarkan di node dengan anak juga bakal nerima event yang terjadi di anaknya. Kalau button di dalam paragraf diklik, event handler di paragrafnya juga bakal ngelihat event klik-nya.
Tapi kalau paragraf maupun button punya handler, handler yang lebih spesifik (yang di button) yang duluan. Event-nya dibilang propagate (menyebar) keluar dari node tempat dia terjadi ke node induk node itu dan terus ke root dokumen. Terakhir, setelah semua handler yang didaftarkan di node tertentu udah dapet gilirannya, handler yang didaftarkan di seluruh jendela dapet kesempatan buat merespons event-nya.
Di titik mana pun, event handler bisa manggil method stopPropagation di event object buat nyegah handler yang lebih ke atas nerima event-nya. Ini bisa berguna waktu, misalnya, kamu punya button di dalam elemen lain yang bisa diklik dan kamu gak mau klik di button-nya ngaktifin perilaku klik elemen luarnya.
Contoh berikut mendaftarkan handler "mousedown" di button maupun paragraf di sekitarnya. Waktu diklik dengan tombol mouse kanan, handler buat button-nya manggil stopPropagation, yang bakal nyegah handler di paragrafnya jalan. Waktu button-nya diklik dengan tombol mouse lain, kedua handler bakal jalan.
<p>A paragraph with a <button>button</button>.</p>
<script>
let para = document.querySelector("p");
let button = document.querySelector("button");
para.addEventListener("mousedown", () => {
console.log("Handler for paragraph.");
});
button.addEventListener("mousedown", event => {
console.log("Handler for button.");
if (event.button == 2) event.stopPropagation();
});
</script>Kebanyakan event object punya properti target yang nunjuk ke node tempat mereka berasal. Kamu bisa pakai properti ini buat mastiin kamu gak gak sengaja nanganin sesuatu yang menyebar naik dari node yang gak mau kamu tangani.
Bisa juga pakai properti target buat nebar jaring lebar buat jenis event tertentu. Misalnya, kalau kamu punya node yang berisi daftar panjang button, mungkin lebih praktis mendaftarkan satu click handler di node luarnya dan bikin dia pakai properti target buat nyari tahu apakah button diklik, daripada mendaftarkan handler individual di semua button.
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", event => {
if (event.target.nodeName == "BUTTON") {
console.log("Clicked", event.target.textContent);
}
});
</script>Default Action
Banyak event punya default action (aksi bawaan). Kalau kamu klik link, kamu bakal dibawa ke tujuan link-nya. Kalau kamu mencet panah bawah, browser bakal nge-scroll halaman ke bawah. Kalau kamu klik kanan, kamu bakal dapet menu konteks. Dan seterusnya.
Buat kebanyakan jenis event, event handler JavaScript dipanggil sebelum perilaku default terjadi. Kalau handler-nya gak mau perilaku normal ini terjadi, biasanya karena dia udah nanganin event-nya, dia bisa manggil method preventDefault di event object.
Ini bisa dipakai buat ngimplementasiin keyboard shortcut atau menu konteksmu sendiri. Dia juga bisa dipakai buat secara menyebalkan ngeganggu perilaku yang diharapkan pengguna. Misalnya, ini link yang gak bisa diikuti:
<a href="https://developer.mozilla.org/">MDN</a>
<script>
let link = document.querySelector("a");
link.addEventListener("click", event => {
console.log("Nope.");
event.preventDefault();
});
</script>Coba jangan ngelakuin hal kaya gitu tanpa alasan yang beneran bagus. Bakal gak menyenangkan buat orang yang pakai halamanmu waktu perilaku yang diharapkan rusak.
Tergantung browser-nya, beberapa event gak bisa dicegat sama sekali. Di Chrome, misalnya, keyboard shortcut buat nutup tab saat ini (ctrl-W atau command-W) gak bisa ditangani JavaScript.
Event Keyboard
Waktu tombol di keyboard ditekan, browsermu nembakin event "keydown". Waktu dilepas, kamu dapet event "keyup".
<p>This page turns violet when you hold the V key.</p>
<script>
window.addEventListener("keydown", event => {
if (event.key == "v") {
document.body.style.background = "violet";
}
});
window.addEventListener("keyup", event => {
if (event.key == "v") {
document.body.style.background = "";
}
});
</script>Meskipun namanya, "keydown" nembak bukan cuma waktu tombolnya secara fisik ditekan turun. Waktu tombol ditekan dan ditahan, event-nya nembak lagi setiap kali tombolnya mengulang (repeat). Kadang kamu harus hati-hati soal ini. Misalnya, kalau kamu nambahin button ke DOM waktu tombol ditekan dan ngehapusnya lagi waktu tombolnya dilepas, kamu mungkin gak sengaja nambahin ratusan button waktu tombolnya ditahan lebih lama.
Contoh sebelumnya ngelihat properti key dari event object buat ngelihat tombol mana yang jadi soal event-nya. Properti ini megang string yang, buat kebanyakan tombol, sesuai dengan hal yang bakal diketik penekanan tombol itu. Buat tombol khusus kaya enter, dia megang string yang nyebutin tombolnya ("Enter", di kasus ini). Kalau kamu tahan shift sambil mencet tombol, itu mungkin juga mempengaruhi nama tombolnya, "v" jadi "V", dan "1" mungkin jadi "!", kalau itu yang dihasilin mencet shift-1 di keyboard-mu.
Tombol modifier kaya shift, ctrl, alt, dan meta (command di Mac) ngehasilin event tombol kaya tombol normal. Waktu nyari kombinasi tombol, kamu juga bisa nyari tahu apakah tombol-tombol ini ditahan dengan ngelihat properti shiftKey, ctrlKey, altKey, dan metaKey dari event keyboard dan mouse.
<p>Press Control-Space to continue.</p>
<script>
window.addEventListener("keydown", event => {
if (event.key == " " && event.ctrlKey) {
console.log("Continuing!");
}
});
</script>Node DOM tempat event tombol berasal tergantung elemen yang punya fokus waktu tombolnya ditekan. Kebanyakan node gak bisa punya fokus kecuali kamu ngasih mereka atribut tabindex, tapi hal kaya link, button, dan field form bisa. Kita bakal balik ke field form di Bab 18. Waktu gak ada yang khusus punya fokus, document.body bertindak sebagai node target dari event tombol.
Waktu pengguna lagi ngetik teks, pakai event tombol buat nyari tahu apa yang lagi diketik itu bermasalah. Beberapa platform, yang paling terkenal keyboard virtual di ponsel Android, gak nembakin event tombol. Tapi bahkan waktu kamu punya keyboard kuno, beberapa jenis input teks gak cocok dengan penekanan tombol dengan cara yang lugas, kaya software input method editor (IME) yang dipakai orang yang script-nya gak muat di keyboard, di mana beberapa penekanan tombol digabung buat bikin karakter.
Buat nyadar waktu sesuatu diketik, elemen yang bisa kamu ketikin, kaya tag <input> dan <textarea>, nembakin event "input" setiap kali pengguna ngubah isinya. Buat dapet isi sebenarnya yang diketik, paling baik langsung bacanya dari field yang fokus, yang kita bahas di Bab 18.
Event Pointer
Saat ini ada dua cara yang dipakai luas buat nunjuk ke sesuatu di layar: mouse (termasuk perangkat yang bertindak kaya mouse, kaya touchpad dan trackball) dan touchscreen. Ini ngehasilin jenis event yang berbeda.
Klik Mouse
Mencet tombol mouse bikin sejumlah event nembak. Event "mousedown" dan "mouseup" mirip "keydown" dan "keyup" dan nembak waktu tombolnya ditekan dan dilepas. Ini terjadi di node DOM yang tepat di bawah pointer mouse waktu event-nya terjadi.
Setelah event "mouseup", event "click" nembak di node paling spesifik yang berisi penekanan maupun pelepasan tombolnya. Misalnya, kalau saya mencet tombol mouse di satu paragraf lalu mindahin pointer ke paragraf lain dan ngelepas tombolnya, event "click" bakal terjadi di elemen yang berisi kedua paragraf itu.
Kalau dua klik terjadi berdekatan, event "dblclick" (double-click) juga nembak, setelah event klik kedua.
Buat dapet informasi presisi soal tempat event mouse terjadi, kamu bisa ngelihat properti clientX dan clientY-nya, yang berisi koordinat event (dalam piksel) relatif terhadap sudut kiri atas jendela, atau pageX dan pageY, yang relatif terhadap sudut kiri atas seluruh dokumen (yang mungkin beda waktu jendelanya udah di-scroll).
Program berikut ngimplementasiin aplikasi gambar primitif. Setiap kali kamu klik dokumen, dia nambahin titik di bawah pointer mouse-mu.
<style>
body {
height: 200px;
background: beige;
}
.dot {
height: 8px; width: 8px;
border-radius: 4px; /* ngebulatin sudut */
background: teal;
position: absolute;
}
</style>
<script>
window.addEventListener("click", event => {
let dot = document.createElement("div");
dot.className = "dot";
dot.style.left = (event.pageX - 4) + "px";
dot.style.top = (event.pageY - 4) + "px";
document.body.appendChild(dot);
});
</script>Kita bakal bikin aplikasi gambar yang kurang primitif di Bab 19.
Gerakan Mouse
Setiap kali pointer mouse bergerak, event "mousemove" nembak. Event ini bisa dipakai buat ngelacak posisi mouse. Situasi umum di mana ini berguna adalah waktu ngimplementasiin suatu bentuk fungsionalitas mouse-dragging (menyeret).
Sebagai contoh, program berikut nampilin batang dan nyiapin event handler biar nyeret ke kiri atau kanan di batang ini bikinnya lebih sempit atau lebih lebar:
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
let lastX; // Ngelacak posisi X mouse terakhir yang diamati
let bar = document.querySelector("div");
bar.addEventListener("mousedown", event => {
if (event.button == 0) {
lastX = event.clientX;
window.addEventListener("mousemove", moved);
event.preventDefault(); // Nyegah seleksi
}
});
function moved(event) {
if (event.buttons == 0) {
window.removeEventListener("mousemove", moved);
} else {
let dist = event.clientX - lastX;
let newWidth = Math.max(10, bar.offsetWidth + dist);
bar.style.width = newWidth + "px";
lastX = event.clientX;
}
}
</script>Perhatiin bahwa handler "mousemove" didaftarkan di seluruh jendela. Bahkan kalau mouse-nya pergi keluar dari batang selama pengubahan ukuran, selama tombolnya ditahan, kita tetap mau ngupdate ukurannya.
Kita harus berhenti ngubah ukuran batangnya waktu tombol mouse dilepas. Buat itu, kita bisa pakai properti buttons (perhatiin jamaknya), yang ngasih tahu kita soal tombol yang lagi ditahan saat ini. Waktu dia 0, gak ada tombol yang ditekan. Waktu tombol ditahan, nilai properti buttons adalah jumlah dari kode buat tombol-tombol itu, tombol kiri punya kode 1, tombol kanan 2, dan yang tengah 4. Dengan tombol kiri dan kanan ditahan, misalnya, nilai buttons bakal 3.
Perhatiin bahwa urutan kode ini beda dari yang dipakai button, di mana tombol tengah datang sebelum yang kanan. Kaya yang disebutin, konsistensi bukan poin kuat dari interface pemrograman browser.
Event Touch
Gaya browser grafis yang kita pakai didesain dengan interface mouse dalam pikiran, di masa touchscreen langka. Biar web “jalan” di ponsel touchscreen awal, browser buat perangkat itu pura-pura, sampai batas tertentu, bahwa event touch adalah event mouse. Kalau kamu ketuk layarmu, kamu bakal dapet event "mousedown", "mouseup", dan "click".
Tapi ilusi ini gak terlalu tangguh. Touchscreen gak bekerja kaya mouse: dia gak punya beberapa tombol, kamu gak bisa ngelacak jari waktu dia gak di layar (buat nyimulasiin "mousemove"), dan dia ngebolehin beberapa jari di layar di saat yang sama.
Event mouse cuma mencakup interaksi touch di kasus yang lugas, kalau kamu nambahin handler "click" ke button, pengguna touch masih bakal bisa pakainya. Tapi sesuatu kaya batang yang bisa diubah ukurannya di contoh sebelumnya gak jalan di touchscreen.
Ada jenis event tertentu yang ditembakin interaksi touch. Waktu jari mulai nyentuh layar, kamu dapet event "touchstart". Waktu dia digerakin sambil nyentuh, event "touchmove" nembak. Terakhir, waktu dia berhenti nyentuh layar, kamu bakal lihat event "touchend".
Karena banyak touchscreen bisa ngedeteksi beberapa jari di saat yang sama, event ini gak punya satu set koordinat yang terkait dengan mereka. Sebaliknya, event object mereka punya properti touches, yang megang objek mirip-array dari titik, yang masing-masing punya properti clientX, clientY, pageX, dan pageY sendiri.
Kamu bisa ngelakuin sesuatu kaya ini buat nampilin lingkaran merah di sekitar setiap jari yang nyentuh:
<style>
dot { position: absolute; display: block;
border: 2px solid red; border-radius: 50px;
height: 100px; width: 100px; }
</style>
<p>Touch this page</p>
<script>
function update(event) {
for (let dot; dot = document.querySelector("dot");) {
dot.remove();
}
for (let i = 0; i < event.touches.length; i++) {
let {pageX, pageY} = event.touches[i];
let dot = document.createElement("dot");
dot.style.left = (pageX - 50) + "px";
dot.style.top = (pageY - 50) + "px";
document.body.appendChild(dot);
}
}
window.addEventListener("touchstart", update);
window.addEventListener("touchmove", update);
window.addEventListener("touchend", update);
</script>Kamu sering bakal mau manggil preventDefault di handler event touch buat nimpa perilaku default browser (yang mungkin termasuk nge-scroll halaman waktu menggeser) dan buat nyegah event mouse ditembakin, yang mungkin kamu juga punya handler-nya.
Event Scroll
Setiap kali elemen di-scroll, event "scroll" ditembakin di dia. Ini punya berbagai kegunaan, kaya tahu apa yang lagi dilihat pengguna saat ini (buat menonaktifkan animasi di luar layar atau ngirim laporan mata-mata ke markas jahatmu) atau nampilin suatu indikasi kemajuan (dengan nyoroti bagian daftar isi atau nampilin nomor halaman).
Contoh berikut ngegambar progress bar di atas dokumen dan ngupdate dia buat terisi seiring kamu scroll ke bawah:
<style>
#progress {
border-bottom: 2px solid blue;
width: 0;
position: fixed;
top: 0; left: 0;
}
</style>
<div id="progress"></div>
<script>
// Bikin sebagian konten
document.body.appendChild(document.createTextNode(
"supercalifragilisticexpialidocious ".repeat(1000)));
let bar = document.querySelector("#progress");
window.addEventListener("scroll", () => {
let max = document.body.scrollHeight - innerHeight;
bar.style.width = `${(pageYOffset / max) * 100}%`;
});
</script>Ngasih elemen position fixed bertindak mirip position absolute tapi juga nyegahnya nge-scroll bareng sisa dokumen. Efeknya adalah bikin progress bar kita tetap di atas. Lebarnya diubah buat nunjukin kemajuan saat ini. Kita pakai %, bukan px, sebagai unit waktu nyetel lebarnya biar elemennya diukur relatif terhadap lebar halaman.
Binding global innerHeight ngasih kita tinggi jendela, yang harus kita kurangkan dari total tinggi yang bisa di-scroll, kamu gak bisa terus scroll waktu kamu nyampe dasar dokumen. Ada juga innerWidth buat lebar jendela. Dengan ngebagi pageYOffset, posisi scroll saat ini, dengan posisi scroll maksimum dan ngaliin 100, kita dapet persentase buat progress bar-nya.
Manggil preventDefault di event scroll gak nyegah scroll-nya terjadi. Sebenernya, event handler-nya cuma dipanggil setelah scroll-nya terjadi.
Event Focus
Waktu elemen dapet fokus, browser nembakin event "focus" di dia. Waktu dia kehilangan fokus, elemennya dapet event "blur".
Berbeda dari event yang dibahas sebelumnya, dua event ini gak menyebar. Handler di elemen induk gak diberi tahu waktu elemen anak dapet atau kehilangan fokus.
Contoh berikut nampilin teks bantuan buat field teks yang saat ini punya fokus:
<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Your age in years"></p>
<p id="help"></p>
<script>
let help = document.querySelector("#help");
let fields = document.querySelectorAll("input");
for (let field of Array.from(fields)) {
field.addEventListener("focus", event => {
let text = event.target.getAttribute("data-help");
help.textContent = text;
});
field.addEventListener("blur", event => {
help.textContent = "";
});
}
</script>Objek window bakal nerima event "focus" dan "blur" waktu pengguna pindah dari atau ke tab atau jendela browser tempat dokumennya ditampilin.
Event Load
Waktu halaman selesai dimuat, event "load" nembak di objek window dan body dokumen. Ini sering dipakai buat menjadwalkan aksi inisialisasi yang butuh seluruh dokumen udah dibangun. Ingat bahwa isi tag <script> dijalanin langsung waktu tag-nya ditemuin. Ini mungkin terlalu cepat, misalnya waktu script-nya perlu ngelakuin sesuatu dengan bagian dokumen yang muncul setelah tag <script>.
Elemen kaya gambar dan tag script yang ngeload file eksternal juga punya event "load" yang nunjukin file yang mereka rujuk udah dimuat. Kaya event terkait fokus, event loading gak menyebar.
Waktu kamu nutup halaman atau menavigasi menjauh darinya (misalnya, dengan ngikutin link), event "beforeunload" nembak. Kegunaan utama event ini adalah nyegah pengguna gak sengaja kehilangan kerjaan dengan nutup dokumen. Kalau kamu nyegah perilaku default di event ini dan nyetel properti returnValue di event object ke string, browser bakal nampilin dialog ke pengguna yang nanya apakah mereka beneran mau ninggalin halamannya. Dialog itu mungkin nyertain string-mu, tapi karena beberapa situs jahat nyoba pakai dialog ini buat ngebingungin orang biar tetap di halaman mereka buat ngelihat iklan penurun berat badan yang mencurigakan, kebanyakan browser gak lagi nampilinnya.
Event dan Event Loop
Dalam konteks event loop, kaya yang dibahas di Bab 11, event handler browser berperilaku kaya notifikasi asinkron lainnya. Mereka dijadwalkan waktu event-nya terjadi tapi harus nunggu script lain yang lagi jalan selesai sebelum mereka dapet kesempatan buat jalan.
Fakta bahwa event cuma bisa diproses waktu gak ada yang lain jalan berarti kalau event loop-nya keiket sama kerjaan lain, interaksi apa pun dengan halaman (yang terjadi lewat event) bakal ditunda sampai ada waktu buat memprosesnya. Jadi kalau kamu menjadwalkan terlalu banyak kerjaan, entah dengan event handler yang jalan lama atau dengan banyak yang jalan sebentar, halamannya bakal jadi lambat dan berat dipakai.
Buat kasus di mana kamu beneran mau ngelakuin sesuatu yang makan waktu di latar belakang tanpa membekukan halaman, browser nyediain sesuatu yang disebut web worker. Worker adalah proses JavaScript yang jalan berdampingan dengan script utama, di timeline-nya sendiri.
Bayangin bahwa mengkuadratkan angka adalah perhitungan yang berat dan jalan lama yang mau kita lakuin di thread terpisah. Kita bisa nulis file bernama code/squareworker.js yang merespons pesan dengan ngitung kuadrat dan ngirim pesan balik.
addEventListener("message", event => {
postMessage(event.data * event.data);
});Buat ngehindarin masalah punya beberapa thread nyentuh data yang sama, worker gak berbagi scope global mereka atau data lain apa pun dengan environment script utama. Sebaliknya, kamu harus berkomunikasi dengan mereka dengan ngirim pesan bolak-balik.
Kode ini nge-spawn worker yang ngejalanin script itu, ngirim dia beberapa pesan, dan ngeluarin responsnya.
let squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", event => {
console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);Fungsi postMessage ngirim pesan, yang bakal bikin event "message" nembak di penerima. Script yang bikin worker-nya ngirim dan nerima pesan lewat objek Worker, sedangkan worker-nya ngomong ke script yang bikin dia dengan ngirim dan mendengarkan langsung di scope global-nya. Cuma nilai yang bisa diwakili sebagai JSON yang bisa dikirim sebagai pesan, sisi lainnya bakal nerima salinan dari mereka, bukan nilainya sendiri.
Timer
Fungsi setTimeout yang kita lihat di Bab 11 menjadwalkan fungsi lain buat dipanggil nanti, setelah sejumlah milidetik. Kadang kamu perlu ngebatalin fungsi yang udah kamu jadwalin. Kamu bisa ngelakuin ini dengan nyimpen nilai yang dikembaliin setTimeout dan manggil clearTimeout di dia.
let bombTimer = setTimeout(() => {
console.log("BOOM!");
}, 500);
if (Math.random() < 0.5) { // 50% peluang
console.log("Defused.");
clearTimeout(bombTimer);
}Fungsi cancelAnimationFrame bekerja dengan cara yang sama kaya clearTimeout. Manggilnya di nilai yang dikembaliin requestAnimationFrame bakal ngebatalin frame itu (dengan asumsi dia belum dipanggil).
Sekumpulan fungsi serupa, setInterval dan clearInterval, dipakai buat nyetel timer yang harus ngulang setiap X milidetik.
let ticks = 0;
let clock = setInterval(() => {
console.log("tick", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("stop.");
}
}, 200);Debouncing
Beberapa jenis event berpotensi nembak cepat berkali-kali berturut-turut, kaya event "mousemove" dan "scroll". Waktu nanganin event kaya gitu, kamu harus hati-hati buat gak ngelakuin apa pun yang terlalu makan waktu atau handler-mu bakal makan begitu banyak waktu sampai interaksi dengan dokumennya mulai terasa lambat.
Kalau kamu emang perlu ngelakuin sesuatu yang nontrivial di handler kaya gitu, kamu bisa pakai setTimeout buat mastiin kamu gak ngelakuinnya terlalu sering. Ini biasanya disebut debouncing event-nya. Ada beberapa pendekatan yang sedikit berbeda buat ini.
Misalnya, anggap kita mau bereaksi waktu pengguna udah ngetik sesuatu, tapi kita gak mau ngelakuinnya langsung buat setiap event input. Waktu mereka ngetik cepat, kita cuma mau nunggu sampai jeda terjadi. Bukannya langsung ngelakuin aksi di event handler, kita nyetel timeout. Kita juga ngehapus timeout sebelumnya (kalau ada) biar waktu event terjadi berdekatan (lebih dekat dari delay timeout kita), timeout dari event sebelumnya bakal dibatalin.
<textarea>Type something here...</textarea>
<script>
let textarea = document.querySelector("textarea");
let timeout;
textarea.addEventListener("input", () => {
clearTimeout(timeout);
timeout = setTimeout(() => console.log("Typed!"), 500);
});
</script>Ngasih nilai undefined ke clearTimeout atau manggilnya di timeout yang udah nembak gak punya efek. Jadi, kita gak harus hati-hati soal kapan manggilnya, dan kita cuma ngelakuinnya buat setiap event.
Kita bisa pakai pola yang sedikit berbeda kalau kita mau nyilangin respons biar mereka dipisah setidaknya jarak waktu tertentu tapi mau nembak mereka selama serangkaian event, bukan cuma setelahnya. Misalnya, kita mungkin mau merespons event "mousemove" dengan nampilin koordinat mouse saat ini, tapi cuma setiap 250 milidetik.
<script>
let scheduled = null;
window.addEventListener("mousemove", event => {
if (!scheduled) {
setTimeout(() => {
document.body.textContent =
`Mouse at ${scheduled.pageX}, ${scheduled.pageY}`;
scheduled = null;
}, 250);
}
scheduled = event;
});
</script>Ringkasan
Event handler bikin mungkin buat ngedeteksi dan bereaksi ke event yang terjadi di halaman web kita. Method addEventListener dipakai buat mendaftarkan handler kaya gitu.
Setiap event punya jenis ("keydown", "focus", dan seterusnya) yang ngidentifikasinya. Kebanyakan event dipanggil di elemen DOM tertentu lalu menyebar ke leluhur elemen itu, ngebolehin handler yang terkait dengan elemen itu buat nanganinnya.
Waktu event handler dipanggil, dia dikasih event object dengan informasi tambahan soal event-nya. Objek ini juga punya method yang ngebolehin kita ngehentiin penyebaran lebih lanjut (stopPropagation) dan nyegah penanganan default browser dari event-nya (preventDefault).
Mencet tombol nembakin event "keydown" dan "keyup". Mencet tombol mouse nembakin event "mousedown", "mouseup", dan "click". Mindahin mouse nembakin event "mousemove". Interaksi touchscreen bakal ngehasilin event "touchstart", "touchmove", dan "touchend".
Scroll bisa dideteksi dengan event "scroll", dan perubahan fokus bisa dideteksi dengan event "focus" dan "blur". Waktu dokumen selesai dimuat, event "load" nembak di window.
Poin-poin pentingnya:
addEventListener(type, handler)mendaftarkan handler;removeEventListener(fungsi harus sama) ngehapusnya. Lebih baik dari atributonclick(yang cuma satu per node).- Handler dikasih event object dengan info (
type,button,key,clientX/Y,pageX/Y,target, dll). - Event menyebar (propagate) dari node target ke atas.
stopPropagation()menghentikannya,preventDefault()mencegah aksi default browser. - Jenis event: keyboard (
keydown/keyup,event.key), mouse (mousedown/mouseup/click/dblclick/mousemove), touch (touchstart/touchmove/touchend,event.touches),scroll,focus/blur(gak menyebar),load,beforeunload. - Event jalan di event loop: kerjaan berat bikin halaman lambat. Pakai web worker buat kerjaan berat di thread terpisah (komunikasi lewat
postMessage). - Timer:
setTimeout/clearTimeout,setInterval/clearInterval,requestAnimationFrame/cancelAnimationFrame. - Debouncing: pakai
setTimeoutbiar event yang nembak cepat (mousemove, scroll, input) gak ngebanjirin handler.
Latihan
Balon
Tulis halaman yang nampilin balon (pakai emoji balon, 🎈). Waktu kamu mencet panah atas, dia harus mengembang (membesar) 10 persen. Waktu kamu mencet panah bawah, dia harus mengempis (mengecil) 10 persen.
Kamu bisa ngendaliin ukuran teks (emoji itu teks) dengan nyetel properti CSS font-size (style.fontSize) di elemen induknya. Ingat buat nyertain unit di nilainya, misalnya, piksel (10px).
Nama tombol dari tombol panah adalah "ArrowUp" dan "ArrowDown". Pastiin tombolnya cuma ngubah balonnya, tanpa nge-scroll halaman.
Begitu itu jalan, tambahin fitur di mana kalau kamu ngembangin balon melewati ukuran tertentu, dia “meledak”. Di kasus ini, meledak berarti dia diganti dengan emoji 💥, dan event handler-nya dihapus (biar kamu gak bisa ngembangin atau ngempisin ledakannya).
<p>🎈</p>
<script>
// Kodemu di sini
</script>Petunjuk:
Kamu bakal mau mendaftarkan handler buat event "keydown" dan ngelihat event.key buat nyari tahu apakah tombol panah atas atau bawah yang ditekan.
Ukuran saat ini bisa disimpen di binding biar kamu bisa mendasarkan ukuran baru padanya. Bakal membantu buat mendefinisikan fungsi yang ngupdate ukurannya, baik binding maupun style balon di DOM, biar kamu bisa manggilnya dari event handler-mu, dan mungkin juga sekali waktu mulai, buat nyetel ukuran awal.
Kamu bisa ngubah balonnya jadi ledakan dengan ngeganti node teksnya dengan yang lain (pakai replaceChild) atau dengan nyetel properti textContent dari node induknya ke string baru.
Solusi:
<p>🎈</p>
<script>
let p = document.querySelector("p");
let size;
function setSize(newSize) {
size = newSize;
p.style.fontSize = size + "px";
}
setSize(20);
function handleArrow(event) {
if (event.key == "ArrowUp") {
if (size > 70) {
p.textContent = "💥";
document.body.removeEventListener("keydown", handleArrow);
} else {
setSize(size * 1.1);
event.preventDefault();
}
} else if (event.key == "ArrowDown") {
setSize(size * 0.9);
event.preventDefault();
}
}
document.body.addEventListener("keydown", handleArrow);
</script>Jejak Mouse
Di hari-hari awal JavaScript, yang merupakan masa jaya home page mencolok dengan banyak gambar beranimasi, orang-orang nemuin cara yang beneran menginspirasi buat pakai bahasanya. Salah satunya adalah jejak mouse (mouse trail), serangkaian elemen yang bakal ngikutin pointer mouse waktu kamu mindahinnya di seluruh halaman.
Di latihan ini, saya mau kamu ngimplementasiin jejak mouse. Pakai elemen <div> yang diposisikan secara absolut dengan ukuran tetap dan warna latar (lihat kode di bagian “Klik Mouse” buat contoh). Bikin sekumpulan elemen ini dan, waktu mouse-nya bergerak, tampilin mereka di belakang pointer mouse.
Ada berbagai pendekatan yang mungkin di sini. Kamu bisa bikin jejakmu sesederhana atau serumit yang kamu mau. Solusi sederhana buat mulai adalah ngejaga jumlah tetap elemen jejak dan bergilir lewat mereka, mindahin yang berikutnya ke posisi mouse saat ini setiap kali event "mousemove" terjadi.
<style>
.trail { /* className buat elemen jejak */
position: absolute;
height: 6px; width: 6px;
border-radius: 3px;
background: teal;
}
body {
height: 300px;
}
</style>
<script>
// Kodemu di sini.
</script>Petunjuk:
Bikin elemennya paling baik dilakuin dengan loop. Tambahin mereka ke dokumen biar mereka muncul. Biar bisa ngaksesnya nanti buat ngubah posisi mereka, kamu bakal mau nyimpen elemennya di array.
Bergilir lewat mereka bisa dilakuin dengan ngejaga variabel counter dan nambahin 1 ke dia setiap kali event "mousemove" nembak. Operator sisa bagi (% elements.length) lalu bisa dipakai buat dapet indeks array yang valid buat milih elemen yang kamu mau posisikan selama event tertentu.
Efek menarik lain bisa dicapai dengan memodelkan sistem fisika sederhana. Pakai event "mousemove" cuma buat ngupdate sepasang binding yang ngelacak posisi mouse. Lalu pakai requestAnimationFrame buat nyimulasiin elemen jejak ditarik ke posisi pointer mouse. Di setiap langkah animasi, update posisi mereka berdasarkan posisi mereka relatif terhadap pointer (dan, opsional, kecepatan yang disimpen buat setiap elemen). Nyari tahu cara yang baik buat ngelakuin ini terserah kamu.
Solusi:
<style>
.trail { /* className buat elemen jejak */
position: absolute;
height: 6px; width: 6px;
border-radius: 3px;
background: teal;
}
body {
height: 300px;
}
</style>
<body>
<script>
let dots = [];
for (let i = 0; i < 12; i++) {
let node = document.createElement("div");
node.className = "trail";
document.body.appendChild(node);
dots.push(node);
}
let currentDot = 0;
window.addEventListener("mousemove", event => {
let dot = dots[currentDot];
dot.style.left = (event.pageX - 3) + "px";
dot.style.top = (event.pageY - 3) + "px";
currentDot = (currentDot + 1) % dots.length;
});
</script>
</body>Tab
Panel bertab umum di interface pengguna. Mereka ngebolehin kamu milih panel interface dengan milih dari sejumlah tab yang “nyembul keluar” di atas sebuah elemen.
Implementasikan interface bertab sederhana. Tulis fungsi, asTabs, yang ngambil node DOM dan bikin interface bertab yang nampilin elemen anak dari node itu. Dia harus nyisipin daftar elemen <button> di atas node-nya, satu buat setiap elemen anak, yang berisi teks yang diambil dari atribut data-tabname dari anaknya. Semua kecuali satu dari anak aslinya harus disembunyiin (dikasih style display none). Node yang saat ini terlihat bisa dipilih dengan ngeklik button-nya.
Waktu itu jalan, perluas dia buat nge-style button buat tab yang saat ini terpilih secara berbeda biar jelas tab mana yang terpilih.
<tab-panel>
<div data-tabname="one">Tab one</div>
<div data-tabname="two">Tab two</div>
<div data-tabname="three">Tab three</div>
</tab-panel>
<script>
function asTabs(node) {
// Kodemu di sini.
}
asTabs(document.querySelector("tab-panel"));
</script>Petunjuk:
Salah satu jebakan yang mungkin kamu temuin adalah kamu gak bisa langsung pakai properti childNodes node-nya sebagai koleksi node tab. Pertama, waktu kamu nambahin button, mereka juga bakal jadi node anak dan berakhir di objek ini karena dia struktur data yang live. Kedua, node teks yang dibikin buat whitespace di antara node juga ada di childNodes tapi harusnya gak dapet tab mereka sendiri. Kamu bisa pakai children bukannya childNodes buat ngabaikan node teks.
Kamu bisa mulai dengan ngebangun array tab biar kamu punya akses gampang ke mereka. Buat ngimplementasiin styling button-nya, kamu bisa nyimpen objek yang berisi panel tab maupun button-nya.
Saya nyaranin nulis fungsi terpisah buat ngeganti tab. Kamu bisa nyimpen tab yang sebelumnya terpilih dan cuma ngubah style yang dibutuhin buat nyembunyiin itu dan nampilin yang baru, atau kamu bisa cuma ngupdate style semua tab setiap kali tab baru dipilih.
Kamu mungkin mau manggil fungsi ini langsung biar interface-nya mulai dengan tab pertama terlihat.
Solusi:
<tab-panel>
<div data-tabname="one">Tab one</div>
<div data-tabname="two">Tab two</div>
<div data-tabname="three">Tab three</div>
</tab-panel>
<script>
function asTabs(node) {
let tabs = Array.from(node.children).map(node => {
let button = document.createElement("button");
button.textContent = node.getAttribute("data-tabname");
let tab = {node, button};
button.addEventListener("click", () => selectTab(tab));
return tab;
});
let tabList = document.createElement("div");
for (let {button} of tabs) tabList.appendChild(button);
node.insertBefore(tabList, node.firstChild);
function selectTab(selectedTab) {
for (let tab of tabs) {
let selected = tab == selectedTab;
tab.node.style.display = selected ? "" : "none";
tab.button.style.color = selected ? "red" : "";
}
}
selectTab(tabs[0]);
}
asTabs(document.querySelector("tab-panel"));
</script>