หน่วยความจำหน้าต่างที่ปลดออกรั่วไหล

ค้นหาและแก้ไขหน่วยความจำที่รั่วไหลซึ่งตรวจจับได้ยากซึ่งเกิดจากหน้าต่างที่แยกออกมา

Bartek Nowierski
Bartek Nowierski

หน่วยความจํารั่วใน 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 ที่เหลืออยู่ในพร็อพเพอร์ตี้ของหน้าต่าง

การใช้เครื่องมือสำหรับนักพัฒนาเว็บของ Chrome เพื่อสาธิตวิธีเก็บเอกสารไว้หลังจากปิดหน้าต่างแล้ว

ปัญหาเดียวกันนี้อาจเกิดขึ้นได้เมื่อใช้องค์ประกอบ <iframe> อินเฟรมทํางานเหมือนหน้าต่างที่ฝังอยู่ซึ่งมีเอกสาร และพร็อพเพอร์ตี้ contentWindow จะให้สิทธิ์เข้าถึงออบเจ็กต์ Window ที่รวมอยู่ภายใน คล้ายกับค่าที่ window.open() แสดง โค้ด JavaScript สามารถอ้างอิง contentWindow หรือ contentDocument ของ iframe ได้แม้ว่าระบบจะนำ iframe ออกจาก DOM หรือ URL ของ iframe จะเปลี่ยนแปลงไป ซึ่งจะช่วยป้องกันไม่ให้ระบบรวบรวมขยะในเอกสารเนื่องจากยังคงเข้าถึงพร็อพเพอร์ตี้ของเอกสารได้

การสาธิตวิธีที่ตัวแฮนเดิลเหตุการณ์จะเก็บเอกสารของ iframe ไว้ได้ แม้ว่าจะไปยัง 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 ที่แสดงการอ้างอิงที่เก็บวัตถุขนาดใหญ่ไว้
สแนปชอตฮีปที่แสดงการอ้างอิงที่เก็บออบเจ็กต์ขนาดใหญ่ไว้

หากต้องการบันทึกสแนปชอตกอง ให้ไปที่แท็บหน่วยความจําในเครื่องมือสําหรับนักพัฒนาเว็บใน Chrome แล้วเลือกสแนปชอตกองในรายการประเภทการโปรไฟล์ที่ใช้ได้ เมื่อบันทึกเสร็จแล้ว มุมมองสรุปจะแสดงออบเจ็กต์ปัจจุบันในหน่วยความจำ ซึ่งจัดกลุ่มตามคอนสตรคเตอร์

การสาธิตการจับภาพฮีปในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

การวิเคราะห์กองข้อมูลอาจเป็นเรื่องที่น่ากลัวและอาจหาข้อมูลที่เหมาะสมสำหรับการแก้ไขข้อบกพร่องได้ยาก วิศวกร Chromium yossik@ และ peledni@ ได้พัฒนาเครื่องมือตัวล้างกองแบบสแตนด์อโลนเพื่อช่วยไฮไลต์โหนดที่เฉพาะเจาะจง เช่น หน้าต่างที่แยกออกมา การใช้เครื่องมือล้างกองขยะในร่องรอยจะนําข้อมูลอื่นๆ ที่ไม่จําเป็นออกจากกราฟการคงผู้ใช้ไว้ ซึ่งทําให้ร่องรอยสะอาดขึ้นและอ่านได้ง่ายขึ้นมาก

วัดหน่วยความจําแบบเป็นโปรแกรม

สแนปชอตฮีปให้รายละเอียดในระดับสูงและเหมาะอย่างยิ่งในการหาตําแหน่งที่เกิดการรั่วไหล แต่การถ่ายสแนปชอตฮีปเป็นกระบวนการที่ต้องทำด้วยตนเอง อีกวิธีในการตรวจสอบการรั่วไหลของหน่วยความจำคือการรับขนาดกอง JavaScript ที่ใช้อยู่ในปัจจุบันจาก performance.memory API

ภาพหน้าจอของส่วนอินเทอร์เฟซผู้ใช้ของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
การตรวจสอบขนาดฮีป JS ที่ใช้ในเครื่องมือสำหรับนักพัฒนาเว็บเมื่อมีการสร้างป๊อปอัป ปิดป๊อปอัป และยกเลิกการอ้างอิง

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