
การเข้าสู่หลักสูตรความเชี่ยวชาญส่วนหน้า (vue): การเข้าสู่การเรียนรู้ JavaScript ไม่ได้จัดให้มีการดำเนินการจัดการหน่วยความจำใดๆ แต่หน่วยความจำได้รับการจัดการโดย JavaScript VM ผ่านกระบวนการเรียกคืนหน่วยความจำที่เรียกว่า การรวบรวมขยะ
เนื่องจากเราไม่สามารถบังคับเก็บขยะได้ เราจะรู้ได้อย่างไรว่ามันได้ผล? เรารู้เรื่องนี้มากแค่ไหน?
การดำเนินการสคริปต์ถูกหยุดชั่วคราวในระหว่างกระบวนการนี้
มันปล่อยหน่วยความจำสำหรับทรัพยากรที่ไม่สามารถเข้าถึงได้
มันไม่แน่นอน
มันไม่ได้ตรวจสอบหน่วยความจำทั้งหมดพร้อมกัน แต่ทำงานในหลายรอบ
ไม่สามารถคาดเดาได้แต่จะดำเนินการเมื่อจำเป็น
หมายความว่าไม่จำเป็นต้องกังวลเกี่ยวกับปัญหาการจัดสรรทรัพยากรและหน่วยความจำใช่ไหม หากเราไม่ระวังหน่วยความจำรั่วอาจเกิดขึ้นได้

