ค้นหาและแก้ไขการรั่วไหลของหน่วยความจำที่ยุ่งยากซึ่งเกิดจากหน้าต่างที่ถอดออก
หน่วยความจำรั่วไหลใน JavaScript คืออะไร
การรั่วไหลของหน่วยความจำคือปริมาณหน่วยความจำที่แอปพลิเคชันใช้เมื่อเวลาผ่านไปเพิ่มขึ้นโดยไม่ได้ตั้งใจ ใน JavaScript การรั่วไหลของหน่วยความจำเกิดขึ้นเมื่อไม่จำเป็นต้องใช้ออบเจ็กต์อีกต่อไป แต่ยังคงมีการอ้างอิงโดยฟังก์ชันหรือออบเจ็กต์อื่นๆ แหล่งอ้างอิงเหล่านี้ช่วยป้องกันไม่ให้พนักงานเก็บขยะนำวัตถุที่ไม่จำเป็นกลับมาใช้งาน
หน้าที่ของผู้เก็บขยะคือการระบุและเรียกคืนวัตถุที่ไม่สามารถเข้าถึงได้จากแอปพลิเคชันอีกต่อไป ฟีเจอร์นี้ทำงานได้แม้ว่าวัตถุจะอ้างอิงตัวเองหรืออ้างอิงกันและกันเป็นวงจร เมื่อแอปพลิเคชันเข้าถึงกลุ่มวัตถุได้จนไม่มีการอ้างอิงเหลืออยู่แล้ว ก็อาจเป็นขยะที่เก็บได้
let A = {};
console.log(A); // local variable reference
let B = {A}; // B.A is a second reference to A
A = null; // unset local variable reference
console.log(B.A); // A can still be referenced by B
B.A = null; // unset B's reference to A
// No references to A are left. It can be garbage collected.
การรั่วไหลของหน่วยความจำที่มีความซับซ้อนเป็นพิเศษจะเกิดขึ้นเมื่อแอปพลิเคชันอ้างอิงออบเจ็กต์ที่มีวงจรชีวิตของตนเอง เช่น องค์ประกอบ DOM หรือหน้าต่างป๊อปอัป มีโอกาสที่ออบเจ็กต์ประเภทนี้อาจไม่มีการใช้งานโดยที่แอปพลิเคชันไม่ทราบ ซึ่งหมายความว่าโค้ดของแอปพลิเคชันอาจมีการอ้างอิงไปยังออบเจ็กต์ที่อาจเป็นขยะที่รวบรวมได้เพียงอย่างเดียวเท่านั้น
หน้าต่างที่ถูกแยกออกคืออะไร
ในตัวอย่างต่อไปนี้ แอปพลิเคชันดูภาพสไลด์จะมีปุ่มสำหรับเปิดและปิดป๊อปอัปบันทึกของผู้นำเสนอ สมมติว่าผู้ใช้คลิกแสดงบันทึก แล้วปิดหน้าต่างป๊อปอัปโดยตรงแทนที่จะคลิกปุ่มซ่อนบันทึก ตัวแปร notesWindow
จะยังคงมีการอ้างอิงไปยังป๊อปอัปที่เข้าถึงได้แม้ว่าจะไม่ได้ใช้ป๊อปอัปนั้นแล้วก็ตาม
<button id="show">Show Notes</button>
<button id="hide">Hide Notes</button>
<script type="module">
let notesWindow;
document.getElementById('show').onclick = () => {
notesWindow = window.open('/presenter-notes.html');
};
document.getElementById('hide').onclick = () => {
if (notesWindow) notesWindow.close();
};
</script>
นี่คือตัวอย่างของหน้าต่างที่แยกออก หน้าต่างป๊อปอัปปิดลง แต่โค้ดของเรามีการอ้างอิงมายัง หน้าต่างที่ป้องกันไม่ให้เบราว์เซอร์ทำลายและเรียกคืนหน่วยความจำนั้น
เมื่อหน้าเว็บเรียกใช้ window.open()
เพื่อสร้างหน้าต่างหรือแท็บใหม่ของเบราว์เซอร์ ระบบจะส่งออบเจ็กต์ Window
กลับมาแทนหน้าต่างหรือแท็บ แม้ว่าหน้าต่างดังกล่าวจะปิดไปแล้วหรือผู้ใช้ออกจากหน้าต่างไป ออบเจ็กต์ Window
ที่ส่งคืนจาก window.open()
จะยังคงสามารถใช้เพื่อเข้าถึงข้อมูลเกี่ยวกับหน้าต่างนั้นได้ นี่เป็นหน้าต่างที่แยกออกมาประเภทหนึ่ง: เนื่องจากโค้ด JavaScript ยังมีโอกาสเข้าถึงพร็อพเพอร์ตี้ในออบเจ็กต์ Window
ที่ปิดอยู่ จึงต้องเก็บไว้ในหน่วยความจำ หากหน้าต่างมีออบเจ็กต์ JavaScript หรือ iframe จำนวนมาก หน่วยความจำนั้นจะไม่สามารถเรียกคืนได้จนกว่าจะไม่มีการอ้างอิง JavaScript ที่เหลืออยู่ไปยังคุณสมบัติของหน้าต่าง
ปัญหาเดียวกันนี้ยังอาจเกิดขึ้นได้เมื่อใช้องค์ประกอบ <iframe>
iframe จะทำงานเหมือนหน้าต่างที่ซ้อนกันที่มีเอกสาร และพร็อพเพอร์ตี้ contentWindow
ของ iframe จะให้สิทธิ์เข้าถึงออบเจ็กต์ Window
ที่มีอยู่ ซึ่งเหมือนกับค่าที่ window.open()
แสดงผล โค้ด JavaScript จะเก็บการอ้างอิงไปยัง contentWindow
หรือ contentDocument
ของ iframe ได้แม้ว่าจะนำ iframe ออกจาก DOM หรือการเปลี่ยนแปลง URL แล้ว ซึ่งทำให้ระบบเก็บเอกสารขยะเนื่องจากยังเข้าถึงพร็อพเพอร์ตี้ได้อยู่
ในกรณีที่การอ้างอิงไปยัง document
ภายในหน้าต่างหรือ iframe ถูกเก็บจาก JavaScript เอกสารนั้นจะถูกเก็บไว้ในหน่วยความจำแม้ว่าหน้าต่างหรือ iframe นั้นจะไปที่ URL ใหม่ก็ตาม ปัญหานี้จะสร้างปัญหาอย่างมากเมื่อ JavaScript ที่ระงับการอ้างอิงนั้นตรวจไม่พบว่าหน้าต่าง/เฟรมได้ไปยัง URL ใหม่ เนื่องจากไม่ทราบว่าเมื่อใดที่ JavaScript จะกลายเป็นข้อมูลอ้างอิงสุดท้ายที่เก็บเอกสารไว้ในหน่วยความจำ
หน้าต่างที่ถอดแยกทำให้หน่วยความจำรั่วไหลได้อย่างไร
เมื่อทำงานกับหน้าต่างและ iframe ในโดเมนเดียวกับหน้าหลัก จึงเป็นเรื่องปกติที่จะรับฟังเหตุการณ์หรือเข้าถึงพร็อพเพอร์ตี้ข้ามขอบเขตของเอกสาร ตัวอย่างเช่น เรามาดูหน้ารูปแบบใหม่
ในตัวอย่างโปรแกรมดูงานนำเสนอในช่วงต้นของคู่มือนี้กัน ผู้ดูจะเปิดหน้าต่างที่ 2
สำหรับแสดงบันทึกของผู้บรรยาย หน้าต่างบันทึกของผู้บรรยายจะคอยฟัง click
เหตุการณ์เป็นจุดเริ่มการเลื่อนไปยังสไลด์ถัดไป หากผู้ใช้ปิดหน้าต่างบันทึกนี้ JavaScript ที่ทำงานในหน้าต่างระดับบนสุดเดิมจะยังคงมีสิทธิ์เข้าถึงเอกสารบันทึกของผู้บรรยายอย่างเต็มรูปแบบได้ ดังนี้
<button id="notes">Show Presenter Notes</button>
<script type="module">
let notesWindow;
function showNotes() {
notesWindow = window.open('/presenter-notes.html');
notesWindow.document.addEventListener('click', nextSlide);
}
document.getElementById('notes').onclick = showNotes;
let slide = 1;
function nextSlide() {
slide += 1;
notesWindow.document.title = `Slide ${slide}`;
}
document.body.onclick = nextSlide;
</script>
สมมติว่าเราปิดหน้าต่างเบราว์เซอร์ที่สร้างโดย showNotes()
ด้านบน ไม่มีตัวแฮนเดิลเหตุการณ์ที่ตรวจพบว่ามีการปิดหน้าต่าง จึงไม่มีข้อมูลที่แจ้งโค้ดว่าควรทำความสะอาดการอ้างอิงไปยังเอกสาร ฟังก์ชัน nextSlide()
ยังคงเป็นแบบ "เผยแพร่" อยู่เนื่องจากมีการเชื่อมโยงเป็นเครื่องจัดการคลิกในหน้าหลักของเรา และ nextSlide
มีการอ้างอิงไปยัง notesWindow
หมายความว่ายังมีการอ้างอิงหน้าต่างอยู่และไม่สามารถเป็นขยะที่รวบรวมได้
มีสถานการณ์อื่นๆ มากมายที่มีการเก็บรักษาข้อมูลอ้างอิงโดยไม่ได้ตั้งใจซึ่งป้องกันไม่ให้หน้าต่างที่แยกออกไปมีสิทธิ์ในการเก็บขยะ เช่น
คุณสามารถลงทะเบียนเครื่องจัดการเหตุการณ์ในเอกสารเริ่มต้นของ iframe ก่อนที่เฟรมจะไปยัง URL ที่ต้องการ ส่งผลให้มีการอ้างอิงเอกสารโดยไม่ตั้งใจและ iframe จะยังคงอยู่หลังจากลบการอ้างอิงอื่นๆ แล้ว
เอกสารที่มีหน่วยความจำจำนวนมากซึ่งโหลดในหน้าต่างหรือ iframe อาจเก็บไว้ในหน่วยความจำโดยไม่ได้ตั้งใจเป็นเวลานานหลังจากไปยัง URL ใหม่ ปัญหานี้มักเกิดจากการที่หน้าหลักยังคงอ้างอิงถึงเอกสารดังกล่าวไว้เพื่อให้นำ Listener ออก
เมื่อส่งออบเจ็กต์ JavaScript ไปยังหน้าต่างหรือ iframe อื่น ห่วงโซ่ต้นแบบของออบเจ็กต์จะรวมถึงการอ้างอิงสภาพแวดล้อมที่สร้างขึ้น รวมถึงหน้าต่างที่สร้างออบเจ็กต์ดังกล่าว ซึ่งหมายความว่าการหลีกเลี่ยงการอ้างอิงไปยังวัตถุจากหน้าต่างอื่นก็มีความสำคัญไม่แพ้กัน เนื่องจากการหลีกเลี่ยงการอ้างอิงไว้ที่หน้าต่างนั้นเอง
index.html:
<script> let currentFiles; function load(files) { // this retains the popup: currentFiles = files; } window.open('upload.html'); </script>
upload.html:
<input type="file" id="file" /> <script> file.onchange = () => { parent.load(file.files); }; </script>
การตรวจจับการรั่วไหลของหน่วยความจำที่เกิดจากหน้าต่างที่ถอดออก
การติดตามการรั่วไหลของหน่วยความจำอาจเป็นเรื่องยุ่งยาก การสร้างสำเนาของปัญหาเหล่านี้ซ้ำๆ มักจะทำได้ยาก โดยเฉพาะเมื่อมีเอกสารหลายฉบับหรือหลายหน้าต่างมาเกี่ยวข้อง การตรวจสอบข้อมูลอ้างอิงที่รั่วไหลที่อาจเกิดขึ้นอาจสร้างการอ้างอิงเพิ่มเติมเพื่อป้องกันไม่ให้ออบเจ็กต์ที่ตรวจสอบกลายเป็นขยะ ซึ่งเพื่อทำให้ทุกอย่างซับซ้อนขึ้น ด้วยเหตุนี้ คุณจึงควรเริ่มต้นด้วยเครื่องมือ ที่หลีกเลี่ยงการแนะนำความเป็นไปได้นี้โดยเฉพาะ
จุดที่ดีในการเริ่มต้นแก้ไขข้อบกพร่องเกี่ยวกับหน่วยความจำคือถ่ายฮีพสแนปชอต วิธีนี้ทำให้ดูหน่วยความจำที่แอปพลิเคชันใช้อยู่ในปัจจุบัน ซึ่งก็คือออบเจ็กต์ทั้งหมดที่สร้างขึ้นแต่ยังไม่ได้มีการเก็บข้อมูลขยะ สแนปชอตฮีปมีข้อมูลที่มีประโยชน์เกี่ยวกับออบเจ็กต์ รวมถึงขนาดของออบเจ็กต์และรายการตัวแปรและการปิดที่อ้างอิงออบเจ็กต์เหล่านั้น
หากต้องการบันทึกสแนปชอตฮีป ให้ไปที่แท็บหน่วยความจำในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome แล้วเลือกสแนปชอตฮีปในรายการประเภทการสร้างโปรไฟล์ที่ใช้ได้ เมื่อบันทึกเสร็จสิ้นแล้ว มุมมองสรุปจะแสดงออบเจ็กต์ปัจจุบันในหน่วยความจำโดยจัดกลุ่มตามตัวสร้าง
การวิเคราะห์ฮีพดัมพ์อาจเป็นงานที่ยุ่งยาก และการหาข้อมูลที่ถูกต้องเพื่อใช้ในการแก้ไขข้อบกพร่องก็ค่อนข้างยาก วิศวกร Chromium yossik@ และ peledni@ ได้พัฒนาเครื่องมือ Heap Cleaner แบบสแตนด์อโลนที่สามารถช่วยไฮไลต์โหนดที่เฉพาะเจาะจง เช่น หน้าต่างที่แยกออกมา การเรียกใช้ฮีป Cleaner ในการติดตามจะนำข้อมูลที่ไม่จำเป็นอื่นๆ ออกจากกราฟการเก็บรักษา ซึ่งจะทำให้การติดตามดูสะอาดตาและอ่านง่ายขึ้นมาก
วัดหน่วยความจำแบบเป็นโปรแกรม
ฮีปสแนปชอตมีรายละเอียดในระดับสูงและเหมาะสำหรับการระบุตำแหน่งที่เกิดการรั่วไหล
แต่การบันทึกฮีปสแนปชอตเป็นกระบวนการที่ต้องดำเนินการเอง อีกวิธีหนึ่งในการตรวจสอบการรั่วไหลของหน่วยความจำคือการรับขนาดฮีป JavaScript ที่ใช้อยู่ในปัจจุบันจาก performance.memory
API โดยทำดังนี้
performance.memory
API จะให้ข้อมูลเกี่ยวกับขนาดฮีปของ JavaScript เท่านั้น ซึ่งหมายความว่าไม่รวมหน่วยความจำที่เอกสารและทรัพยากรของป๊อปอัปใช้ เพื่อให้เห็นภาพทั้งหมด เราต้องใช้ performance.measureUserAgentSpecificMemory()
API ใหม่ที่กำลังทดลองใช้ใน Chrome
วิธีแก้ปัญหาสำหรับหน้าต่างรั่วซึม
กรณีที่พบบ่อยที่สุด 2 กรณีที่หน้าต่างที่แยกออกมาทำให้หน่วยความจำรั่วไหลคือเมื่อเอกสารหลักเก็บการอ้างอิงไปยังป๊อปอัปที่ปิดไปแล้วหรือ iframe ที่ถูกนำออก และเมื่อการนำทางที่ไม่คาดคิดของหน้าต่างหรือ iframe ทำให้ตัวแฮนเดิลเหตุการณ์ไม่ถูกยกเลิกการลงทะเบียน
ตัวอย่าง: การปิดป๊อปอัป
ในตัวอย่างต่อไปนี้ จะใช้ปุ่ม 2 ปุ่มในการเปิดและปิดหน้าต่างป๊อปอัป ระบบจะจัดเก็บการอ้างอิงไปยังหน้าต่างป๊อปอัปที่เปิดอยู่ไว้ในตัวแปร เพื่อให้ปุ่มปิดป๊อปอัปทำงานได้
<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
};
</script>
เมื่อมองเผินๆ ดูเหมือนว่าโค้ดข้างต้นจะหลีกเลี่ยงข้อผิดพลาดทั่วไปได้ กล่าวคือ ไม่มีการเก็บรักษาการอ้างอิงเอกสารของป๊อปอัปไว้ และไม่มีการบันทึกเครื่องจัดการเหตุการณ์ในหน้าต่างป๊อปอัป อย่างไรก็ตาม เมื่อมีการคลิกปุ่มเปิดป๊อปอัป ตัวแปร popup
จะอ้างอิงหน้าต่างที่เปิดอยู่ และตัวแปรนั้นจะเข้าถึงได้จากขอบเขตของตัวแฮนเดิลคลิกของปุ่มปิดป๊อปอัป เว้นแต่จะมีการมอบหมาย popup
ใหม่หรือนำเครื่องจัดการการคลิกออก การอ้างอิงภายในของตัวแฮนเดิลดังกล่าวไปยัง popup
จะทำให้ระบบรวบรวมข้อมูลขยะไม่ได้
วิธีแก้ปัญหา: ยกเลิกการตั้งค่าการอ้างอิง
ตัวแปรที่อ้างอิงหน้าต่างอื่นหรือเอกสารนั้นทำให้ระบบเก็บตัวแปรนั้นไว้ในหน่วยความจำ เนื่องจากออบเจ็กต์ใน JavaScript จะเป็นการอ้างอิงเสมอ การกำหนดค่าใหม่ให้กับตัวแปรจึงเป็นการนำการอ้างอิงเหล่านั้นไปยังออบเจ็กต์ต้นฉบับ หากต้องการ "ยกเลิกการตั้งค่า" การอ้างอิงไปยังออบเจ็กต์ เราสามารถกำหนดตัวแปรเหล่านั้นใหม่ให้กับค่า null
ได้
หากนำไปใช้กับตัวอย่างป๊อปอัปก่อนหน้านี้ เราจะปรับเปลี่ยนแฮนเดิลของปุ่มปิดให้ "ยกเลิกการตั้งค่า" การอ้างอิงเป็นหน้าต่างป๊อปอัปได้
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
popup = null;
};
วิธีนี้ช่วย แต่เผยให้เห็นปัญหาเฉพาะหน้าต่างที่สร้างขึ้นโดยใช้ open()
ซึ่งก็คือจะเกิดอะไรขึ้นหากผู้ใช้ปิดหน้าต่างแทนที่จะคลิกปุ่มปิดที่กำหนดเอง นอกจากนี้ จะเกิดอะไรขึ้นหากผู้ใช้เริ่ม
เรียกดูเว็บไซต์อื่นๆ ในหน้าต่างที่เราเปิด แม้ว่าเดิมจะดูเหมือนว่าเพียงพอต่อการยกเลิกการตั้งค่าการอ้างอิง popup
เมื่อคลิกปุ่มปิด แต่หน่วยความจำก็ยังรั่วไหลเมื่อผู้ใช้ไม่ได้ใช้ปุ่มดังกล่าวเพื่อปิดหน้าต่าง ในการแก้ปัญหานี้ คุณจะต้องตรวจหากรณีเหล่านี้เพื่อยกเลิกการอ้างอิงที่ค้างเอาไว้เมื่อเกิดขึ้น
วิธีแก้ปัญหา: ตรวจสอบและกำจัด
ในหลายๆ สถานการณ์ JavaScript ที่มีหน้าที่เปิดหน้าต่างหรือสร้างเฟรมก็ไม่ได้ควบคุมอายุการใช้งานของ JavaScript แต่เพียงผู้เดียว ผู้ใช้สามารถปิดป๊อปอัปได้ หรือการไปยังเอกสารใหม่อาจทำให้เอกสารที่ก่อนหน้านี้อยู่ในหน้าต่างหรือเฟรมถูกถอดออก ในทั้ง 2 กรณี เบราว์เซอร์จะเริ่มการทำงานของเหตุการณ์ pagehide
เพื่อส่งสัญญาณว่ามีการยกเลิกการโหลดเอกสาร
คุณใช้เหตุการณ์ pagehide
เพื่อตรวจหาหน้าต่างที่ปิดอยู่และออกจากเอกสารปัจจุบันได้ อย่างไรก็ตาม มีข้อควรระวังที่สำคัญอย่างหนึ่งคือ หน้าต่างและ iframe ที่สร้างขึ้นใหม่ทั้งหมดจะมีเอกสารเปล่า แล้วไปยัง URL ที่ให้มาแบบอะซิงโครนัส หากมี ด้วยเหตุนี้ เหตุการณ์ pagehide
เริ่มต้นจึงเริ่มทำงานไม่นานหลังจากที่สร้างหน้าต่างหรือเฟรมก่อนที่เอกสารเป้าหมายจะโหลด เนื่องจากโค้ดการล้างข้อมูลข้อมูลอ้างอิงของเราจะต้องทำงานเมื่อมีการยกเลิกการโหลดเอกสาร target เราจึงจำเป็นต้องละเว้นเหตุการณ์ pagehide
แรกนี้ วิธีดังกล่าวมีเทคนิคหลายวิธี วิธีที่ง่ายที่สุดคือละเว้นเหตุการณ์ซ่อนหน้าที่มาจาก URL about:blank
ของเอกสารเริ่มต้น ตัวอย่างป๊อปอัปของเราจะมีลักษณะดังนี้
let popup;
open.onclick = () => {
popup = window.open('/login.html');
// listen for the popup being closed/exited:
popup.addEventListener('pagehide', () => {
// ignore initial event fired on "about:blank":
if (!popup.location.host) return;
// remove our reference to the popup window:
popup = null;
});
};
โปรดทราบว่าเทคนิคนี้จะใช้งานได้เฉพาะกับหน้าต่างและเฟรมที่มีต้นทางที่มีประสิทธิภาพเดียวกันกับหน้าหลักที่โค้ดของเราทำงานอยู่เท่านั้น เมื่อโหลดเนื้อหาจากแหล่งที่มาอื่น ทั้งเหตุการณ์ location.host
และ pagehide
จะใช้งานไม่ได้เนื่องจากเหตุผลด้านความปลอดภัย แม้ว่าโดยทั่วไปวิธีที่ดีที่สุดคือหลีกเลี่ยงการเก็บการอ้างอิงไปยังต้นทางอื่นๆ แต่ในบางกรณีที่จำเป็น ก็ตรวจสอบพร็อพเพอร์ตี้ window.closed
หรือ frame.isConnected
ได้ เมื่อคุณสมบัติเหล่านี้เปลี่ยนไปเพื่อระบุหน้าต่างที่ปิดอยู่หรือมีการนำ iframe ออก คุณควรยกเลิกการตั้งค่าการอ้างอิงใดๆ ที่มี
let popup = window.open('https://example.com');
let timer = setInterval(() => {
if (popup.closed) {
popup = null;
clearInterval(timer);
}
}, 1000);
วิธีแก้ปัญหา: ใช้ WeakRef
เมื่อเร็วๆ นี้ JavaScript ได้รองรับวิธีใหม่ในการอ้างอิงออบเจ็กต์ซึ่งทำให้เกิดการรวบรวมขยะที่เรียกว่า WeakRef
WeakRef
ที่สร้างขึ้นสำหรับออบเจ็กต์ไม่ใช่การอ้างอิงโดยตรง แต่เป็นออบเจ็กต์แยกต่างหากซึ่งมีเมธอด .deref()
พิเศษซึ่งแสดงผลการอ้างอิงไปยังออบเจ็กต์ตราบใดที่ไม่มีการเก็บข้อมูลขยะ เมื่อใช้ WeakRef
คุณอาจเข้าถึงค่าปัจจุบันของหน้าต่างหรือเอกสารได้ แต่ยังคงอนุญาตให้ระบบเก็บขยะ แทนที่จะเก็บการอ้างอิงไปยังหน้าต่างที่ต้องยกเลิกการตั้งค่าด้วยตนเองเพื่อตอบสนองต่อเหตุการณ์ เช่น pagehide
หรือพร็อพเพอร์ตี้ เช่น window.closed
ระบบจะรับสิทธิ์เข้าถึงหน้าต่างดังกล่าวตามที่จําเป็น เมื่อปิดหน้าต่าง ระบบอาจเปิดทิ้งขยะอาจทําให้เมธอด .deref()
เริ่มแสดงผล undefined
<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
let popup;
open.onclick = () => {
popup = new WeakRef(window.open('/login.html'));
};
close.onclick = () => {
const win = popup.deref();
if (win) win.close();
};
</script>
รายละเอียดที่น่าสนใจอย่างหนึ่งที่ควรพิจารณาเมื่อใช้ WeakRef
เพื่อเข้าถึงหน้าต่างหรือเอกสารคือ ข้อมูลอ้างอิงดังกล่าวจะยังใช้ได้อยู่เป็นระยะเวลาสั้นๆ หลังจากที่ปิดหน้าต่างหรือนำ iframe ออก นั่นเป็นเพราะ WeakRef
ยังคงแสดงผลค่าต่อไปจนกว่าออบเจ็กต์ที่เกี่ยวข้องจะมีการรวบรวมแบบขยะ ซึ่งจะเกิดขึ้นแบบไม่พร้อมกันใน JavaScript และโดยทั่วไปในช่วงที่ไม่มีการใช้งาน โชคดีที่เมื่อตรวจสอบหน้าต่างที่แยกออกในแผงหน่วยความจำของ Chrome DevTools การบันทึกฮีพสแนปชอตจะทริกเกอร์การรวบรวมขยะและกำจัดหน้าต่างที่อ้างอิงที่ไม่รัดกุม นอกจากนี้ยังตรวจสอบได้ว่ามีการลบออบเจ็กต์ที่อ้างอิงผ่าน WeakRef
ออกจาก JavaScript แล้วหรือไม่ด้วยการตรวจจับเมื่อ deref()
แสดงผล undefined
หรือใช้ FinalizationRegistry
API ใหม่ ดังนี้
let popup = new WeakRef(window.open('/login.html'));
// Polling deref():
let timer = setInterval(() => {
if (popup.deref() === undefined) {
console.log('popup was garbage-collected');
clearInterval(timer);
}
}, 20);
// FinalizationRegistry API:
let finalizers = new FinalizationRegistry(() => {
console.log('popup was garbage-collected');
});
finalizers.register(popup.deref());
วิธีแก้ปัญหา: สื่อสารผ่าน postMessage
การตรวจจับเมื่อมีการปิดหน้าต่างหรือการนำทางยกเลิกการโหลดเอกสารช่วยให้เรานำแฮนเดิลออกและข้อมูลอ้างอิงที่ไม่ได้ตั้งค่าได้ เพื่อที่ว่าหน้าต่างที่แยกออกจะเป็นขยะที่รวบรวมได้ อย่างไรก็ตาม การเปลี่ยนแปลงเหล่านี้เป็นการแก้ไขเฉพาะที่บางครั้งอาจเป็นข้อกังวลพื้นฐานมากกว่า ซึ่งก็คือการเชื่อมโยงโดยตรงระหว่างหน้าต่างๆ
นอกจากนี้ คุณยังใช้วิธีทางเลือกที่ครอบคลุมยิ่งขึ้นเพื่อหลีกเลี่ยงไม่ให้การอ้างอิงที่ไม่มีการอัปเดตระหว่างหน้าต่างกับเอกสารต่างๆ โดยการแยกตัวออกจากกันโดยจำกัดการสื่อสารระหว่างเอกสารกับ postMessage()
ลองนึกย้อนกลับไปถึงตัวอย่างบันทึกของผู้นำเสนอรายการเดิม ฟังก์ชันอย่าง nextSlide()
ได้อัปเดตหน้าต่างโน้ตโดยตรงด้วยการอ้างอิงและดัดแปลงเนื้อหา หน้าหลักอาจส่งข้อมูลที่จำเป็นไปยังหน้าต่างบันทึกแบบไม่พร้อมกันและโดยอ้อมผ่านทาง postMessage()
แทน
let updateNotes;
function showNotes() {
// keep the popup reference in a closure to prevent outside references:
let win = window.open('/presenter-view.html');
win.addEventListener('pagehide', () => {
if (!win || !win.location.host) return; // ignore initial "about:blank"
win = null;
});
// other functions must interact with the popup through this API:
updateNotes = (data) => {
if (!win) return;
win.postMessage(data, location.origin);
};
// listen for messages from the notes window:
addEventListener('message', (event) => {
if (event.source !== win) return;
if (event.data[0] === 'nextSlide') nextSlide();
});
}
let slide = 1;
function nextSlide() {
slide += 1;
// if the popup is open, tell it to update without referencing it:
if (updateNotes) {
updateNotes(['setSlide', slide]);
}
}
document.body.onclick = nextSlide;
แม้ว่าการดำเนินการนี้จะต้องให้หน้าต่างอ้างอิงกันและกัน แต่ไม่ได้เก็บการอ้างอิงเอกสารปัจจุบันจากหน้าต่างอื่นไว้ วิธีการส่งข้อความยังส่งเสริมการออกแบบที่จะมีการเก็บการอ้างอิงหน้าต่างไว้ในที่เดียว นั่นคือ คุณไม่ต้องตั้งค่าข้อมูลอ้างอิงเพียงรายการเดียวเมื่อปิดหน้าต่างหรือออกไป ในตัวอย่างข้างต้น มีเพียง showNotes()
เท่านั้นที่เก็บรักษาการอ้างอิงไปยังหน้าต่างบันทึก และจะใช้เหตุการณ์ pagehide
เพื่อให้แน่ใจว่ามีการล้างข้อมูลข้อมูลอ้างอิง
วิธีแก้ปัญหา: หลีกเลี่ยงการอ้างอิงโดยใช้ noopener
ในกรณีที่เปิดหน้าต่างป๊อปอัปไว้ซึ่งหน้าเว็บของคุณไม่จำเป็นต้องสื่อสารหรือควบคุม คุณอาจหลีกเลี่ยงการได้รับการอ้างอิงไปยังหน้าต่างนั้นได้ วิธีนี้มีประโยชน์อย่างยิ่งเมื่อสร้างหน้าต่างหรือ iframe ที่จะโหลดเนื้อหาจากเว็บไซต์อื่น ในกรณีเหล่านี้ window.open()
จะยอมรับตัวเลือก "noopener"
ที่ทำงานเหมือนกับแอตทริบิวต์ rel="noopener"
สำหรับลิงก์ HTML
window.open('https://example.com/share', null, 'noopener');
ตัวเลือก "noopener"
จะทำให้ window.open()
แสดงผล null
ทำให้ไม่สามารถจัดเก็บการอ้างอิงไปยังป๊อปอัปโดยไม่ได้ตั้งใจได้ และยังป้องกันไม่ให้หน้าต่างป๊อปอัปได้รับการอ้างอิงไปยังหน้าต่างระดับบนสุดด้วย เนื่องจากพร็อพเพอร์ตี้ window.opener
จะเป็น null
ความคิดเห็น
เราหวังว่าคำแนะนำบางส่วนในบทความนี้จะช่วยคุณค้นหาและแก้ไขการรั่วไหลของหน่วยความจำได้ หากคุณมีเทคนิคอื่นในการแก้ไขข้อบกพร่องของหน้าต่างที่แยกออกมาหรือบทความนี้ช่วยเผยรอยรั่วในแอป ก็คงจะรู้ดี พบกันได้ที่ Twitter @_developit