ค้นหาและแก้ไขหน่วยความจำที่รั่วไหลซึ่งตรวจจับได้ยากซึ่งเกิดจากหน้าต่างที่แยกออกมา
หน่วยความจํารั่วใน JavaScript คืออะไร
การรั่วไหลของหน่วยความจําคือปริมาณหน่วยความจําที่แอปพลิเคชันใช้เพิ่มขึ้นโดยไม่ตั้งใจเมื่อเวลาผ่านไป ใน JavaScript ปัญหาหน่วยความจำรั่วเกิดขึ้นเมื่อไม่จำเป็นต้องใช้ออบเจ็กต์อีกต่อไป แต่ยังคงมีการอ้างอิงโดยฟังก์ชันหรือออบเจ็กต์อื่นๆ การอ้างอิงเหล่านี้จะป้องกันไม่ให้โปรแกรมรวบรวมขยะรีไซเคิลออบเจ็กต์ที่ไม่จำเป็น
งานของ Garbage Collector คือระบุและเรียกคืนออบเจ็กต์ที่เข้าถึงจากแอปพลิเคชันไม่ได้อีกต่อไป ซึ่งจะทำงานได้แม้ว่าออบเจ็กต์จะอ้างอิงตัวเองหรืออ้างอิงกันและกันแบบวนไปวนมาก็ตาม เมื่อไม่มีข้อมูลอ้างอิงเหลืออยู่ซึ่งแอปพลิเคชันจะเข้าถึงกลุ่มออบเจ็กต์ได้ ระบบก็จะทำการเก็บขยะ
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
ที่ปิดอยู่ได้ จึงต้องเก็บไว้ในหน่วยความจำ หากหน้าต่างมีออบเจ็กต์หรือ iframe ของ JavaScript จำนวนมาก ระบบจะไม่สามารถเรียกคืนหน่วยความจำนั้นจนกว่าจะไม่มีการอ้างอิง JavaScript ที่เหลืออยู่ในพร็อพเพอร์ตี้ของหน้าต่าง
ปัญหาเดียวกันนี้อาจเกิดขึ้นได้เมื่อใช้องค์ประกอบ <iframe>
อินเฟรมทํางานเหมือนหน้าต่างที่ฝังอยู่ซึ่งมีเอกสาร และพร็อพเพอร์ตี้ contentWindow
จะให้สิทธิ์เข้าถึงออบเจ็กต์ Window
ที่รวมอยู่ภายใน คล้ายกับค่าที่ window.open()
แสดง โค้ด JavaScript สามารถอ้างอิง contentWindow
หรือ contentDocument
ของ iframe ได้แม้ว่าระบบจะนำ iframe ออกจาก DOM หรือ URL ของ iframe จะเปลี่ยนแปลงไป ซึ่งจะช่วยป้องกันไม่ให้ระบบรวบรวมขยะในเอกสารเนื่องจากยังคงเข้าถึงพร็อพเพอร์ตี้ของเอกสารได้
ในกรณีที่มีการเก็บการอ้างอิง document
ภายในหน้าต่างหรือ iframe จาก JavaScript ระบบจะเก็บเอกสารนั้นไว้ในหน่วยความจำแม้ว่าหน้าต่างหรือ iframe ที่บรรจุจะไปยัง URL ใหม่ ปัญหานี้อาจเกิดขึ้นได้เมื่อ JavaScript ที่เก็บข้อมูลอ้างอิงนั้นไม่ตรวจพบว่าหน้าต่าง/เฟรมไปยัง URL ใหม่แล้ว เนื่องจากไม่ทราบว่าเมื่อใดที่ข้อมูลอ้างอิงนั้นกลายเป็นข้อมูลอ้างอิงสุดท้ายที่เก็บเอกสารไว้ในหน่วยความจำ
วิธีที่หน้าต่างที่แยกออกมาทําให้หน่วยความจํารั่วไหล
เมื่อทํางานกับหน้าต่างและ 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>
การตรวจหาหน่วยความจําที่รั่วไหลซึ่งเกิดจากหน้าต่างที่แยกอยู่
การติดตามหน่วยความจําที่รั่วอาจเป็นเรื่องยาก การสร้างการจำลองปัญหาเหล่านี้แยกต่างหากมักทำได้ยาก โดยเฉพาะอย่างยิ่งเมื่อเกี่ยวข้องกับเอกสารหรือหน้าต่างหลายรายการ การตรวจสอบการอ้างอิงที่อาจรั่วไหลอาจทําให้เกิดการสร้างการอ้างอิงเพิ่มเติมที่ป้องกันไม่ให้ระบบเก็บขยะออบเจ็กต์ที่ตรวจสอบ ดังนั้น คุณจึงควรเริ่มต้นด้วยเครื่องมือที่หลีกเลี่ยงโอกาสนี้โดยเฉพาะ
วิธีที่ดีในการเริ่มแก้ไขข้อบกพร่องเกี่ยวกับหน่วยความจำคือการถ่ายภาพฮีป ซึ่งจะแสดงมุมมอง ณ เวลาหนึ่งเกี่ยวกับหน่วยความจําที่แอปพลิเคชันใช้อยู่ในปัจจุบัน ซึ่งก็คือออบเจ็กต์ทั้งหมดที่สร้างแล้ว แต่ยังไม่ได้เก็บขยะ สแนปชอตฮีปมีข้อมูลที่เป็นประโยชน์เกี่ยวกับออบเจ็กต์ ซึ่งรวมถึงขนาดและรายการตัวแปรและ Closure ที่อ้างอิงถึงออบเจ็กต์
หากต้องการบันทึกสแนปชอตกอง ให้ไปที่แท็บหน่วยความจําในเครื่องมือสําหรับนักพัฒนาเว็บใน Chrome แล้วเลือกสแนปชอตกองในรายการประเภทการโปรไฟล์ที่ใช้ได้ เมื่อบันทึกเสร็จแล้ว มุมมองสรุปจะแสดงออบเจ็กต์ปัจจุบันในหน่วยความจำ ซึ่งจัดกลุ่มตามคอนสตรคเตอร์
การวิเคราะห์กองข้อมูลอาจเป็นเรื่องที่น่ากลัวและอาจหาข้อมูลที่เหมาะสมสำหรับการแก้ไขข้อบกพร่องได้ยาก วิศวกร Chromium yossik@ และ peledni@ ได้พัฒนาเครื่องมือตัวล้างกองแบบสแตนด์อโลนเพื่อช่วยไฮไลต์โหนดที่เฉพาะเจาะจง เช่น หน้าต่างที่แยกออกมา การใช้เครื่องมือล้างกองขยะในร่องรอยจะนําข้อมูลอื่นๆ ที่ไม่จําเป็นออกจากกราฟการคงผู้ใช้ไว้ ซึ่งทําให้ร่องรอยสะอาดขึ้นและอ่านได้ง่ายขึ้นมาก
วัดหน่วยความจําแบบเป็นโปรแกรม
สแนปชอตฮีปให้รายละเอียดในระดับสูงและเหมาะอย่างยิ่งในการหาตําแหน่งที่เกิดการรั่วไหล แต่การถ่ายสแนปชอตฮีปเป็นกระบวนการที่ต้องทำด้วยตนเอง อีกวิธีในการตรวจสอบการรั่วไหลของหน่วยความจำคือการรับขนาดกอง 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
เป็นค่าว่างเมื่อคลิกปุ่มปิดจะเพียงพอแล้ว แต่ยังคงมีการรั่วไหลของหน่วยความจำเมื่อผู้ใช้ไม่ได้ใช้ปุ่มดังกล่าวเพื่อปิดหน้าต่าง การแก้ปัญหานี้ต้องอาศัยการตรวจหากรณีเหล่านี้เพื่อยกเลิกการตั้งค่าการอ้างอิงที่เหลืออยู่เมื่อเกิดกรณีดังกล่าว
โซลูชัน: ตรวจสอบและกำจัด
ในหลายสถานการณ์ จาวาสคริปต์ที่มีหน้าที่เปิดหน้าต่างหรือสร้างเฟรมจะไม่มีการควบคุมวงจรชีวิตของหน้าต่างหรือเฟรมนั้นๆ โดยเฉพาะ ผู้ใช้สามารถปิดป๊อปอัปได้ หรือการเปลี่ยนไปยังเอกสารใหม่อาจทำให้เอกสารที่ก่อนหน้านี้อยู่ในหน้าต่างหรือเฟรมแยกออก ไม่ว่าในกรณีใดเบราว์เซอร์จะเรียกเหตุการณ์ pagehide
เพื่อส่งสัญญาณว่าระบบกำลังยกเลิกการโหลดเอกสาร
เหตุการณ์ pagehide
สามารถใช้เพื่อตรวจหาหน้าต่างที่ปิดอยู่และการไปยังส่วนอื่นที่ไม่ใช่เอกสารปัจจุบัน อย่างไรก็ตาม มีข้อควรระวังสำคัญอย่างหนึ่งคือ หน้าต่างและ iframe ที่สร้างขึ้นใหม่ทั้งหมดจะมีเอกสารว่าง จากนั้นจะไปยัง URL ที่ระบุแบบไม่สอดคล้องกันหากระบุไว้ ด้วยเหตุนี้ ระบบจึงเรียกเหตุการณ์ pagehide
เริ่มต้นหลังจากสร้างกรอบหรือหน้าต่างไม่นาน ก่อนที่เอกสารเป้าหมายจะโหลด เนื่องจากโค้ดล้างข้อมูลอ้างอิงต้องทำงานเมื่อระบบยกเลิกการโหลดเอกสารเป้าหมาย เราจึงต้องละเว้นเหตุการณ์ pagehide
รายการแรกนี้ การทำเช่นนี้มีหลายวิธี แต่วิธีที่ง่ายที่สุดคือการละเว้นเหตุการณ์ pagehide ที่มาจากabout:blank
URL ของเอกสารเริ่มต้น ตัวอย่างป๊อปอัปจะมีลักษณะดังนี้
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 การทำสแนปชอตกองขยะจะเรียกใช้การเก็บขยะและกำจัดหน้าต่างที่มีการอ้างอิงแบบไม่แน่น นอกจากนี้ คุณยังตรวจสอบได้ว่าออบเจ็กต์ที่อ้างอิงผ่าน 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