หน่วยความจำรั่วคือบล็อกของหน่วยความจำที่จัดสรรซึ่งซอฟต์แวร์ไม่สามารถเรียกคืนได้
Javascript มีตัวรวบรวมขยะ แต่นั่นไม่ได้หมายความว่าเราสามารถหลีกเลี่ยงการรั่วไหลของหน่วยความจำได้ เพื่อให้มีสิทธิ์ในการรวบรวมขยะ จะต้องไม่อ้างอิงออบเจ็กต์ที่อื่น หากคุณมีการอ้างอิงถึงทรัพยากรที่ไม่ได้ใช้ การดำเนินการนี้จะป้องกันไม่ให้ทรัพยากรเหล่านั้นถูกเรียกคืน นี้เรียกว่า การเก็บความทรงจำโดยไม่รู้ตัว
หน่วยความจำที่รั่วอาจทำให้ตัวรวบรวมขยะทำงานบ่อยขึ้น เนื่องจากกระบวนการนี้จะป้องกันไม่ให้สคริปต์ทำงาน จึงอาจทำให้โปรแกรมของเราค้าง หากเกิดความล่าช้าดังกล่าว ผู้ใช้ที่จู้จี้จุกจิกจะสังเกตเห็นอย่างแน่นอนว่าหากพวกเขาไม่พอใจ ผลิตภัณฑ์ก็จะออฟไลน์เป็นเวลานาน ที่ร้ายแรงกว่านั้นคืออาจทำให้แอปพลิเคชันทั้งหมดขัดข้องซึ่งก็คือ gg
จะป้องกันหน่วยความจำรั่วได้อย่างไร สิ่งสำคัญคือ เราควรหลีกเลี่ยงการเก็บรักษาทรัพยากรที่ไม่จำเป็น มาดูสถานการณ์ทั่วไปบางประการกัน
เมธอด setInterval() เรียกใช้ฟังก์ชันซ้ำๆ หรือดำเนินการส่วนของโค้ด โดยมีการหน่วงเวลาคงที่ระหว่างการโทรแต่ละครั้ง โดยส่งคืน ID ช่วงเวลา ID ระบุช่วงเวลาโดยไม่ซ้ำกัน ดังนั้นคุณจึงสามารถลบออกได้ในภายหลังโดยการเรียก clearInterval()
เราสร้างส่วนประกอบที่เรียกใช้ฟังก์ชันเรียกกลับเพื่อระบุว่าเสร็จสิ้นหลังจากจำนวนลูป x จำนวน ฉันใช้ React ในตัวอย่างนี้ แต่ใช้งานได้กับเฟรมเวิร์ก FE ใดก็ได้
นำเข้าปฏิกิริยา { useRef } จาก 'ปฏิกิริยา';
const Timer = ({ cicles, onFinish }) => {
const currentCicles = useRef (0);
setInterval(() => {
ถ้า (currentCicles.current >= cicles) {
onFinish();
กลับ;
-
currentCicles.ปัจจุบัน++;
}, 500);
กลับ (
<p>กำลังโหลด...</p>
-
-
ส่งออกตัวจับเวลาเริ่มต้น;เมื่อมองแวบแรกดูเหมือนว่าจะไม่มีปัญหา ไม่ต้องกังวล มาสร้างส่วนประกอบอื่นที่ทริกเกอร์ตัวจับเวลานี้และวิเคราะห์ประสิทธิภาพของหน่วยความจำกันดีกว่า
นำเข้าปฏิกิริยา { useState } จาก 'ปฏิกิริยา';
นำเข้าสไตล์จาก '../styles/Home.module.css'
นำเข้าตัวจับเวลาจาก '../components/Timer';
ส่งออกฟังก์ชันเริ่มต้น Home() {
const [showTimer, setShowTimer] = useState();
const onFinish = () => setShowTimer(เท็จ);
กลับ (
<p className={styles.container}>
{showTimer ? (
<ตัวจับเวลา cicles={10} onFinish={onFinish} />
-
<ปุ่ม onClick={() => setShowTimer(true)}>
ลองอีกครั้ง
</ปุ่ม>
-
</p>
-
- หลังจากคลิกปุ่ม Retry ไม่กี่ครั้ง นี่คือผลลัพธ์ของการใช้ Chrome Dev Tools เพื่อรับการใช้หน่วยความจำ:

เมื่อเราคลิกปุ่มลองใหม่ เราจะเห็นว่ามีการจัดสรรหน่วยความจำเพิ่มมากขึ้นเรื่อยๆ ซึ่งหมายความว่าหน่วยความจำที่จัดสรรไว้ก่อนหน้านี้ยังไม่ได้รับการเผยแพร่ ตัวจับเวลายังคงทำงานอยู่แทนที่จะถูกเปลี่ยน
วิธีแก้ปัญหานี้? ค่าที่ส่งคืนของ setInterval คือ ID ช่วงเวลา ซึ่งเราสามารถใช้เพื่อยกเลิกช่วงเวลานี้ได้ ในกรณีนี้ เราสามารถเรียก clearInterval หลังจากที่คอมโพเนนต์ถูกยกเลิกการโหลดแล้ว
useEffect(() => {
const IntervalId = setInterval(() => {
ถ้า (currentCicles.current >= cicles) {
onFinish();
กลับ;
-
currentCicles.ปัจจุบัน++;
}, 500);
return () => clearInterval(intervalId);
-บางครั้งเป็นเรื่องยากที่จะพบปัญหานี้เมื่อเขียนโค้ด วิธีที่ดีที่สุดคือการสรุปส่วนประกอบต่างๆ
เมื่อใช้ React ที่นี่ เราสามารถรวมตรรกะทั้งหมดนี้ไว้ใน Hook แบบกำหนดเองได้
นำเข้า { useEffect } จาก 'ตอบสนอง';
ส่งออก const useTimeout = (refreshCycle = 100, โทรกลับ) => {
useEffect(() => {
ถ้า (รีเฟรชวงจร <= 0) {
setTimeout (โทรกลับ, 0);
กลับ;
-
const IntervalId = setInterval(() => {
โทรกลับ ();
}, รีเฟรชไซเคิล);
return () => clearInterval(intervalId);
}, [refreshCycle, setInterval, clearInterval]);
-
ส่งออก useTimeout เริ่มต้น; ตอนนี้เมื่อใดก็ตามที่คุณต้องการใช้ setInterval คุณสามารถทำได้:
const handleTimeout = () => ...; useTimeout(100, จัดการหมดเวลา);
ตอนนี้คุณสามารถใช้ useTimeout Hook นี้ได้โดยไม่ต้องกังวลว่าหน่วยความจำจะรั่ว ซึ่งเป็นข้อดีของนามธรรมด้วย
Web API มี Listener เหตุการณ์จำนวนมาก ก่อนหน้านี้ เราได้กล่าวถึง setTimeout ตอนนี้เรามาดูที่ addEventListener
ในตัวอย่างนี้ เราสร้างฟังก์ชันแป้นพิมพ์ลัด เนื่องจากเรามีฟังก์ชันที่แตกต่างกันในแต่ละหน้า ฟังก์ชันคีย์ลัดที่แตกต่างกันจะถูกสร้างขึ้น
ฟังก์ชั่น homeShortcuts ({ คีย์}) {
ถ้า (คีย์ === 'E') {
console.log('แก้ไขวิดเจ็ต')
-
-
// เมื่อผู้ใช้ล็อกอินเข้าสู่หน้าแรก เราจะดำเนินการ document.addEventListener('keyup', homeShortcuts);
// ผู้ใช้ทำอะไรบางอย่างแล้วไปที่ฟังก์ชั่นการตั้งค่า settingsShortcuts({ key}) {
ถ้า (คีย์ === 'E') {
console.log('แก้ไขการตั้งค่า')
-
-
// เมื่อผู้ใช้ล็อกอินเข้าสู่หน้าแรก เราจะดำเนินการ document.addEventListener('keyup', settingsShortcuts); ยังคงดูดี ยกเว้นว่า keyup ก่อนหน้านี้ไม่ได้รับการล้างข้อมูลเมื่อดำเนินการ addEventListener ที่สอง แทนที่จะแทนที่ตัวฟัง keyup ของเรา โค้ดนี้จะเพิ่ม callback อีกครั้ง ซึ่งหมายความว่าเมื่อกดปุ่ม จะเรียกใช้ฟังก์ชันสองฟังก์ชัน
เพื่อล้างการโทรกลับก่อนหน้านี้ เราจำเป็นต้องใช้ removeEventListener :
document.removeEventListener('keyup', homeShortcuts);ปรับโครงสร้างโค้ดด้านบนใหม่:
ฟังก์ชั่น homeShortcuts ({ คีย์}) {
ถ้า (คีย์ === 'E') {
console.log('แก้ไขวิดเจ็ต')
-
-
// ผู้ใช้มาถึงบ้านแล้วเราก็ดำเนินการ
document.addEventListener('keyup', homeShortcuts);
// ผู้ใช้ทำบางสิ่งและไปที่การตั้งค่า
การตั้งค่าฟังก์ชั่นทางลัด ({ key}) {
ถ้า (คีย์ === 'E') {
console.log('แก้ไขการตั้งค่า')
-
-
// ผู้ใช้มาถึงบ้านแล้วเราก็ดำเนินการ
document.removeEventListener('keyup', homeShortcuts);
document.addEventListener('keyup', การตั้งค่าทางลัด);ตามหลักการทั่วไป ควรระมัดระวังให้มากเมื่อใช้เครื่องมือจากวัตถุส่วนกลาง
ผู้สังเกตการณ์ เป็นคุณลักษณะ Web API ของเบราว์เซอร์ที่นักพัฒนาจำนวนมากไม่ทราบ วิธีนี้จะมีประสิทธิภาพมากหากคุณต้องการตรวจสอบการเปลี่ยนแปลงในการมองเห็นหรือขนาดขององค์ประกอบ HTML
อินเทอร์เฟซ IntersectionObserver (ส่วนหนึ่งของ Intersection Observer API) จัดให้มีวิธีการสังเกตสถานะจุดตัดขององค์ประกอบเป้าหมายแบบอะซิงโครนัสด้วยองค์ประกอบบรรพบุรุษหรือ viewport เอกสารระดับบนสุด องค์ประกอบบรรพบุรุษและ viewport เรียกว่า root
แม้ว่ามันจะทรงพลัง แต่เราต้องใช้มันด้วยความระมัดระวัง เมื่อคุณสังเกตวัตถุเสร็จแล้ว อย่าลืมยกเลิกเมื่อไม่ได้ใช้งาน
ดูรหัส:
อ้างอิง const = ...
const มองเห็นได้ = (มองเห็นได้) => {
console.log(`คือ ${visible}`);
-
useEffect(() => {
ถ้า (!อ้างอิง) {
กลับ;
-
Observer.current = IntersectionObserver ใหม่ (
(รายการ) => {
ถ้า (!รายการ[0].isIntersecting) {
มองเห็นได้ (จริง);
} อื่น {
การมองเห็น (เท็จ);
-
-
{ rootMargin: `-${header.height}px` },
-
ผู้สังเกตการณ์ current.observe (อ้างอิง);
}, [อ้างอิง]); รหัสด้านบนดูดี อย่างไรก็ตาม จะเกิดอะไรขึ้นกับผู้สังเกตการณ์เมื่อส่วนประกอบถูกยกเลิกการโหลด ส่วนประกอบนั้นไม่ถูกล้างและหน่วยความจำรั่วไหล เราจะแก้ไขปัญหานี้ได้อย่างไร เพียงใช้วิธี disconnect :
อ้างอิง const = ...
const มองเห็นได้ = (มองเห็นได้) => {
console.log(`คือ ${visible}`);
-
useEffect(() => {
ถ้า (!อ้างอิง) {
กลับ;
-
Observer.current = IntersectionObserver ใหม่ (
(รายการ) => {
ถ้า (!รายการ[0].isIntersecting) {
มองเห็นได้ (จริง);
} อื่น {
การมองเห็น (เท็จ);
-
-
{ rootMargin: `-${header.height}px` },
-
ผู้สังเกตการณ์ current.observe (อ้างอิง);
return () => ผู้สังเกตการณ์.current?.disconnect();
}, [อ้างอิง]); การเพิ่มวัตถุลงในหน้าต่างถือเป็นข้อผิดพลาดทั่วไป ในบางสถานการณ์ อาจเป็นเรื่องยากที่จะค้นหา โดยเฉพาะอย่างยิ่งเมื่อใช้คำสำคัญ this ในบริบทการดำเนินการของหน้าต่าง ลองดูตัวอย่างต่อไปนี้:
ฟังก์ชั่น addElement (องค์ประกอบ) {
ถ้า (!this.stack) {
นี่.สแต็ค = {
องค์ประกอบ: []
-
-
this.stack.elements.push (องค์ประกอบ);
- ดูไม่เป็นอันตราย แต่ขึ้นอยู่กับบริบทที่คุณเรียก addElement หากคุณเรียก addElement จากบริบทของหน้าต่าง ฮีปจะขยายใหญ่ขึ้น
ปัญหาอื่นอาจกำหนดตัวแปรส่วนกลางไม่ถูกต้อง:
var a = 'example 1'; // ขอบเขตถูกจำกัดอยู่ที่ตำแหน่งที่สร้าง var b = 'example 2'; // เพิ่มไปยังวัตถุ Window
เพื่อป้องกันปัญหานี้ คุณสามารถใช้โหมดเข้มงวดได้:
"ใช้อย่างเข้มงวด"
เมื่อใช้โหมดเข้มงวด คุณจะส่งสัญญาณไปยังคอมไพลเลอร์ JavaScript ว่าคุณต้องการป้องกันตนเองจากลักษณะการทำงานเหล่านี้ คุณยังคงใช้ Window ได้เมื่อจำเป็น อย่างไรก็ตาม คุณต้องใช้มันในลักษณะที่ชัดเจน
โหมดเข้มงวดส่งผลต่อตัวอย่างก่อนหน้าของเราอย่างไร:
สำหรับฟังก์ชัน addElement this จะไม่ได้กำหนดไว้เมื่อเรียกจากขอบเขตส่วนกลาง
หากคุณไม่ระบุ const | let | var บนตัวแปร คุณจะได้รับข้อผิดพลาดต่อไปนี้:
Uncaught ReferenceError: b ไม่ได้ถูกกำหนดไว้
โหนด DOM ก็ไม่รอดพ้นจากการรั่วไหลของหน่วยความจำเช่นกัน เราต้องระวังที่จะไม่บันทึกการอ้างอิงถึงพวกเขา มิฉะนั้นคนเก็บขยะจะไม่สามารถทำความสะอาดได้เนื่องจากยังสามารถเข้าถึงได้
สาธิตด้วยโค้ดเล็กๆ น้อยๆ:
องค์ประกอบ const = [];
รายการ const = document.getElementById('รายการ');
ฟังก์ชั่น addElement() {
// ทำความสะอาดโหนด
list.innerHTML = '';
const pElement= document.createElement('p');
องค์ประกอบ const = document.createTextNode(`เพิ่มองค์ประกอบ ${elements.length}`);
pElement.appendChild(องค์ประกอบ);
list.appendChild(องค์ประกอบ);
องค์ประกอบ.ผลักดัน(pElement);
-
document.getElementById('addElement').onclick = addElement; โปรดทราบว่าฟังก์ชัน addElement จะล้างรายการ p และเพิ่มองค์ประกอบใหม่ให้เป็นองค์ประกอบลูก องค์ประกอบที่สร้างขึ้นใหม่นี้จะถูกเพิ่มลงในอาร์เรย์ elements
ครั้งถัดไปที่ addElement ถูกดำเนินการ องค์ประกอบจะถูกลบออกจากรายการ p แต่ไม่เหมาะสำหรับการรวบรวมขยะเนื่องจากถูกเก็บไว้ในอาร์เรย์ elements
เราตรวจสอบฟังก์ชั่นหลังจากดำเนินการสองสามครั้ง:
ดูว่าโหนดถูกบุกรุกอย่างไรในภาพหน้าจอด้านบน แล้วจะแก้ไขปัญหานี้อย่างไร? การล้างอาร์เรย์ elements จะทำให้มีสิทธิ์ในการรวบรวมขยะ
ในบทความนี้ เราได้ดูวิธีทั่วไปที่ทำให้หน่วยความจำรั่ว เห็นได้ชัดว่า JavaScript เองไม่ได้ทำให้หน่วยความจำรั่วไหล แต่มันเกิดจากการกักเก็บความทรงจำโดยไม่ได้ตั้งใจในส่วนของนักพัฒนา ตราบใดที่โค้ดสะอาดและเราไม่ลืมทำความสะอาดด้วยตัวเอง การรั่วไหลก็จะไม่เกิดขึ้น
การทำความเข้าใจวิธีการทำงานของหน่วยความจำและการรวบรวมขยะใน JavaScript เป็นสิ่งจำเป็น นักพัฒนาซอฟต์แวร์บางรายเข้าใจผิดว่าเนื่องจากเป็นไปโดยอัตโนมัติ พวกเขาจึงไม่จำเป็นต้องกังวลเกี่ยวกับปัญหานี้