disconnectedCallback และการ Cleanup
เมื่อไหร่ที่ disconnectedCallback ทำงาน
หัวข้อที่มีชื่อว่า “เมื่อไหร่ที่ disconnectedCallback ทำงาน”disconnectedCallback จะถูกเรียกทุกครั้งที่ element ถูกลบออกจาก document ที่เชื่อมต่ออยู่ งานทำความสะอาดที่ควรทำที่นี่ได้แก่:
- ลบ event listener ที่เพิ่มไว้ใน
connectedCallback - ยกเลิก timer ของ
setIntervalหรือsetTimeout - ปิด WebSocket connections
- Disconnect
MutationObserver,ResizeObserverหรือIntersectionObserver
ข้อควรระวังสำคัญ: disconnectedCallback จะไม่ถูกเรียกเมื่อ page ทั้งหมดถูก unload (เช่น ผู้ใช้เปิดหน้าใหม่หรือปิด tab) จะถูก trigger เฉพาะจากการลบ DOM อย่างชัดเจน เช่น removeChild, replaceWith หรือการกำหนดค่าใหม่ให้ innerHTML
รูปแบบ Symmetry
หัวข้อที่มีชื่อว่า “รูปแบบ Symmetry”คิดว่า connectedCallback และ disconnectedCallback เป็นเหมือนวงเล็บที่จับคู่กัน ทรัพยากรที่คุณได้มาเมื่อ element เข้าสู่ DOM คุณก็ต้องคืนตอน element ออกไป:
class TimerDemo extends HTMLElement { connectedCallback() { // เริ่มทำงาน this._intervalId = setInterval(this._tick.bind(this), 1000); }
disconnectedCallback() { // หยุดทำงาน — สมมาตรกับ connectedCallback clearInterval(this._intervalId); }
_tick() { // ถูกเรียกทุก 1 วินาทีขณะที่ connected }}การเก็บ handle (this._intervalId) ไว้บน element instance คือวิธีมาตรฐานเพื่อให้ disconnectedCallback เข้าถึงได้
รูปแบบ Timer ที่สมบูรณ์
หัวข้อที่มีชื่อว่า “รูปแบบ Timer ที่สมบูรณ์”class TimerDemo extends HTMLElement { connectedCallback() { this._seconds = 0; this._render(); this._intervalId = setInterval(this._tick.bind(this), 1000); }
disconnectedCallback() { clearInterval(this._intervalId); }
_tick() { this._seconds += 1; this._render(); }
_render() { this.textContent = 'Running: ' + this._seconds + 's'; }}
customElements.define('timer-demo', TimerDemo);เดโมใช้งานจริง
หัวข้อที่มีชื่อว่า “เดโมใช้งานจริง”คลิก Remove timer เพื่อลบ element ออกจาก DOM — บรรทัด log ยืนยันว่า disconnectedCallback ทำงานและ timer หยุดแล้ว คลิก Re-add timer เพื่อแทรก element ใหม่อีกครั้ง counter จะ reset และ log จะแสดง connectedCallback ทำงานอีกครั้ง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
Cleanup ทุกอย่างใน disconnectedCallback | ป้องกัน memory leak, CPU leak และ event listener ค้างอย่างครบถ้วน symmetry ชัดเจนกับ connectedCallback | ต้องเขียน logic คู่กันเสมอ (setup/teardown) เพิ่ม boilerplate และเสี่ยงลืมคู่ที่ตรงกัน |
| ปล่อยให้ garbage collector จัดการเอง | โค้ดสั้นลง ไม่ต้องเขียน disconnectedCallback | listener, timer, observer ที่ยังอ้างอิงถึง element จะไม่ถูกเก็บกวาดจนกว่า reference ทั้งหมดจะหลุด ทำให้เกิด memory และ CPU leak โดยเฉพาะกับ element ที่ถูกสร้าง/ลบซ้ำๆ (เช่น ใน list ที่ scroll) |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ไม่ implement
disconnectedCallbackเลย — event listener,setInterval/setTimeout, และ observer ที่ตั้งไว้ในconnectedCallbackจะยังทำงานต่อแม้ element ถูกลบออกจาก DOM แล้ว ทำให้เกิด memory leak สะสมเมื่อ element ถูกสร้างและลบซ้ำๆ - ลืม disconnect
MutationObserver/ResizeObserver/IntersectionObserver— observer เหล่านี้ถือ reference ไปยัง element และ callback ทำให้ garbage collector เก็บกวาดไม่ได้จนกว่าจะเรียก.disconnect()อย่างชัดเจน - สมมติว่า
disconnectedCallbackจะถูกเรียกตอน page unload — ไม่ถูกเรียกอย่างน่าเชื่อถือเมื่อผู้ใช้ปิด tab หรือ refresh หน้า ถ้าต้องเซฟข้อมูลก่อนออกจากหน้า ให้ใช้ eventbeforeunloadหรือpagehideแทน ไม่ใช่พึ่งพาdisconnectedCallback
💡 ตัวอย่างจากของจริง
GitHub —
<relative-time>element ของ GitHub ใช้setIntervalเพื่ออัปเดตข้อความ “3 minutes ago” และต้อง clear interval ในdisconnectedCallbackทุกครั้งเพื่อไม่ให้ leak เมื่อ timeline ถูก scroll และ element ถูกสร้าง/ลบซ้ำจำนวนมากX (Twitter) — timeline แบบ virtualized ของ X สร้างและทำลาย custom element จำนวนมากขณะ scroll การไม่ cleanup listener ใน disconnectedCallback อย่างถูกต้องจะทำให้ memory ค่อยๆ โตขึ้นเรื่อยๆ จนหน้าเว็บหน่วง