第12章 DOM操作
阿新 • • 發佈:2020-11-28
目錄
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>