操作 DOM#

DOM(Document Object Model)是實現高度動態網頁應用程式的主要手段之一。理解 DOM 操作在函式庫中的運作方式,能讓我們寫出更好、更快的程式碼。本章探討高效修改 DOM 的技術,涵蓋 HTML 注入、屬性與特性的差異、樣式處理,以及避免 layout thrashing 的效能最佳化。

12.1 將 HTML 注入 DOM#

將 HTML 字串插入文件的任何位置,是建構高度動態網頁的常見需求。相較於逐一使用 DOM API(createElementappendChild),直接注入 HTML 字串更為簡潔高效。

將 HTML 轉換為 DOM#

透過 innerHTML 屬性將 HTML 字串轉為 DOM 結構,步驟如下:

  1. 確保 HTML 字串包含合法的 HTML 程式碼
  2. 依瀏覽器規則用必要的外層標籤包裹字串
  3. 使用 innerHTML 將字串插入一個暫時的 DOM 元素
  4. 取出建立好的 DOM 節點

預處理 HTML 原始字串

自閉合標籤(self-closing tags)在某些瀏覽器中可能導致問題。例如 <table/> 這樣的自閉合語法只對少數元素(如 <img><br><hr>)有效。解決方式是透過正規表達式將自閉合標籤轉換為標準形式:

const tags =
  /^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|
    source|track|wbr)$/i;
function convert(html) {
  return html.replace(
    /(<(\w+)[^>]*?)\/>/g, (all, front, tag) => {
      return tags.test(tag) ? all :
        front + "></" + tag + ">";
    });
}

HTML 包裹

某些 HTML 元素必須位於特定的容器元素內才能被正確注入。例如:

  • <option><optgroup> 必須在 <select multiple>
  • <td><th> 必須在 <table><tbody><tr>
  • <legend> 必須在 <fieldset>

書中建立了一個對照表(map),記錄這些元素及其所需的包裹層級和父容器 HTML。

將元素插入文件#

為了減少插入操作的數量,使用 DOM fragments(文件片段):

  • DOM fragments 是 W3C DOM 規範的一部分,作為 DOM 節點的容器
  • Fragment 可以在單次操作中被注入和複製,大幅減少所需的操作數量
  • 若需將同一 fragment 插入多個位置,需使用 cloneNode(true) 複製
const fragment = doc.createDocumentFragment();
// 將節點加入 fragment
// 單次操作即可插入所有節點

12.2 使用 DOM 屬性與特性#

存取元素值有兩種方式:

  • 傳統 DOM 方法:getAttributesetAttribute
  • DOM 物件的屬性(property)直接存取
e.getAttribute("id"); // 透過 attribute 存取
e.id; // 透過 property 存取

DOM 元素的 attribute 和 property 雖然連結在一起,但並非總是相同的。修改 id property 會同步改變 id attribute,反之亦然。但對於自訂屬性(custom attributes),它們不會自動成為 element properties,必須使用 getAttribute()setAttribute() 來存取。

在 HTML5 中,自訂屬性應使用 data- 前綴,這是將自訂屬性與原生屬性明確區分的良好慣例。

12.3 樣式屬性的難題#

樣式資訊的來源#

