分享

DOM系列:DOM樹和遍歷DOM

 ipilipala 2018-09-30

上一節,咱們整理了DOM系列中的第一篇,主要介紹瀏覽器與DOM相關的知識。從標題中我們可以看出來,今天所要學的東西包含兩個部分,第一部分是DOM樹,第二部分是遍歷DOM。如果你和我一樣對於DOM樹和遍歷DOM是初次接觸,那個人建議您花點時間好好看看這兩部分的知識。

DOM樹

眾所周之,HTML文檔的主幹就是標記(也就是大家熟悉的HTML標籤元素)。

根據文檔對象模型(即:DOM),每個HTML標籤事實上都是一個對象。嵌套的標籤被稱為元素(或子標籤)。除此之外,標籤內的文本也是一個對象。而這些對象都可以使用JavaScript訪問。

那麼啥是DOM樹呢?我們先來看看現實生活中的例子。想像一棵與所有世代有關係的家庭樹(大家熟知的族譜),其包括了:祖父母、父母、孩子、兄弟姐妹等等。我們通常以等級的方式組織豪庭樹(族譜)。

上圖是一個家族族譜的圖。其中TossicoAkikazuHitomiTakemi是祖父母。而Toshiakijuliana是父母。另外TKYujiBrunoKaio是父母的孩子(其實也是我的兄弟姐妹們)。

除了家族族譜之外,生活中還有另一個示例,那就是一個組織的結構層次,比如:

而在HTML中,DOM其實也類似一棵樹的,它和前面所舉例的家族族譜,組織機構圖是類似的,HTML中DOM就是一棵樹

DOM的一個示例

我們來看一個DOM的示例,比如下面這樣的一個HTML文檔:

<!DOCTYPE HTML>
<html>
    <head>
        <title>About elks</title>
    </head>
    <body>
        The truth about elks.
    </body>
</html>

DOM將HMTML表示為標記的樹結構(也就是大家所說的DOM樹),就如下面這樣的樣子:

在上面的圖中,你可以單擊元素的節點,它們的子節點可以展開或者收縮,如下圖所示:

HTML的標籤被稱為元素(element)節點(或只是元素)。嵌套標籤成為一個子元素(也被稱為子)。因此,對於一個HTML文檔而言,<html>是一個根節點(也被稱為根元素),然後<head><body><html>的子元素。

元素內的文本被稱這文本節點,標記為#text。文本節點僅包含一個字符串。它可能沒有子元素,也就是說它永遠只是樹的葉子(沒有成為樹枝的可能)。

