1. 程式人生 > 實用技巧 >第12章 DOM操作

第12章 DOM操作

目錄

1. 向DOM中注入HTML

1.1 將HTNL字串轉換成DOM

  • 轉換的步驟如下所示:
    • 確保HTML字串是合法有效的
    • 將它包裹在任意符合瀏覽器規則要求的閉合標籤中
    • 使用innerHTML將這串HTML插入到虛擬的DOM元素中
    • 提取該DOM節點

預處理HTML源字串

// 確保自閉合元素被正確解釋

// 單標籤
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 + ">";
    });
}

console.log(convert("<a/>"));
// <a></a>
console.log(convert("<hr />"));
// <hr />

包裝HTML

  • 根據HTML語義,一些HTML元素必須包裝在某些容器元素中。有兩種方式可以解決(都需要構建問題元素和容器之間的對映關係)
    • 通過innnerHTML將該字串直接注入到它的特定父元素中,該父元素提前使用內建的document.creatElemnet建立好
    • HTML字串可以在使用對應父元素包裝後,直接注入到任意容器元素中
  • 需要包裝在其他元素中的元素
元素名稱 父級元素
<option>, <optgroup> <select multiple>...</select>
<legend> <fieldset>...</fieldset>
<thead>, <tbody>, <tfoot>,
<colgroup>, <caption>
<table>...</table>
<tr> <table><thead>...</thead></table>
<table><tbody>...</tbody></table>
<table><tfoot>...</tfoot></table>
<td>, <th> <table><tbody><tr>...</tr></tbody></table>
<col> <table>
<tbody></tbody>
<colgroup>...</colgroup>
</table>

使用具有multiple屬性的<select>元素,因為它不會自動檢查任何包含在其中的選項
對<col>的相容處理需要一個額外的,否則<colgroup>不能正確生成

// 將元素標籤轉換為一系列DOM節點
function getNodes(htmlString, doc) {
    // 需要特殊父級容器的元素對映表。
    // 每個條目包含新節點的深度,以及父元素的HTML頭尾片段
    const map = {
        "<td": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
        "<th": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
        "<tr": [2, "<table><thead>", "</thead></table>"],
        "<option": [1, "<select multiple>", "</select>"],
        "<optgroup": [1, "<select multiple>", "</select>"],
        "<thead": [1, "<table>", "</table>"],
        "<tbody": [1, "<table>", "</table>"],
        "<tfoot": [1, "<table>", "</table>"],
        "<colgroup": [1, "<table>", "</table>"],
        "<caption": [1, "<table>", "</table>"],
        "<col": [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"]
    }
    const tagName = htmlString.match(/<\w+/);
    let mapEntry = tagName ? map[tagName[0]] : null;
    // 如果對映表中有匹配,使用匹配結果
    // 如果沒有,則構造空的父標記,深度為0作為結果
    if (!mapEntry) { mapEntry = [0, "", ""] }
    // 建立用於包含新節點的元素,如果傳入了文件物件,則使用傳入的,否則使用當前的
    let div = (doc || document).createElement("div");
    // 使用匹配得到的父級容器元素,包裝後注入新建立的元素中
    div.innerHTML = mapEntry[1] + htmlString + mapEntry[2];
    // 參照對映關係定義的深度,向下遍歷剛剛建立的DOM樹,最終得到新建立的元素
    while (mapEntry[0]--) {
        div = div.lastChild;
    }
    // 返回新建立的元素
    return div.childNodes;
}

1.2 將DOM元素插入到文件中

// 新增frgment引數,新增節點將被新增到這個DOM片段中
function getNodes(htmlString, doc, fragment) {
    const map = {
        "<td": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
        "<th": [3, "<table><tbody><tr>", "</tr></tbody></table>"],
        "<tr": [2, "<table><thead>", "</thead></table>"],
        "<option": [1, "<select multiple>", "</select>"],
        "<optgroup": [1, "<select multiple>", "</select>"],
        "<thead": [1, "<table>", "</table>"],
        "<tbody": [1, "<table>", "</table>"],
        "<tfoot": [1, "<table>", "</table>"],
        "<colgroup": [1, "<table>", "</table>"],
        "<caption": [1, "<table>", "</table>"],
        "<col": [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"]
    }
    const tagName = htmlString.match(/<\w+/);
    let mapEntry = tagName ? map[tagName[0]] : null;
    if (!mapEntry) { mapEntry = [0, "", ""] }
    let div = (doc || document).createElement("div");
    div.innerHTML = mapEntry[1] + htmlString + mapEntry[2];
    while (mapEntry[0]--) {
        div = div.lastChild;
    }
    // 新增節點到DOM片段中
    if (fragment) {
        while (div.firstChild) {
            fragment.appendChild(div.firstChild);
        }
    }
    return div.childNodes;
}
<div id="test"><b>Hello</b>, I'm Wango!</div>
<div id="test2"></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
    
    // 在DOM的國歌位置插入DOM片段
    function insert(elems, args, callback) {
        if (elems.length) {
            const doc = elems[0].ownerDocument || elems[0];
            const fragment = doc.createDocumentFragment();
            const scripts = getNodes(args, doc, fragment);
            const first = fragment.firstChild;

            if (first) {
                for (let i =0; elems[i]; i++) {
                    callback.call(elems[i], i > 0 ? fragment.cloneNode(true) : fragment);
                }
            }
        }
    }

    const divs = document.querySelectorAll("div");
    insert(divs, "<b>Name:</b>", function(fragment) {
        this.appendChild(fragment);
    });

    insert(divs, "<span>First</span><span>Last</span>", function (fragment) {
        this.parentNode.insertBefore(fragment, this);
    });
});
</script>

