Constructor และ connectedCallback
constructor
หัวข้อที่มีชื่อว่า “constructor”constructor คือจุดที่คุณตั้งค่า internal state เริ่มต้นของ element เช่น กำหนดค่า property หรือสร้าง Shadow DOM ข้อกำหนดที่สำคัญมีดังนี้:
- ต้องเรียก
super()เป็น statement แรกเสมอ - ห้าม อ่านหรือเขียน attribute ของ element ใน constructor
- ห้าม แตะ children หรือ parent node ของ element ใน constructor
- ห้าม render เนื้อหาลงใน DOM ใน constructor
เหตุผลคือเมื่อ constructor ถูกเรียก element ยังไม่ได้อยู่ใน document และ attribute ยังไม่ได้ถูกกำหนดค่า
class MyWidget extends HTMLElement { constructor() { super(); // MUST be first — gives you access to `this` // Good: set up internal state this._count = 0; this._listeners = []; // Good: attach shadow root this.attachShadow({ mode: 'open' }); // BAD — do NOT do this in constructor: // this.innerHTML = '<p>hello</p>'; // may throw // this.getAttribute('label'); // always null here }}connectedCallback
หัวข้อที่มีชื่อว่า “connectedCallback”connectedCallback จะถูกเรียกทุกครั้งที่ element ถูกแทรกเข้าไปใน document ที่เชื่อมต่ออยู่ นี่คือจุดที่ถูกต้องและปลอดภัยสำหรับ:
- การ render เนื้อหา HTML ของ element
- การอ่าน attribute เพื่อกำหนดพฤติกรรม
- การลงทะเบียน event listener
- การเริ่มต้น timer หรือ fetch ข้อมูล
สิ่งที่ต้องระวังคือ connectedCallback จะถูกเรียกซ้ำทุกครั้งที่ element ถูกแทรกเข้าไปใหม่ ถ้าย้าย element ไปยัง parent อื่นด้วย appendChild callback นี้จะถูกเรียกอีกครั้ง
class MyWidget extends HTMLElement { constructor() { super(); this._initialized = false; }
connectedCallback() { if (this._initialized) return; // guard against re-connection this._initialized = true; var label = this.getAttribute('label') || 'default'; this.innerHTML = '<span>' + label + '</span>'; this.addEventListener('click', this._handleClick.bind(this)); }}ผิดกับถูก
หัวข้อที่มีชื่อว่า “ผิดกับถูก”// WRONG — DOM manipulation in constructorclass BadWidget extends HTMLElement { constructor() { super(); this.innerHTML = '<p>' + this.getAttribute('title') + '</p>'; // title is null here! }}
// RIGHT — DOM manipulation in connectedCallbackclass GoodWidget extends HTMLElement { constructor() { super(); // only internal state this._title = ''; } connectedCallback() { this._title = this.getAttribute('title') || 'Untitled'; this.innerHTML = '<p>' + this._title + '</p>'; // attribute is available now }}ตัวอย่างที่รันได้จริง
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้จริง”สังเกตว่า constructor ถูกเรียกก่อน แต่ไม่ได้แตะ DOM ส่วน connectedCallback จะนับ _count ขึ้นทุกครั้งที่ element ถูกแทรกเข้าไปใหม่
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
ตั้งค่าใน constructor | ทำงานเร็วที่สุด ก่อน element เข้า DOM เหมาะกับ internal state ล้วนๆ | เข้าถึง attribute, children, parent ไม่ได้เลย เพราะ element ยังไม่ถูกแทรกเข้า document |
ตั้งค่าใน connectedCallback | เข้าถึง attribute, light DOM children, parent ได้ครบ และ render ได้อย่างปลอดภัย | ถูกเรียกซ้ำได้หลายครั้งตลอดอายุ element ต้องเขียนโค้ดให้ทนต่อการเรียกซ้ำ (idempotent) |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- อ่าน attribute ใน
constructor— attribute ยังไม่ถูกกำหนดค่าตอนที่ constructor ทำงาน ให้ย้าย logic การอ่าน attribute ไปไว้ในconnectedCallbackแทน - แตะ children หรือ parent ใน
constructor— light DOM children ยังไม่ถูก parse ครบตอนนั้น การเข้าถึงthis.childrenหรือthis.parentNodeใน constructor อาจได้ค่าที่ไม่ครบหรือnull - สมมติว่า
connectedCallbackเรียกแค่ครั้งเดียว — ถ้าย้าย element ด้วยappendChildไปยัง parent ใหม่ browser จะเรียกdisconnectedCallbackแล้วตามด้วยconnectedCallbackอีกครั้ง ควรเขียนให้ re-run ได้โดยไม่พังหรือซ้ำซ้อน (เช่น เช็คก่อนเพิ่ม event listener ซ้ำ)
💡 ตัวอย่างจากของจริง
Stencil (compiler ที่อยู่เบื้องหลัง component ของ Ionic Framework) เอกสาร lifecycle ของมันระบุการแบ่งงานแบบนี้ไว้ชัดเจน: เก็บเฉพาะ internal state ไว้ใน constructor และย้าย logic ที่ต้องพึ่งพา attribute/DOM ไปไว้ใน
connectedCallbackหรือcomponentWillLoadเพื่อให้ component ทนต่อการถูก re-mount ซ้ำในแอปแบบ SPA (stenciljs.com/docs/component-lifecycle)