具代表性的面試主題

前端面試:如何設計可維護的 Custom Elements 生命週期?

前端困難
OfferCC 編輯團隊發佈 更新

題幹

請設計一個可在多個團隊與框架中重複使用的 Custom Element。你會如何安排 constructor、connectedCallback、disconnectedCallback 與 attributeChangedCallback,如何處理 Shadow DOM、屬性同步、重複註冊及瀏覽器相容性?

題幹與適用場景

你要交付一個 <account-summary> 自訂元素,宿主頁面可能由 React、Vue 或伺服器模板產生。元素要顯示帳號狀態、回應 status 屬性、派發可監聽事件,並在從文件移除後清理訂閱。團隊也要求樣式不外洩,鍵盤與輔助技術語意不能因封裝而遺失。

預設先實作 autonomous custom element;若必須擴充原生 button 等元素,再單獨評估 customized built-in elements 的相容性。題目考察生命週期邊界與可驗證的資源管理,不是特定框架語法。

面試官考察什麼

  • 能否說明 constructor、connectedCallback、disconnectedCallback 與屬性觀察回呼的責任邊界。
  • 能否處理重複連線、移動、升級與初始屬性,而非假設每個回呼只執行一次。
  • 能否把 Shadow DOM 樣式封裝、可及性、事件穿透與宿主整合放在同一設計中。
  • 能否提出註冊冪等、相容性、清理與測試方案。

只背生命週期名稱不夠。高品質回答會先定義資源所有權,再說明連線狀態、屬性狀態和渲染狀態如何互相影響。

回答前要澄清的問題

  1. 元素是否需要擴充原生語意元素?若需要,is 語法和瀏覽器支援會改變方案。
  2. 屬性是字串設定,還是要傳入物件、回呼等複雜值?複雜值應透過屬性、property 或方法清楚區分。
  3. 元素移到同一文件的其他位置時,狀態是否必須保留?這決定是否採用 connectedMoveCallback 或顯式狀態保護。
  4. 事件需要跨過 Shadow DOM 邊界嗎?先約定 bubbles、composed 和事件資料,再決定事件名稱。

30 秒回答框架

「我先把元素分成定義、連線、屬性同步、渲染和清理五個階段。constructor 只做輕量欄位初始化,不依賴子節點或外部文件;connectedCallback 建立訂閱並觸發渲染,而且必須冪等;disconnectedCallback 釋放監聽器、計時器和觀察器。只觀察真正需要的屬性,在 attributeChangedCallback 正規化字串並統一進入渲染入口。Shadow DOM 負責內部樣式邊界,但公開語意、焦點與事件契約仍要驗證。最後用重複連線、移動、屬性初始化、卸載和不支援瀏覽器的測試覆蓋生命週期。」

分步深入分析

1. 先定義狀態和資源所有權

把「已連線」「已訂閱」「已渲染」分開記錄。constructor 不應讀取外部 DOM 或啟動網路請求,因為瀏覽器可能先建立元素,稍後才把它連到文件。

js
class AccountSummary extends HTMLElement {
  static observedAttributes = ["status"];
  #connected = false;
  #unsubscribe = null;
  #root;

  constructor() {
    super();
    this.#root = this.attachShadow({ mode: "open" });
  }
}

2. 讓 connectedCallback 可重複執行

元素可能被移除後再次插入,所以 connectedCallback 不能無條件重複註冊監聽器。先檢查連線狀態,再建立訂閱;渲染函式也要能安全重複呼叫。

js
connectedCallback() {
  if (this.#connected) return;
  this.#connected = true;
  this.#unsubscribe = accountStore.subscribe(() => this.#render());
  this.#render();
}

3. 在 disconnectedCallback 釋放所有副作用

事件監聽器、setInterval、ResizeObserver、AbortController 和 store 訂閱都要有明確擁有者。清理後把參照設為空值,重新連線時才能建立新的一組資源。

js
disconnectedCallback() {
  this.#unsubscribe?.();
  this.#unsubscribe = null;
  this.#connected = false;
}

若元素只是被移動,舊式移動方式可能依序觸發斷開與連線回呼。若狀態保留很重要,應在支援的環境評估 connectedMoveCallback,或讓重連流程從屬性與持久狀態重新建立。

4. 觀察屬性並區分 property

observedAttributes 只列出真正需要同步的屬性。attributeChangedCallback 可能在解析初始屬性時呼叫,因此不能把「變更」理解成使用者剛操作;要統一處理初始值與後續值,並避免在回呼中再次寫回同一屬性形成迴圈。

js
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 自訂屬性建立契約。

css
:host { display: block; }
:host([hidden]) { display: none; }

內部事件若要讓宿主監聽,應明確設定 bubbles 與 composed,並限制事件資料。跨邊界派發事件不代表把內部節點暴露給宿主。

6. 處理註冊、升級與相容性

同一全域註冊表中的名稱只能定義一次。共享函式庫應採命名空間前綴,並在定義前檢查 customElements.get(name);不能用捕獲例外掩蓋兩個版本搶同名元素。若元素先出現在 HTML 再註冊,定義後會升級,程式要覆蓋這個順序。

js
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,因此宿主不應依賴內部節點結構。

如何證明元件重連後沒有洩漏?

反覆插入和移除同一實例,記錄訂閱數、事件觸發次數和計時器句柄,再用記憶體快照確認舊節點可回收。測試還要覆蓋初始屬性回呼、移動及未定義到升級的路徑。

公開來源

同類題目

資源

產品比較

每份比較均依官方資料整理,並標示複核日期。

查看完整比較