2. DOM的特性和屬性

通過DOM方法和屬性訪問特性值

<div></div>
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const div = document.querySelector("div");
        
        // HTML DOM的原生特性,通常能被屬性表示
        div.setAttribute("id", "news-01");
        console.log(div.id);
        // news-01
        console.log(div.getAttribute("id"));
        // news-01
        div.id = "news-02";
        console.log(div.getAttribute("id"));
        // news-02
        
        // 但自定義特性不能被元素屬性表示,需要使用
        // setAttribute()和getAttribute()
        div.setAttribute("data-news", "breaking");
        console.log(div.getAttribute("data-news"));
        // breaking
    });
</script>

在HTML5中,為遵循規範,建議使用data-作為自定義屬性的字首,方便區分自定義特性和原生特性

3. 令人頭疼的樣式特性

常用的style元素屬性是一個物件,該物件的屬性與元素標籤內指定的樣式相對應。

3.1 樣式在何處

<style>
    div {
        font-size: 1.8em;
        border: 0 solid gold;
    }
</style>
<div style="color: #000;" title="Hello"></div>
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const div = document.querySelector("div");
        // 內聯樣式被記錄
        console.log(div.style.color);
        // rgb(0, 0, 0)

        // <style>標籤內定義的的樣式沒有被記錄
        console.log(div.style.fontSize === "1.8em");
        // false
        console.log(div.style.borderWidth === "0");
        // false
        // 樣式物件中不反應從CSS樣式表中繼承的任何樣式資訊

        // 新賦值的樣式被記錄
        div.style.borderWidth = "10px";
        console.log(div.style.borderWidth);
        // 10px
    });
</script>

內聯樣式中的任何值,都優先於樣式表繼承的值(即使樣式表規則使用!important的註釋)

3.2 樣式屬性命名

一種訪問樣式的簡單方法

<div style="color: red;font-size: 10px;background-color: #eee;"></div>
<script>
    // 處理樣式函式
    // 如果傳入value,將相應樣式屬性值賦值為value
    // 如果沒有傳入value,則返回改樣式屬性值
    // 可以通過它來設定/讀取樣式屬性
    function style(elem, key, value) {
        // 將屬性名轉為駝峰格式
        // 以同時相容駝峰式和連字元式樣式名
        key = key.replace(/-([a-z])/ig, (all, letter) => {
            return letter.toUpperCase();
        });
        // 如果傳入value則將相應樣式屬性值設定為value
        if (typeof value !== "undefined") {
            elem.style[key] = value;
        }

        return elem.style[key];
    }

    document.addEventListener("DOMContentLoaded", () => {
        const div = document.querySelector("div");
        // 設定屬性
        style(div, "font-size", "20px");
        style(div, "background-color", "#000");

        console.log(div.style.fontSize === "20px");
        // true
        console.log(div.style.backgroundColor === "rgb(0, 0, 0)");
        // true

        // 讀取屬性
        console.log(style(div, "font-size"));
        // 20px
        console.log(style(div, "background-color"));
        // rgb(0, 0, 0)
    });
</script>

3.3 獲取計算後樣式

一個元素的計算後樣式(computed style)都是應用在該元素上的所有樣式的組合,這些樣式包括樣式表、元素的style內聯樣式、、瀏覽器內建樣式、JS指令碼對style所作的各種操作等

<style>
    div {
        background-color: #ffc;
        display: inline;
        font-size: 1.8em;
        border: 1px solid crimson;
        color: green;
    }
</style>

<div style="color: crimson;" id="test" title="hello"></div>

