題幹與適用場景
你要交付一個 <account-summary> 自訂元素,宿主頁面可能由 React、Vue 或伺服器模板產生。元素要顯示帳號狀態、回應 status 屬性、派發可監聽事件,並在從文件移除後清理訂閱。團隊也要求樣式不外洩,鍵盤與輔助技術語意不能因封裝而遺失。
預設先實作 autonomous custom element;若必須擴充原生 button 等元素,再單獨評估 customized built-in elements 的相容性。題目考察生命週期邊界與可驗證的資源管理,不是特定框架語法。
面試官考察什麼
- 能否說明 constructor、connectedCallback、disconnectedCallback 與屬性觀察回呼的責任邊界。
- 能否處理重複連線、移動、升級與初始屬性,而非假設每個回呼只執行一次。
- 能否把 Shadow DOM 樣式封裝、可及性、事件穿透與宿主整合放在同一設計中。
- 能否提出註冊冪等、相容性、清理與測試方案。
只背生命週期名稱不夠。高品質回答會先定義資源所有權,再說明連線狀態、屬性狀態和渲染狀態如何互相影響。
回答前要澄清的問題
- 元素是否需要擴充原生語意元素?若需要,
is語法和瀏覽器支援會改變方案。 - 屬性是字串設定,還是要傳入物件、回呼等複雜值?複雜值應透過屬性、property 或方法清楚區分。
- 元素移到同一文件的其他位置時,狀態是否必須保留?這決定是否採用
connectedMoveCallback或顯式狀態保護。 - 事件需要跨過 Shadow DOM 邊界嗎?先約定
bubbles、composed和事件資料,再決定事件名稱。
30 秒回答框架
「我先把元素分成定義、連線、屬性同步、渲染和清理五個階段。constructor 只做輕量欄位初始化,不依賴子節點或外部文件;connectedCallback 建立訂閱並觸發渲染,而且必須冪等;disconnectedCallback 釋放監聽器、計時器和觀察器。只觀察真正需要的屬性,在 attributeChangedCallback 正規化字串並統一進入渲染入口。Shadow DOM 負責內部樣式邊界,但公開語意、焦點與事件契約仍要驗證。最後用重複連線、移動、屬性初始化、卸載和不支援瀏覽器的測試覆蓋生命週期。」
分步深入分析
1. 先定義狀態和資源所有權
把「已連線」「已訂閱」「已渲染」分開記錄。constructor 不應讀取外部 DOM 或啟動網路請求,因為瀏覽器可能先建立元素,稍後才把它連到文件。
class AccountSummary extends HTMLElement {
static observedAttributes = ["status"];
#connected = false;
#unsubscribe = null;
#root;
constructor() {
super();
this.#root = this.attachShadow({ mode: "open" });
}
}2. 讓 connectedCallback 可重複執行
元素可能被移除後再次插入,所以 connectedCallback 不能無條件重複註冊監聽器。先檢查連線狀態,再建立訂閱;渲染函式也要能安全重複呼叫。
connectedCallback() {
if (this.#connected) return;
this.#connected = true;
this.#unsubscribe = accountStore.subscribe(() => this.#render());
this.#render();
}3. 在 disconnectedCallback 釋放所有副作用
事件監聽器、setInterval、ResizeObserver、AbortController 和 store 訂閱都要有明確擁有者。清理後把參照設為空值,重新連線時才能建立新的一組資源。
disconnectedCallback() {
this.#unsubscribe?.();
this.#unsubscribe = null;
this.#connected = false;
}若元素只是被移動,舊式移動方式可能依序觸發斷開與連線回呼。若狀態保留很重要,應在支援的環境評估 connectedMoveCallback,或讓重連流程從屬性與持久狀態重新建立。
4. 觀察屬性並區分 property
observedAttributes 只列出真正需要同步的屬性。attributeChangedCallback 可能在解析初始屬性時呼叫,因此不能把「變更」理解成使用者剛操作;要統一處理初始值與後續值,並避免在回呼中再次寫回同一屬性形成迴圈。
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === "status") this.#renderStatus(newValue ?? "unknown");
}字串屬性適合宣告式 HTML。陣列、物件和回呼應透過公開 property 或方法傳入,並寫明連線前設定的行為;不要把 JSON 字串當成沒有邊界的複雜值協定。
5. 設計 Shadow DOM 與宿主契約
Shadow DOM 可以隔離內部 CSS 和 DOM 查詢,但不會自動解決語意問題。元件仍要使用正確角色、名稱、焦點順序和可見狀態;需要讓宿主參與布局時,用 :host、slot 或有限的 CSS 自訂屬性建立契約。
:host { display: block; }
:host([hidden]) { display: none; }內部事件若要讓宿主監聽,應明確設定 bubbles 與 composed,並限制事件資料。跨邊界派發事件不代表把內部節點暴露給宿主。
6. 處理註冊、升級與相容性
同一全域註冊表中的名稱只能定義一次。共享函式庫應採命名空間前綴,並在定義前檢查 customElements.get(name);不能用捕獲例外掩蓋兩個版本搶同名元素。若元素先出現在 HTML 再註冊,定義後會升級,程式要覆蓋這個順序。
const name = "account-summary";
if (!customElements.get(name)) {
customElements.define(name, AccountSummary);
}autonomous 元素通常比 customized built-in 更容易跨瀏覽器部署。MDN 記錄 Safari 對 customized built-in elements 的限制;若業務必須擴充原生元素,應先確認目標瀏覽器矩陣並準備降級方案。
高品質示範回答
我會先寫出資源與狀態的所有權表:constructor 建立 Shadow Root 和預設欄位,connectedCallback 建立訂閱並渲染,disconnectedCallback 釋放訂閱,attributeChangedCallback 只處理觀察清單中的宣告式設定。每個階段都設計成可重複或可安全跳過,避免把「只執行一次」當成保證。
我會選 autonomous custom element,並用 customElements.get 防止重複註冊。內部樣式放在 Shadow DOM,但元件仍暴露清楚的可及名稱、焦點行為與事件契約;跨邊界事件設定 bubbles、composed 並限制 payload。屬性只承載字串和布林等原始設定,複雜物件透過 property 或方法傳入。
驗證會覆蓋先解析後註冊、屬性初始值、重複連線、移動、卸載清理、事件跨邊界、鍵盤操作和螢幕閱讀器。若必須使用 customized built-in elements,我會先按目標瀏覽器驗證,再決定改用 autonomous 元素或提供框架適配層。
常見錯誤與改進
- 錯誤表現 → 在 constructor 讀子節點、啟動請求 → 失敗原因 → 構造時元素可能尚未連線 → 修正方法 → 將依賴文件和網路的動作移到 connectedCallback,並為請求綁定 AbortController。
- 錯誤表現 → 每次 connectedCallback 都新增監聽器 → 失敗原因 → 重連後重複處理,造成洩漏和重複渲染 → 修正方法 → 記錄訂閱句柄,建立與清理成對出現並保持冪等。
- 錯誤表現 → 在屬性回呼中無條件 setAttribute → 失敗原因 → 可能形成回呼迴圈,初始解析也會觸發回呼 → 修正方法 → 比較舊值,正規化輸入並避免回寫同一屬性。
- 錯誤表現 → 用 Shadow DOM 解決全部可及性 → 失敗原因 → 封裝不會自動提供名稱、焦點和語意 → 修正方法 → 以鍵盤、輔助技術和宿主整合測試公開契約。
追問及應對
元素在定義前已由 HTML 解析出來,如何避免閃爍?
把未定義元素視為可升級狀態,提供基礎可讀內容或載入狀態;註冊完成後由瀏覽器升級實例。若必須等待定義再操作,宿主可以等待 customElements.whenDefined("account-summary"),但不要讓整頁阻塞在元件腳本上。
複雜物件應該放在 attribute 嗎?
預設不放。attribute 天然是字串,適合可序列化、可觀察的宣告式設定;物件和回呼透過公開 property 或方法傳入,並定義連線前後行為。若確實要序列化,應規定版本、大小和錯誤處理。
Shadow DOM 內部按鈕點擊,宿主為何收不到事件?
事件是否跨越 Shadow 邊界取決於 composed,是否向上冒泡取決於 bubbles。我會派發穩定的語意事件並明確設定兩者;邊界外的事件目標可能被重定向到 shadow host,因此宿主不應依賴內部節點結構。
如何證明元件重連後沒有洩漏?
反覆插入和移除同一實例,記錄訂閱數、事件觸發次數和計時器句柄,再用記憶體快照確認舊節點可回收。測試還要覆蓋初始屬性回呼、移動及未定義到升級的路徑。