除此之外,要注意文本節點中的兩個特殊字符:

  • 換行符:(對應JavaScript中的\n
  • 空白符:

空格和換行符是完全有效的字符,它們形成文本節點並成為DOM的一部分。因此,例如在<head>標籤之上的示例中,在<title>這前包含了一些空格,並且該文本成為一個#text節點(它只包含一條換行符和一些空格)。

不過要注意的是,有兩個將會除外:

  • <head>標籤之前的空格和換行符由於歷史原因將被忽略
  • 如果我們將一些東西放在</body>之後,那麼它就會自動地移到</body>的前面,正如HTML規範要求的一樣,所有內容必須在</body>中一樣。因此,在</body>之後可能沒有空格

在其他情況之下,一切都很簡單。如果文檔中有空格(就像任何字符一樣),那麼它們就會成為DOM中的文本節點,如果我們刪除它們,那麼就不會有任何東西,也不再會有空格符或換行符的節點。

比如下面這個示例:

<!DOCTYPE HTML>
<html><head><title>About elks</title></head><body>The truth about elks.</body></html>

上面的HTML結構對應的DOM樹如下圖所示:

相比上面的截圖可以看出來,沒有了空格符和換行符的文本節點。

通過上面的示例,可能你對DOM樹有一定的瞭解了。但對一些一技術的定義估計還不是非常的瞭解,接下來花點時間來說一下DOM中的一些技術定義。

DOM中的技術定義

DOM樹(tree)是一個DOM節點(nodes)的集合(拿到生活中來說,樹是稱為節眯的實體集合)。而其中節點由邊(edges)連接。每個節點(node)都包含一個值(value)或數據(data),它可能或有可能沒有子節點(child node)。

treefirst node稱為root節點。如果root節點由另一個節點連接,則root節點是父節點,連接的節點是子節點。

所有的樹節點(Tree nodes)都被edges連接在一起。它是樹(trees)的重要組成部分,因為它管理節點(nodes)之間的關係。

對於一棵樹而言,葉子(leaes)是樹(tree)上的最後一個節點(nodes)。它們是沒有子節點。就像真正的樹一樣,DOM也是有根(root)、枝(Element)和葉子(文本節點)。

除此之外,其他還需要理解的重要概念是高度(height)和深度(depth)。樹的高度是葉子最長路徑的長度;節點的深度是路徑到其根的長度。用下圖來闡述會更形象一些:

簡單的總結一些術語:

  • root(根節點)是樹(tree)最頂端的節點
  • edge(邊緣)是兩個節點(node)之間的連接
  • child(子節點)是具有父節點的節點
  • parent(父節點)是一個節點,它具有子節點的邊緣
  • leaf(樹葉)是樹中沒有子節點的節點
  • height(高度)是葉子最長路徑的長度
  • depth(深度)是路徑到其根的長度

有關於這方面更深入的介紹可以閱讀@TK的《Everything you need to know about tree data structures》一文。

另外@TK的文章還涉及到了深度優先遍歷廣度優秀遍歷,有關於這兩個概念的深入介紹,可以閱讀:

其實有關於深度優先遍歷廣度優秀遍歷在DOM樹中的作用並不明顯,對於後續的DOM遍歷還是有很大的影響。

經過的上面的學習,我們對於DOM樹有了一定的瞭解。除此之外,瀏覽器對於DOM還具有自動較正的特性。

自動校正

如果瀏覽器遇到格式錯誤的HTML,它會自動更正它(校正)。

例如,HTML最頂端的標籤總是<html>。即使它不存在文檔中 —— 它將存在DOM中,瀏覽器也會創建它。另外<body>也是一樣。

例如,HTML文件只包含一個單詞Hello,瀏覽器將它放置在成<html><body>中,並且也會添加所需的<head>。其DOM將是:

另外,生成DOM時,瀏覽器會自動處理文檔中的錯誤,比如關閉標籤等等。比如下面這樣一個無效的文檔:

<p>Hello
<li>Mom
<li>and
<li>Dad

事實上,瀏覽器渲染時,它照樣會成為一個正常的 DOM,那是因為瀏覽器讀取標籤並會自動修復丟失的部分(比如說關閉標籤):

除此之外,還有一個有趣的“特殊情況”,那就是table(表格元素)。根據DOM規範,它必須有<tbody>,但是如果你在HTML文檔中忘記寫該標籤元素時,瀏覽器會自動在DOM中添加<tbody>標籤。比如:

<table id="table"><tr><td>1</td></tr></table>

此時瀏覽器渲染出來的DOM結構如下:

其他節點類型

我們可以在一個HTML文檔中添加更多的標籤和在頁面中添加註釋,比如:

<!DOCTYPE HTML>
<html>
    <body>
        The truth about elks.
        <ol>
            <li>An elk is a smart</li>
            <!-- comment -->
            <li>...and cunning animal!</li>
        </ol>
    </body>
</html>

對於上面的HTML文檔,其對應的DOM樹如下圖所示:

上圖中,我們看到了一個新的節點類型 —— 註釋節點,標記為 #comment

你可能會想,為什麼要將註釋添加到DOM中呢?它不會以任何方式影響視覺上的效果,但是有一個規則,如果某個東西在HTML中,那麼它也必須在DOM樹中。

HTML中的一切,甚至是註釋,都將成為DOM的一部分。

即使是<!DOCTYPE ...>指令也是一個DOM節點。它在DOM樹中,在<html>之前。我們不會去觸摸那個節點,我們甚至不會在圖上畫它,但它卻實實大大的存在那裡。

document對象也是一個DOM節點,表示整個文檔。在DOM中,其有12種節點類型。在實際操作中,我們通常使用4種方法:

  • document:進入DOM的入口點
  • 元素節點:HTML標籤,樹構建塊
  • 文本節點:包含文本
  • 註釋:有時候我們可以把信息放在這裡,但它不會顯示出來,不過JavaScript卻可以從DOM中讀取它

或許你和我一樣,希望能對每個HTML文檔對應的DOM結構能實時的查看,我們希望有對應的工具能幫助我們。事實上是有類似這樣的工具,比如 Live DOM Viewer。只要輸入文檔,它就會立即顯示DOM樹結構。

DOM中的空白符

DOM 中的空白符會讓處理節點結構時增加不少麻煩。在Mozilla 的軟件中,原始文件裡所有空白符都會在 DOM 中出現(不包括標籤內含的空白符)。這樣的處理方式有其必要之處,一方面編輯器中可逕行排列文字、二方面 CSS 裡的 white-space: pre 也才能發揮作用。 如此一來就表示:

  • 有些空白符會自成一個文本節點。
  • 有些空白符會與其他文本節點合成為一個文本節點。

換句話說,下面這段 HTML 代碼對應的 DOM 節點結構會如附圖所示,其中\n代表換行符:

<!-- My document -->
<html>
    <head>
        <title>My Document</title>
    </head>
    <body>
        <h1>Header</h1>
        <p>
            Paragraph
        </p>
    </body>
</html>

對應的DOM樹,如下圖所示:

這麼一來,要使用 DOM 遊走於節點結構間又不想要無用的空白符時,會有點困難。

以下的 JavaScript 代碼定義了許多函數,能夠讓你在處理 DOM 中的空白符時輕鬆點:

/**
* 以下所謂的“空白符”代表:
*  "\t" TAB \u0009 (製表符)
*  "\n" LF  \u000A (換行符)
*  "\r" CR  \u000D (回車符)
*  " "  SPC \u0020 (真正的空格符)
*
* 不包括 JavaScript 的“\s”,因為那代表如不斷行字符等其他字符。
*/

/**
* 測知某節點的文字內容是否全為空白。
*
* @參數   nod  |CharacterData| 類的節點(如  |Text|、|Comment| 或 |CDATASection|)。
* @傳回值      若 |nod| 的文字內容全為空白則傳回 true,否則傳回 false。
*/
function is_all_ws( nod ) {
    // Use ECMA-262 Edition 3 String and RegExp features
    return !(/[^\t\n\r ]/.test(nod.data));
}


/**
* 測知是否該略過某節點。
*
* @參數   nod  DOM1 |Node| 對象
* @傳回值      若 |Text| 節點內僅有空白符或為 |Comment| 節點時,傳回 true,
*              否則傳回 false。
*/

function is_ignorable( nod ) {
    return ( nod.nodeType == 8) || // 註釋節點
        ( (nod.nodeType == 3) && is_all_ws(nod) ); // 僅含空白符的文字節點
}

/**
* 此為會跳過空白符節點及註釋節點的 |previousSibling| 函數
* ( |previousSibling| 是 DOM 節點的特性值,為該節點的前一個節點。)
*
* @參數   sib  節點。
* @傳回值      有兩種可能:
*               1) |sib| 的前一個“非空白、非註釋”節點(由 |is_ignorable| 測知。)
*               2) 若該節點前無任何此類節點,則傳回 null。
*/
function node_before( sib ) {
    while ((sib = sib.previousSibling)) {
        if (!is_ignorable(sib)) return sib;
    }
    return null;
}

/**
* 此為會跳過空白符節點及註釋節點的 |nextSibling| 函數
*
* @參數   sib  節點。
* @傳回值      有兩種可能:
*               1) |sib| 的下一個“非空白、非註釋”節點。
*               2) 若該節點後無任何此類節點,則傳回 null。
*/
function node_after( sib ) {
    while ((sib = sib.nextSibling)) {
        if (!is_ignorable(sib)) return sib;
    }
    return null;
}

/**
* 此為會跳過空白符節點及註釋節點的 |lastChild| 函數
* ( lastChild| 是 DOM 節點的特性值,為該節點之中最後一個子節點。)
*
* @參數   par  節點。
* @傳回值      有兩種可能:
*               1) |par| 中最後一個“非空白、非註釋”節點。
*               2) 若該節點中無任何此類子節點,則傳回 null。
*/
function last_child( par ){
    var res=par.lastChild;
    while (res) {
        if (!is_ignorable(res)) return res;
        res = res.previousSibling;
    }
    return null;
}

/**
* 此為會跳過空白符節點及註釋節點的 |firstChild| 函數
*
* @參數   par  節點。
* @傳回值      有兩種可能:
*               1) |par| 中第一個“非空白、非註釋”節點。
*               2) 若該節點中無任何此類子節點,則傳回 null。
*/
function first_child( par ){
    var res=par.firstChild;
    while (res) {
        if (!is_ignorable(res)) return res;
        res = res.nextSibling;
    }
    return null;
}

/**
* 此為傳回值不包含文字節點資料的首尾所有空白符、
* 並將兩個以上的空白符縮減為一個的 |data| 函數。
*( data 是 DOM 文字節點的特性值,為該文字節點中的資料。)
*
* @參數   txt 欲傳回其中資料的文字節點
* @傳回值     文字節點的內容,其中空白符已依前述方式處理。
*/
function data_of( txt ) {
    var data = txt.data;
    // Use ECMA-262 Edition 3 String and RegExp features
    data = data.replace(/[\t\n\r ]+/g, " ");
    if (data.charAt(0) == " ")
        data = data.substring(1, data.length);
    if (data.charAt(data.length - 1) == " ")
        data = data.substring(0, data.length - 1);
    return data;
}

DOM的遍歷

如果你閱讀了上面的的內容,或許你已經意識到,DOM看起來就像一個巨大的樹 —— 一棵巨大的樹,它的元素掛載在樹枝上。為了獲得更多的技術,DOM中的元素被安排在一個層次結構中,它定義了你最終在瀏覽器中看到的內容:

這個層次結構用於幫助我們組織HTML元素。它還用於幫助你的CSS樣式規則理解什麼樣式適用於哪些東西。從JavaScript角度來看,這個層次結構確實增加了一點複雜性。你會花相當多的時間去弄清楚你現在所有的DOM和你需要去的地方。當我們考慮創建新的元素或移動元素時,這將變得更加明顯。這種複雜性是你需要適應的。

找到你的方式

在你找到元素並與它們做一些事情之前,你首先需要瞭解元素的位置。我解決這個問題,最簡單的方法就是從頭開始,然後一路向下。這就是我們要做的。

為了更易於幫助在大家理解,先回到上一節中的示例中

<!DOCTYPE html> 
<html> 
    <head> 
        <meta content="DOM, JavaScript, W3cplus" name="keywords" /> 
        <meta content="DOM系列,瀏覽器和DOM!" name="description" /> 
        <title>LOL! Sea Otter! Little Kid!</title> 
        <link href="style.css" rel="stylesheet"/> 
    </head> 
    <body> 
        <div id="container"> 
            <img src="w3cplus_logo.png"/> 
            <h1>DOM系列學習!</h1> 
            <p class="bodyText">開始學習DOM,這是一個有關於DOM學習的系列教程...<p> 
            <div class="submitButton">next</div> 
        </div> 
    <script src="main.js"></script> 
    </body> 
</html>

來自DOM頂部的視圖由windowdocumenthtml元素組成:

由於這三樣東西的重要性,DOM為你提供了通過windowdocumentdocument.documentElement訪問它們的方法。

var windowObject = window; 
var documentObject = document;  
var htmlElement = document.documentElement;

需要注意的一點是,windowdocument都是全局屬性。不必要明確的聲明它們,可以直接從容器裡拿出來用就行了。

往往,最頂層的樹節點可以直接作為document屬性使用,比如:

<html> = document.documentElement

頂部文檔節點document.documentElement,其對應的就是<html>的DOM節點。另外一個廣泛使用的DOM節點是<body>元素,其對應的是document.body

<body> = document.body

同樣的,<head>標籤可以用document.head

不過有一點需要注意:

document.body有可能為null。當腳本在訪問不存在的元素時,返回的值將會為null

比如,當你的腳本在</head>中運行,比如document.body是將返回的值將是null,因為瀏覽器還沒有讀取它。但在</body>中的<script>中返回的則是<body>元素:

<html>
    <head>
        <script>
            console.log('From head:', document.body) // => null
        </script>
    </head>
    <body>
        <script>
            console.log('From body:', document.body) // => HTMLBodyElement
        </script>
    </body>
</html>

上面我們所看到的是htmlheadbody元素的獲取。但事實上,一旦你進入HTML元素級別,你的DOM將開始分支並變得更有趣。在這一點上,你有幾種獲取DOM的方式。通過使用querySelector()querySelectorAll()可以幫助你精確地獲取你想要獲取的DOM元素。或許你已經在項目中大量使用這兩種方法了。但事實上,對於許多實際案例來說,這兩種方法太過侷限。

有時候,你不知道你想去哪裡。querySelector()querySelectorAll()主法在這裡無法幫助您。你只想上車然後開車,並想找到你想要去的地方。回到DOM的世界當中時,你會發現自己一直處理這個位置。這就是DOM提供的各種內置屬性,所有的Motorcycle Diaries將會幫助你,接下來我們將看看這些屬性。

能夠幫助你的是知道所有的DOM元素都至少有一個組合,包括父母(Parents)兄弟姐妹(Siblings)子元素(Children)。為了更直觀的幫助大家理解,來看下圖,下圖中包含divscript的一個樹形圖:

divscript兄弟元素。他們是兄弟元素的原因是他們共有一個相同的父元素bodyscript元素沒有子元素,但是div元素有四個子元素,imgh1pdiv。這四個元素也相互被稱為兄弟元素,同樣的是因為他們有相同的父元素。這其實很好理解,如果你閱讀了文章前面的DOM樹相關的內容,你會發現它們就像現實的生活中一樣,父母、孩子 和兄弟姐妹的關係基於你所關注的樹的位置(對應的就是家族族譜)。幾乎每個元素,取決於你看它們的角度,可以扮演多個家庭角色。

為了更好的理解,DOM中提供了一些對應的屬性(這些屬性具有一定的依賴關係)。包括:firstChildlastChildparentNodechildrenpreviousSiblingnextSibling。從他們的名稱上來看,就可以推出這些屬性的作用。這幾個屬性結合在一起將構建一個DOM遍歷鏈接圖,允許在DOM節點間找到你想要找到的DOM:

為了更好的理解DOM遍歷相關的知識點,咱們接下來將圍繞這幾個屬性來展開。

兄弟姐妹和父母打交道

在這些DOM屬性中,最容易處理的是父母和兄弟姐妹。對應的屬性有parentNodepreviousSiblingnextSibling。下面這張圖將幫助你瞭解這三個屬性是如何工作的:

這張圖雖然有點零亂,但是你仔細看的話,你可以理清楚它們之間的關係,以及他們之間發生了什麼。parentNode屬性指向元素的父元素。previousSiblingnextSibling屬性允許元素元素它的前一個或下一個兄弟元素。你可以在圖中看到箭頭的方向指向。最後一行,imgnextSiblingdiv,相應的divpreviousSiblingimg元素。不管是通過imgdivparentNode屬性都將把我們帶入到第二行中的div(事實上就是imgdiv的元素)。通過上圖,大家理解起來是不是很簡單。

子元素打交道

上面咱們看到的是如何通過DOM的屬性來訪問兄弟元素和父元素,事實上,除此之外,DOM還提供一些屬性可以訪問元素的子元素,比如firstChildlastChildchildren。同樣用一圖來向大家展示:

firstChildlastChild屬性是指父元素的第一個和最後一個子元素。如果父類只有一個子元素,就像例子中的body元素一樣,firstChildlastChild都指向相同的元素div。如果一個元素沒有子元素,那麼firstChildlastChild屬性將返回一個null

與其他屬性相比,其中children屬性相對而方要更為複雜一些。當你在父類上訪問children屬性時,基本上會得到父元素的子元素集合。這個集合並不是數組,但它確實有一些類似數組的能力,就是大家所說的類數組,其具有length屬性,可以通過[]item()來索引集合中具體的元素。比如上圖中的div.children[0]訪問到的是第一個img元素。

在DOM中獲取子節點,除了前面提到的三個屬性之外,還有一個childNodes屬性,不過它和children有一個很明顯的區別:

children只獲取子節點(即子元素),而childNodes除了獲取的子節點還包括文本節點

除此之外,還有一個特殊的函數hasChildNodes()可以用來判斷某個元素是否包含子節點。

這個時候,你把它們放在一起,你就可以對DOM進行遍歷。也可以做一些事情。比如,檢查某個元素是否有子元素存在,我們就可以這樣做:

let bodyElement = document.body

if (bodyElement.firstChild) {
    // 這裡做你想做的事情...
}

如果body沒有子節點,那麼if語獎將返回null。當然,你也可以使用bodyElement.lastChildbodyElement.children做為if語句的條件。

再來看另一個簡單示例,前面提到過了,通過children可以獲取某個元素的所有子節點(前提是這個元素有子元素存在)。這個時候得到的是一個類數組,如查你要獲取到該元素中的每個子節點,就需要使用for循環來處理:

var bodyElement = document.body;

for (var i = 0; i < bodyElement.children.length; i++) {
    var childElement = bodyElement.children[i];

    document.writeln(childElement.tagName);
}

通過元素遍歷DOM

上面咱們看到的是通過DOM節點來遍歷DOM。比如childNodes屬性,除了可以獲取元素節點之外,還可以獲取文本節點,甚至是註釋節點。但很多時候,對於DOM的操作,咱們只需要獲取想要的DOM元素節點,而不需要考慮文本和註釋節點。這個時候咱們只需要操作元素節點即可,這對應DOM中操作元素節點的一些屬性。同樣的使用下圖來向大家闡述,易於理解:

和前面相比,這裡的屬性多了Element這個詞,其對應的含義:

  • children:元素節點的子元素
  • firstElementChildlastElementChild:元素的第一個或最後一個子元素
  • previousElementSiblingnextElementSibling:元素的前一個或後一個相鄰元素
  • parentElement:元素的父元素

總結

這篇文章是DOM系列的第二篇文章,主要介紹了DOM樹和DOM的遍歷。前一部分只要介紹了DOM樹,簡單的理解,任何一個HTML文檔都可以類似於家族的族普來繪製對應的DOM樹。通過DOM樹可以理清楚每個DOM元素(或者說DOM節點)之間的關係。比如,父子關係、兄弟關係等。

另外,在DOM中找到對應的元素是每位JavaScript開發人員都應該需要掌握的技巧之一。這篇文章的後一部分主要向大家介紹了如何對DOM進行遍歷,其實就是通過DOM的屬性怎麼獲取DOM的元素或節點。簡單的歸納一下,分為:

  • 向上獲取,比如parentNodeparentElementclosest
  • 向下獲取,比如querySelector()querySelectorAll()childrenfirstChildrenlastChildrenchildNodes
  • 兄弟元素(節點),比如nextElementSiblingpreviousElementSiblingnextSiblingpreviousSibling

如果文中有不對之處,或者你有更好的經驗,歡迎在下面的評論中與我們一起分享。最後要說明的是,文章中有些圖片來自互聯網,如涉及侵權,煩請告之。

擴展閱讀

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多