<script>
    // 用於獲取元素計算後屬性
    function fetchComputedStyle(elem, property) {
        // getComputedStyle是瀏覽器提供的全域性函式,可直接呼叫
        const computedStyle = getComputedStyle(elem);
        if (computedStyle) {
            // 將傳入的樣式名轉換為中橫線分割
            // 以同時相容駝峰式和連字元式樣式名
            property = property.replace(/([A-Z])/g, "-$1".toLowerCase());
            // getComputedStyle返回的物件提供了getPropertyValue方法
            // 這個方法接收中橫線分割格式的樣式名
            return computedStyle.getPropertyValue(property);
        }
    }

    document.addEventListener("DOMContentLoaded", () => {
        const div = document.querySelector("div");
        console.log(fetchComputedStyle(div, "background-color"));
        // rgb(255, 255, 204)
        console.log(fetchComputedStyle(div, "color"));
        // rgb(220, 20, 60)   返回的是內聯樣式中color的值,內聯樣式將css樣式覆蓋了
        console.log(fetchComputedStyle(div, "borderWidth"));
        // 1px
        console.log(fetchComputedStyle(div, "borderTop"));
        // 1px solid rgb(220, 20, 60)
    });
</script>

3.4 測量元素的高度和寬度

  • height和width的預設值都是auto,所以無法獲取準確的值
  • 使用offsetHeight和offsetWidth,但這兩個值包含了padding值
  • 隱藏元素(display: none)沒有尺寸,offsetHeight和offsetWidth為0
  • 獲取隱藏元素在非隱藏狀態下的尺寸可以先取消隱藏,獲取值,再隱藏,具體為:
    • 將display設定為block(可以獲取值了,但元素會可見)
    • 將visibility設定為hidden(使元素不可見,但元素位置會顯示一個空白)
    • 將position設定為absolute(將元素移出正常的可視區)
    • 獲取元素尺寸
    • 恢復先前更改的屬性
<div id="div1" style="display: none;width: 100px;height: 200px;background-color: #000;"></div>
<div id="div2" style="width: 300px;height: 400px;background-color: #00ff00;"></div>
<script>
    (function(scope) { // 使用立即執行函式建立私有作用域
        const PROPERTIES = {
            position: "absolute",
            visibility: "hidden",
            display: "block"
        }
        scope.getDimensions = elem => {
            const previous = {}; // 用於儲存原有屬性值
            for (let key in PROPERTIES) {
                previous[key] = elem.style[key];    // 儲存原有值
                elem.style[key] = PROPERTIES[key];  // 替換設定
            }
            const results = {   // 儲存結果
                width: elem.offsetWidth,
                height: elem.offsetHeight
            }
            for (let key in PROPERTIES) {   // 還原設定
                elem.style[key] = previous[key];
            }

            return results;
        }
    })(window);

    document.addEventListener("DOMContentLoaded", () => {
        const div1 = document.getElementById("div1");
        const div2 = document.getElementById("div2");

        console.log(getDimensions(div1).height);
        // 200
        console.log(getDimensions(div1).width);
        // 100
        console.log(getDimensions(div2).height);
        // 400
        console.log(getDimensions(div2).width);
        // 300
    });
</script>

檢查offsetHeight和offsetWidth屬性值是否為0,可以非常有效地確定一個元素的可見性

4. 避免佈局抖動

  • 抖動原因:程式碼對DOM執行一系列(通常是不必要的)連續的讀取和寫入時(瀏覽器執行大量重新計算),瀏覽器無法優化佈局操作

引起佈局抖動的API和屬性

介面物件 屬性名
Element clientHeight, clientLeft, clientTop, clientWidth,
focus, getBoundingClientRect, getClientRects, innerText,
offsetHeight. offsetLeft, offsetParent, offsetTop, offsetWidth,
outerText, scrollByLines, scrollByPages, scrollHeight,
scrollIntoView, scroollIntoViewIfNeeded,
scrollLeft, scrollTop, scrollWidth
MouseEvent layerX, layerY, offsetX, offsetY
Window getComputedStyle, scrollBy, scrollTo, scroll, scrollY
Frame,
Document, Image
height, width
<div id="div1">Hello</div>
<div id="div2">World</div>
<div id="div3">!!!!!!!!</div>

<script>
    // 獲取元素
    const div1 = document.getElementById("div1");
    const div2 = document.getElementById("div2");
    const div3 = document.getElementById("div3");

    // 執行一系列來納許的讀寫操作,修改DOM使得佈局失效
    const div1Width = div1.clientWidth;
    div1.style.width = div1Width/2 + "px";

    const div2Width = div2.clientWidth;
    div2.style.width = div2Width/2 + "px";

    const div3Width = div3.clientWidth;
    div3.style.width = div3Width/2 + "px";


    // 防抖的一種方法:批量讀寫

    // 批量讀取所有佈局屬性
    const div1Width = div1.clientWidth;
    const div2Width = div2.clientWidth;
    const div3Width = div3.clientWidth;

    // 批量寫入所有佈局屬性
    div1.style.width = div1Width/2 + "px";
    div2.style.width = div2Width/2 + "px";
    div3.style.width = div3Width/2 + "px";
</script>