Entri front-end (vue) ke kursus kemahiran: Masuk untuk mempelajari JavaScript tidak menyediakan operasi manajemen memori apa pun. Sebaliknya, memori dikelola oleh VM JavaScript melalui proses reklamasi memori yang disebut pengumpulan sampah .
Karena kita tidak bisa memaksa pengumpulan sampah, bagaimana kita tahu cara ini berhasil? Berapa banyak yang kita ketahui tentang hal itu?
Eksekusi skrip dijeda selama proses ini
Ini melepaskan memori untuk sumber daya yang tidak dapat diakses
itu tidak pasti
Itu tidak memeriksa seluruh memori sekaligus, tetapi berjalan dalam beberapa siklus
Hal ini tidak dapat diprediksi tetapi akan dilakukan bila diperlukan
Apakah ini berarti tidak perlu khawatir tentang masalah alokasi sumber daya dan memori? Jika kita tidak berhati-hati, beberapa kebocoran memori mungkin terjadi.
Kebocoran memori adalah blok memori yang dialokasikan yang tidak dapat diperoleh kembali oleh perangkat lunak.
Javascript menyediakan pengumpul sampah, namun bukan berarti kita bisa menghindari kebocoran memori. Agar memenuhi syarat untuk pengumpulan sampah, objek tersebut tidak boleh dirujuk ke tempat lain. Jika Anda menyimpan referensi ke sumber daya yang tidak terpakai, hal ini akan mencegah sumber daya tersebut diklaim kembali. Ini disebut retensi memori bawah sadar .
Kebocoran memori dapat menyebabkan pengumpul sampah berjalan lebih sering. Karena proses ini akan mencegah skrip berjalan, hal ini dapat menyebabkan program kita terhenti. Jika kelambatan seperti itu terjadi, pengguna yang pilih-pilih pasti akan menyadari bahwa jika mereka tidak menyukainya, produk akan offline untuk waktu yang lama. Lebih serius lagi, ini dapat menyebabkan seluruh aplikasi mogok, yaitu gg.
Bagaimana cara mencegah kebocoran memori? Hal utama adalah kita harus menghindari penyimpanan sumber daya yang tidak perlu. Mari kita lihat beberapa skenario umum.
Metode setInterval()
berulang kali memanggil suatu fungsi atau mengeksekusi sebuah fragmen kode, dengan waktu tunda yang tetap di antara setiap panggilan. Ia mengembalikan ID
interval ID
secara unik mengidentifikasi interval sehingga Anda nanti dapat menghapusnya dengan memanggil clearInterval()
.
Kita membuat komponen yang memanggil fungsi callback untuk menunjukkan bahwa komponen tersebut selesai setelah x
jumlah loop. Saya menggunakan React dalam contoh ini, tetapi ini berfungsi dengan kerangka FE apa pun.
impor Bereaksi, { useRef } dari 'reaksi'; const Timer = ({ siklus, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { diSelesai(); kembali; } currentCicles.saat ini++; }, 500); kembali ( <p>Memuat...</p> ); } ekspor Timer bawaan;
Sekilas sepertinya tidak ada masalah. Jangan khawatir, mari buat komponen lain yang memicu pengatur waktu ini dan menganalisis kinerja memorinya.
impor Bereaksi, { useState } dari 'reaksi'; impor gaya dari '../styles/Home.module.css' impor Timer dari '../components/Timer'; ekspor fungsi default Beranda() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); kembali ( <p className={styles.container}> {pengatur waktu pertunjukan? ( <Siklus pengatur waktu={10} onFinish={onFinish} /> ): ( <tombol onClick={() => setShowTimer(true)}> Mencoba kembali </tombol> )} </p> ) }
Setelah beberapa kali klik pada tombol Retry
, inilah hasil penggunaan Chrome Dev Tools untuk mengetahui penggunaan memori:
Saat kita mengklik tombol coba lagi, kita dapat melihat bahwa semakin banyak memori yang dialokasikan. Artinya memori yang dialokasikan sebelumnya belum dibebaskan. Timernya masih berjalan bukannya diganti.
Bagaimana cara mengatasi masalah ini? Nilai kembalian setInterval
adalah ID interval, yang dapat kita gunakan untuk membatalkan interval ini. Dalam kasus khusus ini, kita dapat memanggil clearInterval
setelah komponen dibongkar.
gunakanEffect(() => { const intervalId = setInterval(() => { if (currentCicles.current >= cicles) { diSelesai(); kembali; } currentCicles.saat ini++; }, 500); kembali () => clearInterval(intervalId); }, [])
Terkadang, sulit untuk menemukan masalah ini saat menulis kode. Cara terbaik adalah dengan mengabstraksi komponen.
Dengan menggunakan React di sini, kita dapat menggabungkan semua logika ini dalam sebuah Hook khusus.
impor { useEffect } dari 'reaksi'; ekspor const useTimeout = (refreshCycle = 100, panggilan balik) => { gunakanEffect(() => { if (segarkanSiklus <= 0) { setTimeout(panggilan balik, 0); kembali; } const intervalId = setInterval(() => { panggilan balik(); }, segarkanSiklus); kembali () => clearInterval(intervalId); }, [refreshCycle, setInterval, clearInterval]); }; ekspor useTimeout default;
Sekarang kapan pun Anda perlu menggunakan setInterval
, Anda dapat melakukan ini:
const handleTimeout = () => ...; useTimeout(100, handleTimeout);
Sekarang Anda dapat menggunakan useTimeout Hook
ini tanpa mengkhawatirkan kebocoran memori, yang juga merupakan manfaat abstraksi.
Web API menyediakan sejumlah besar pendengar acara. Sebelumnya, kita membahas setTimeout
. Sekarang mari kita lihat addEventListener
.
Dalam contoh ini, kami membuat fungsi pintasan keyboard. Karena kami memiliki fungsi berbeda di halaman berbeda, fungsi tombol pintasan berbeda akan dibuat
fungsi berandaPintasan({ kunci}) { jika (kunci === 'E') { console.log('edit widget') } } // Saat pengguna masuk ke beranda, kami menjalankan document.addEventListener('keyup', homeShortcuts); // Pengguna melakukan sesuatu dan kemudian menavigasi ke fungsi pengaturan settingsShortcuts({ key}) { jika (kunci === 'E') { console.log('edit pengaturan') } } // Saat pengguna masuk ke beranda, kami menjalankan document.addEventListener('keyup', settingsShortcuts);
Masih terlihat baik-baik saja, kecuali keyup
sebelumnya tidak dibersihkan saat menjalankan addEventListener
kedua. Daripada mengganti keyup
pendengar kita, kode ini akan menambahkan callback
lain. Artinya ketika sebuah tombol ditekan, dua fungsi akan terpicu.
Untuk menghapus callback sebelumnya, kita perlu menggunakan removeEventListener
:
document.removeEventListener('keyup', homeShortcuts);
Refactor kode di atas:
fungsi berandaPintasan({ kunci}) { jika (kunci === 'E') { console.log('edit widget') } } // pengguna mendarat di rumah dan kami mengeksekusi document.addEventListener('keyup', homeShortcuts); // pengguna melakukan beberapa hal dan membuka pengaturan pengaturan fungsiPintasan({ kunci}) { jika (kunci === 'E') { console.log('edit pengaturan') } } // pengguna mendarat di rumah dan kami mengeksekusi document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', settingsShortcuts);
Sebagai aturan praktis, berhati-hatilah saat menggunakan alat dari objek global.
Pengamat adalah fitur API Web browser yang tidak disadari oleh banyak pengembang. Ini berguna jika Anda ingin memeriksa perubahan visibilitas atau ukuran elemen HTML.
Antarmuka IntersectionObserver
(bagian dari Intersection Observer API) menyediakan metode untuk mengamati status perpotongan elemen target dengan elemen leluhurnya atau viewport
dokumen tingkat atas secara asinkron. Elemen leluhur dan viewport
disebut root
.
Meski kuat, kita harus menggunakannya dengan hati-hati. Setelah Anda selesai mengamati suatu objek, ingatlah untuk membatalkannya jika tidak digunakan.
Lihatlah kodenya:
referensi konstan = ... const terlihat = (terlihat) => { console.log(`Ini ${terlihat}`); } gunakanEffect(() => { jika (!ref) { kembali; } pengamat.saat ini = new IntersectionObserver( (entri) => { if (!entries[0].isIntersectioning) { terlihat (benar); } kalau tidak { terlihat (salah); } }, { rootMargin: `-${header.height}px` }, ); pengamat.saat ini.mengamati(ref); }, [ref]);
Kode di atas terlihat baik-baik saja. Namun, apa yang terjadi pada pengamat setelah komponen dibongkar? Bagaimana kita mengatasi masalah ini? Cukup gunakan metode disconnect
:
referensi konstan = ... const terlihat = (terlihat) => { console.log(`Ini ${terlihat}`); } gunakanEffect(() => { jika (!ref) { kembali; } pengamat.saat ini = new IntersectionObserver( (entri) => { if (!entries[0].isIntersectioning) { terlihat (benar); } kalau tidak { terlihat (salah); } }, { rootMargin: `-${header.height}px` }, ); pengamat.saat ini.mengamati(ref); return() => pengamat.saat ini?.disconnect(); }, [ref]);
Menambahkan objek ke Window adalah kesalahan umum. Dalam beberapa skenario, mungkin sulit untuk menemukannya, terutama ketika menggunakan kata kunci this
dalam konteks Eksekusi Window. Lihatlah contoh berikut:
fungsi addElement(elemen) { if (!ini.tumpukan) { ini.tumpukan = { elemen: [] } } this.stack.elements.push(elemen); }
Tampaknya tidak berbahaya, tetapi itu tergantung pada konteks mana Anda memanggil addElement
. Jika Anda memanggil addElement dari Konteks Jendela, heap akan bertambah.
Masalah lain mungkin salah dalam mendefinisikan variabel global:
var a = 'contoh 1'; // Cakupan terbatas pada tempat di mana var dibuat b = 'contoh 2'; // Ditambahkan ke objek Window
Untuk mencegah masalah ini Anda dapat menggunakan mode ketat:
"gunakan yang ketat"
Dengan menggunakan mode ketat, Anda memberi isyarat kepada kompiler JavaScript bahwa Anda ingin melindungi diri Anda dari perilaku ini. Anda masih dapat menggunakan Window saat diperlukan. Namun, Anda harus menggunakannya secara eksplisit.
Bagaimana mode ketat memengaruhi contoh kita sebelumnya:
Untuk fungsi addElement
, this
tidak terdefinisi ketika dipanggil dari lingkup global
Jika Anda tidak menentukan const | let | var
pada suatu variabel, Anda akan mendapatkan kesalahan berikut:
Kesalahan Referensi yang Tidak Tertangkap: b tidak ditentukan
Node DOM juga tidak kebal terhadap kebocoran memori. Kita perlu berhati-hati untuk tidak menyimpan referensi mengenai hal tersebut. Jika tidak, pemulung tidak akan bisa membersihkannya karena masih bisa diakses.
Peragakan dengan sepotong kecil kode:
elemen const = []; const daftar = document.getElementById('daftar'); fungsi addElement() { // membersihkan node daftar.innerHTML = ''; const pElement= dokumen.createElement('p'); elemen const = document.createTextNode(`menambahkan elemen ${elements.length}`); pElement.appendChild(elemen); daftar.appendChild(pElement); elemen.push(pElement); } document.getElementById('addElement').onclick = addElement;
Perhatikan bahwa fungsi addElement
menghapus daftar p
dan menambahkan elemen baru ke dalamnya sebagai elemen turunan. Elemen yang baru dibuat ini ditambahkan ke array elements
.
Saat addElement
dijalankan lagi, elemen tersebut akan dihapus dari daftar p
, tetapi tidak cocok untuk pengumpulan sampah karena disimpan dalam array elements
.
Kami memantau fungsi tersebut setelah menjalankannya beberapa kali:
Lihat bagaimana node disusupi pada gambar di atas. Lalu bagaimana cara mengatasi masalah ini? Menghapus array elements
akan membuatnya memenuhi syarat untuk pengumpulan sampah.
Pada artikel ini, kita telah melihat cara paling umum terjadinya kebocoran memori. Jelas sekali bahwa JavaScript sendiri tidak membocorkan memori. Sebaliknya, hal ini disebabkan oleh retensi memori yang tidak disengaja dari pihak pengembang. Selama kodenya bersih dan kita tidak lupa membersihkannya sendiri, kebocoran tidak akan terjadi.
Memahami cara kerja memori dan pengumpulan sampah di JavaScript adalah suatu keharusan. Beberapa pengembang mempunyai pemahaman yang salah bahwa karena ini otomatis, mereka tidak perlu khawatir tentang masalah ini.