操作 DOM#
DOM(Document Object Model)是實現高度動態網頁應用程式的主要手段之一。理解 DOM 操作在函式庫中的運作方式,能讓我們寫出更好、更快的程式碼。本章探討高效修改 DOM 的技術,涵蓋 HTML 注入、屬性與特性的差異、樣式處理,以及避免 layout thrashing 的效能最佳化。
12.1 將 HTML 注入 DOM#
將 HTML 字串插入文件的任何位置,是建構高度動態網頁的常見需求。相較於逐一使用 DOM API(createElement、appendChild),直接注入 HTML 字串更為簡潔高效。
將 HTML 轉換為 DOM#
透過 innerHTML 屬性將 HTML 字串轉為 DOM 結構,步驟如下:
- 確保 HTML 字串包含合法的 HTML 程式碼
- 依瀏覽器規則用必要的外層標籤包裹字串
- 使用
innerHTML將字串插入一個暫時的 DOM 元素 - 取出建立好的 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 方法:
getAttribute和setAttribute - DOM 物件的屬性(property)直接存取
e.getAttribute("id"); // 透過 attribute 存取
e.id; // 透過 property 存取
DOM 元素的 attribute 和 property 雖然連結在一起,但並非總是相同的。修改
idproperty 會同步改變idattribute,反之亦然。但對於自訂屬性(custom attributes),它們不會自動成為 element properties,必須使用getAttribute()和setAttribute()來存取。
在 HTML5 中,自訂屬性應使用
data-前綴,這是將自訂屬性與原生屬性明確區分的良好慣例。
12.3 樣式屬性的難題#
樣式資訊的來源#
元素的 style 屬性是一個物件,持有與元素標記中 style 屬性對應的樣式值。但要注意:
style物件只反映行內樣式(inline style)和透過腳本設定的值- 來自
<style>元素或外部樣式表的值不會出現在style物件中 styleproperty 中的值優先於樣式表繼承的值(即使使用!important)

Figure 12.1: CSS 樣式表中 inline 和 assigned 樣式有記錄,但 inherited 樣式無法直接取得
樣式屬性命名#
CSS 中使用連字號的多字屬性名(如 font-size、background-color),在 JavaScript 中會轉換為 camelCase 形式:
font-size→fontSizebackground-color→backgroundColor
書中展示了一個同時支援 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-color、border-top-width),無法直接取得 border。
轉換像素值#
設定數值型樣式屬性時,必須指定單位才能跨瀏覽器可靠運作:
element.style.height = "10px"; // 安全
element.style.height = 10 + "px"; // 安全
element.style.height = 10; // 不安全
某些屬性使用非像素的數值,不應自動加上 px:z-index、font-weight、opacity、zoom、line-height。
測量高度與寬度#
height和width預設為auto,無法從style屬性取得準確值- 使用
offsetHeight和offsetWidth取得元素的實際尺寸(含 padding) - 隱藏元素(
display: none)的 offset 值為0
測量隱藏元素的技巧:
- 將
display改為block - 設
visibility為hidden - 設
position為absolute - 讀取尺寸
- 還原所有變更的屬性
檢查
offsetWidth和offsetHeight是否為零,可作為判斷元素可見性的高效方式。

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 和屬性包括:
- Element:
clientHeight、clientWidth、offsetHeight、offsetWidth、scrollHeight、getBoundingClientRect、focus等 - Window:
getComputedStyle、scrollTo、scrollY - MouseEvent:
layerX、layerY、offsetX、offsetY
React 的 Virtual DOM 就是透過在虛擬 DOM 上執行所有修改,然後在適當時機批次更新實際 DOM,來避免 layout thrashing 並提升效能。
12.5 本章重點#
- 將 HTML 字串轉為 DOM 元素需要:預處理自閉合標籤、包裹必要的外層標記、透過
innerHTML插入、取出節點 - 使用 DOM fragments 可大幅減少插入操作的數量
- DOM attribute 和 property 連結但不總是相同;自訂屬性不會自動成為 properties
styleproperty 只包含行內和腳本設定的樣式;使用getComputedStyle取得完整的計算樣式- 使用
offsetWidth/offsetHeight取得元素尺寸 - 批次處理 DOM 更新以避免 layout thrashing