元素的 style 屬性是一個物件,持有與元素標記中 style 屬性對應的樣式值。但要注意:

  • style 物件只反映行內樣式(inline style)和透過腳本設定的值
  • 來自 <style> 元素或外部樣式表的值不會出現在 style 物件中
  • style property 中的值優先於樣式表繼承的值(即使使用 !important

Figure 12.1: CSS 樣式表中 inline 和 assigned 樣式有記錄,但 inherited 樣式無法直接取得

樣式屬性命名#

CSS 中使用連字號的多字屬性名(如 font-sizebackground-color),在 JavaScript 中會轉換為 camelCase 形式:

  • font-sizefontSize
  • background-colorbackgroundColor

書中展示了一個同時支援 getter/setter 的 style() 工具函式,自動處理 camelCase 轉換。

取得計算樣式#

Computed style 是瀏覽器內建樣式、樣式表、style 屬性與腳本修改的綜合結果。使用 getComputedStyle 方法取得:

const computedStyles = getComputedStyle(element);
computedStyles.getPropertyValue("background-color");

getPropertyValue 接受 CSS 屬性名稱格式(如 font-size),而非 camelCase 版本。書中的 fetchComputedStyle 函式同時支援兩種格式。

Figure 12.2: 元素最終的樣式來自多個來源:瀏覽器預設、外部樣式表、行內樣式等

Figure 12.3: Computed styles 包含所有指定與繼承的樣式

合併屬性(Amalgam Properties)#

CSS 允許使用簡寫(如 border: 1px solid crimson),但在取得計算樣式時,必須取得低階的個別屬性(如 border-top-colorborder-top-width),無法直接取得 border

轉換像素值#

設定數值型樣式屬性時,必須指定單位才能跨瀏覽器可靠運作:

element.style.height = "10px"; // 安全
element.style.height = 10 + "px"; // 安全
element.style.height = 10; // 不安全

某些屬性使用非像素的數值,不應自動加上 pxz-indexfont-weightopacityzoomline-height

測量高度與寬度#

  • heightwidth 預設為 auto,無法從 style 屬性取得準確值
  • 使用 offsetHeightoffsetWidth 取得元素的實際尺寸(含 padding)
  • 隱藏元素(display: none)的 offset 值為 0

測量隱藏元素的技巧:

  1. display 改為 block
  2. visibilityhidden
  3. positionabsolute
  4. 讀取尺寸
  5. 還原所有變更的屬性

檢查 offsetWidthoffsetHeight 是否為零,可作為判斷元素可見性的高效方式。

Figure 12.4: 使用可見與隱藏的圖片來測試取得隱藏元素尺寸

Figure 12.5: 暫時調整 style 屬性來測量隱藏元素的尺寸

12.4 最小化 Layout Thrashing#

Layout thrashing 發生在連續執行 DOM 讀取和寫入操作時,迫使瀏覽器反覆重新計算佈局,導致效能下降。

瀏覽器通常會延遲佈局計算,盡量批次處理 DOM 寫入操作。但如果在每次 DOM 修改後立即讀取佈局資訊,就會強制觸發重新計算。

造成 layout thrashing 的範例:

// 不好的做法:交替讀寫
const ninjaWidth = ninja.clientWidth;
ninja.style.width = ninjaWidth / 2 + "px";

const samuraiWidth = samurai.clientWidth; // 強制重算佈局
samurai.style.width = samuraiWidth / 2 + "px";

批次處理以避免 thrashing:

// 好的做法:先集中讀取,再集中寫入
const ninjaWidth = ninja.clientWidth;
const samuraiWidth = samurai.clientWidth;
const roninWidth = ronin.clientWidth;

ninja.style.width = ninjaWidth / 2 + "px";
samurai.style.width = samuraiWidth / 2 + "px";
ronin.style.width = roninWidth / 2 + "px";

會觸發佈局失效的常見 API 和屬性包括:

  • ElementclientHeightclientWidthoffsetHeightoffsetWidthscrollHeightgetBoundingClientRectfocus
  • WindowgetComputedStylescrollToscrollY
  • MouseEventlayerXlayerYoffsetXoffsetY

React 的 Virtual DOM 就是透過在虛擬 DOM 上執行所有修改,然後在適當時機批次更新實際 DOM,來避免 layout thrashing 並提升效能。

12.5 本章重點#

  • 將 HTML 字串轉為 DOM 元素需要:預處理自閉合標籤、包裹必要的外層標記、透過 innerHTML 插入、取出節點
  • 使用 DOM fragments 可大幅減少插入操作的數量
  • DOM attribute 和 property 連結但不總是相同;自訂屬性不會自動成為 properties
  • style property 只包含行內和腳本設定的樣式;使用 getComputedStyle 取得完整的計算樣式
  • 使用 offsetWidth/offsetHeight 取得元素尺寸
  • 批次處理 DOM 更新以避免 layout thrashing