การเข้าสู่หลักสูตรความเชี่ยวชาญส่วนหน้า (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); กลับ (กำลังโหลด...
- - ส่งออกตัวจับเวลาเริ่มต้น;
เมื่อมองแวบแรกดูเหมือนว่าจะไม่มีปัญหา ไม่ต้องกังวล มาสร้างส่วนประกอบอื่นที่ทริกเกอร์ตัวจับเวลานี้และวิเคราะห์ประสิทธิภาพของหน่วยความจำกันดีกว่า
นำเข้าปฏิกิริยา { useState } จาก 'ปฏิกิริยา'; นำเข้าสไตล์จาก '../styles/Home.module.css' นำเข้าตัวจับเวลาจาก '../components/Timer'; ส่งออกฟังก์ชันเริ่มต้น Home() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(เท็จ); กลับ ({showTimer ? ( <ตัวจับเวลา cicles={10} onFinish={onFinish} /> - <ปุ่ม onClick={() => setShowTimer(true)}> ลองอีกครั้ง ปุ่ม> -
- -
หลังจากคลิกปุ่ม 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 เป็นสิ่งจำเป็น นักพัฒนาซอฟต์แวร์บางรายเข้าใจผิดว่าเนื่องจากเป็นไปโดยอัตโนมัติ พวกเขาจึงไม่จำเป็นต้องกังวลเกี่ยวกับปัญหานี้