kodingup.camp

Beranda/JavaScript Mastery: Prototype, Class, Waktu, Error & Async

Penjadwalan & Pemrosesan Waktu

Modul 9 dari 153 menit bacaScheduling & Time Processing

Isi modul

    Kenapa ini penting

    Sejauh ini kodemu berjalan dari atas ke bawah, langsung selesai. Padahal aplikasi nyata sering perlu menunda atau mengulang sesuatu: menutup notifikasi setelah 3 detik, memperbarui jam di layar setiap detik, mengecek pesan baru setiap 30 detik, atau menunggu pengguna berhenti mengetik sebelum mencari ke server. Semua ini dilakukan dengan timer.

    Timer juga pintu masuk pertamamu ke dunia asynchronous: kode yang dijadwalkan untuk berjalan nanti, sementara kode lain tetap berjalan. Selain itu kamu akan belajar dua teknik yang dipakai hampir di setiap aplikasi web profesional: debounce dan throttle. Tanpa keduanya, kolom pencarian bisa mengirim 20 request ke server untuk satu kata, dan event scroll bisa membuat halaman tersendat. Analoginya: debounce seperti lift yang menunggu sampai tidak ada lagi orang masuk sebelum pintunya menutup, throttle seperti pintu putar yang hanya meloloskan satu orang setiap beberapa detik.

    Konsep inti

    setTimeout dan clearTimeout

    const id = setTimeout(fungsi, jedaMs, ...argumen);
    clearTimeout(id); // batalkan sebelum sempat jalan
    

    setInterval dan clearInterval

    Menjalankan fungsi berulang setiap jedaMs. Wajib dihentikan dengan clearInterval(id) jika sudah tidak dibutuhkan, jika tidak ia akan berjalan selamanya dan bisa menyebabkan kebocoran memori.

    Kenapa timer tidak tepat waktu?

    JavaScript menjalankan satu hal pada satu waktu (single-threaded). Timer hanya "mendaftarkan" callback untuk dijalankan nanti. Jika ada pekerjaan berat yang sedang berjalan, callback harus antre. Browser juga memperlambat timer di tab yang tidak aktif (biasanya maksimal sekali per detik atau lebih jarang) untuk menghemat baterai. Karena itu, jam dan countdown harus menghitung dari Date.now(), bukan menghitung jumlah tick.

    setTimeout berulang vs setInterval

    setInterval menjadwalkan tanpa peduli apakah pekerjaan sebelumnya selesai. Jika pekerjaannya asinkron dan lama (misalnya request ke server), pekerjaan bisa menumpuk. Pola yang lebih aman: setTimeout yang menjadwalkan dirinya sendiri setelah pekerjaan selesai.

    Debounce

    Tunda eksekusi sampai tidak ada panggilan baru selama X ms. Setiap panggilan baru membatalkan timer sebelumnya dan memulai lagi. Cocok untuk: pencarian saat mengetik, menyimpan draf otomatis, menghitung ulang layout setelah jendela selesai di-resize.

    Throttle

    Jalankan paling banyak sekali setiap X ms. Panggilan di antara jeda diabaikan. Cocok untuk: event scroll, gerakan mouse, tombol yang ditekan berkali-kali.

    Debounce Throttle
    Kapan jalan Setelah aktivitas berhenti Berkala selama aktivitas
    Mengetik "kopi" cepat 1 kali (setelah selesai) Beberapa kali
    Contoh Pencarian otomatis Posisi scroll

    Contoh kode

    // 1. setTimeout dasar & urutan eksekusi
    console.log("A: mulai");
    setTimeout(() => console.log("C: dari setTimeout 0 ms"), 0);
    console.log("B: kode sinkron selesai");
    // → A: mulai
    // → B: kode sinkron selesai
    // → C: dari setTimeout 0 ms
    
    // 2. Membatalkan timeout
    const idNotif = setTimeout(() => console.log("Notifikasi ditutup otomatis"), 3000);
    clearTimeout(idNotif); // pengguna menutup manual, timer dibatalkan → tidak ada output
    
    // 3. setInterval dengan batas
    let hitung = 0;
    const idInterval = setInterval(() => {
      hitung++;
      console.log(`Tick ke-${hitung}`);
      if (hitung === 3) {
        clearInterval(idInterval);
        console.log("Interval dihentikan");
      }
    }, 100);
    // → Tick ke-1
    // → Tick ke-2
    // → Tick ke-3
    // → Interval dihentikan
    
    // 4. Debounce
    function debounce(fn, jeda) {
      let timer;
      return function (...args) {
        clearTimeout(timer); // batalkan rencana sebelumnya
        timer = setTimeout(() => fn.apply(this, args), jeda);
      };
    }
    
    const cariProduk = debounce((kata) => console.log(`Mencari ke server: "${kata}"`), 300);
    // simulasi mengetik "kopi" huruf demi huruf, jarak 50 ms
    ["k", "ko", "kop", "kopi"].forEach((teks, i) => setTimeout(() => cariProduk(teks), 400 + i * 50));
    // → Mencari ke server: "kopi"   (hanya sekali!)
    
    // 5. Throttle
    function throttle(fn, jeda) {
      let terakhir = 0;
      return function (...args) {
        const now = Date.now();
        if (now - terakhir >= jeda) {
          terakhir = now;
          fn.apply(this, args);
        }
      };
    }
    
    const catatScroll = throttle((posisi) => console.log(`Posisi scroll: ${posisi}`), 200);
    // simulasi 10 event scroll setiap 50 ms (total ±500 ms)
    for (let i = 0; i < 10; i++) {
      setTimeout(() => catatScroll(i * 100), 900 + i * 50);
    }
    // → Posisi scroll: 0
    // → Posisi scroll: 400
    // → Posisi scroll: 800 atau 900   (sekitar 3 kali, bukan 10; angka pasti tergantung timing)
    
    // 6. Polling aman dengan setTimeout berantai
    let percobaan = 0;
    function cekStatusPembayaran() {
      percobaan++;
      const lunas = percobaan === 3; // simulasi: lunas di percobaan ke-3
      console.log(`Cek pembayaran #${percobaan}: ${lunas ? "LUNAS" : "menunggu"}`);
      if (!lunas) setTimeout(cekStatusPembayaran, 150); // jadwalkan setelah selesai
    }
    setTimeout(cekStatusPembayaran, 1600);
    // → Cek pembayaran #1: menunggu
    // → Cek pembayaran #2: menunggu
    // → Cek pembayaran #3: LUNAS
    
    // 7. Countdown yang akurat (menghitung dari target)
    function mulaiCountdown(targetMs, onTick, onSelesai) {
      const id = setInterval(() => {
        const sisa = targetMs - Date.now();
        if (sisa <= 0) {
          clearInterval(id);
          onSelesai();
          return;
        }
        onTick(Math.ceil(sisa / 1000));
      }, 250); // cek lebih sering dari 1 detik agar tampilan tidak telat
      return () => clearInterval(id); // kembalikan fungsi pembatal
    }
    setTimeout(() => {
      let terakhirTampil;
      mulaiCountdown(
        Date.now() + 2000,
        (detik) => { if (detik !== terakhirTampil) console.log(`Sisa ${detik} detik`); terakhirTampil = detik; },
        () => console.log("Waktu habis!"),
      );
    }, 2200);
    // → Sisa 2 detik
    // → Sisa 1 detik
    // → Waktu habis!
    

    Jalankan file di atas dengan node dan perhatikan: semua output muncul dalam urutan waktu, bukan urutan baris kode. Itulah esensi asynchronous.

    Untuk penggunaan di browser, debounce biasanya dipasang ke event input:

    <input id="cari" placeholder="Cari produk..." />
    <script>
      function debounce(fn, jeda) {
        let t;
        return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), jeda); };
      }
      const input = document.getElementById("cari");
      input.addEventListener("input", debounce((e) => {
        console.log("Cari:", e.target.value); // hanya jalan 300 ms setelah berhenti mengetik
      }, 300));
    </script>
    

    Kesalahan umum

    1. Memanggil function, bukan memberikannya. setTimeout(sapa(), 1000) menjalankan sapa langsung, lalu mengirim hasilnya (biasanya undefined) ke timer. Yang benar: setTimeout(sapa, 1000) atau setTimeout(() => sapa("Sari"), 1000).
    2. Lupa clearInterval. Interval terus berjalan meski komponen/halaman sudah tidak dipakai, memboroskan CPU dan memori.
    3. Mengandalkan timer untuk ketepatan waktu. Jeda hanyalah batas minimal. Hitung waktu nyata dengan Date.now().
    4. Debounce dibuat ulang setiap event. input.oninput = () => debounce(f, 300)() membuat timer baru setiap kali sehingga tidak pernah membatalkan yang lama. Buat debounce sekali, simpan hasilnya.
    5. setInterval untuk pekerjaan asinkron yang lama. Request bisa menumpuk. Pakai setTimeout berantai.

    Latihan

    1. Mudah, Tampilkan "Halo!" setelah 1 detik, lalu "Apa kabar?" 1 detik setelahnya, memakai setTimeout.
    2. Sedang, Buat jam digital konsol yang mencetak waktu WIB (HH.MM.SS) setiap detik, dan berhenti otomatis setelah 5 kali.
    3. Menantang, Tambahkan method .batal() pada function hasil debounce sehingga eksekusi yang tertunda bisa dibatalkan, lalu uji.
    Lihat solusi
    // 1
    setTimeout(() => {
      console.log("Halo!");
      setTimeout(() => console.log("Apa kabar?"), 1000);
    }, 1000);
    
    // 2
    const fmtJam = new Intl.DateTimeFormat("id-ID", {
      hour: "2-digit", minute: "2-digit", second: "2-digit", timeZone: "Asia/Jakarta",
    });
    let n = 0;
    const idJam = setInterval(() => {
      console.log(fmtJam.format(new Date())); // → 14.30.01 (contoh)
      if (++n === 5) clearInterval(idJam);
    }, 1000);
    
    // 3
    function debounce(fn, jeda) {
      let timer;
      const hasil = (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), jeda);
      };
      hasil.batal = () => clearTimeout(timer);
      return hasil;
    }
    const simpan = debounce(() => console.log("Draf disimpan"), 200);
    simpan();
    simpan.batal(); // → (tidak ada output, eksekusi dibatalkan)
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.