瀏覽器DOM API有哪些?

簡(jiǎn)介

What

雖然網(wǎng)頁內(nèi)容使用各種標(biāo)簽平鋪而成的,但是在瀏覽器中它們卻不是同樣的平鋪結(jié)構(gòu),而是被解析為樹形結(jié)構(gòu),所有的標(biāo)簽按照層級(jí)關(guān)系建模構(gòu)建成一棵樹,這棵樹就被稱為“文檔對(duì)象模型”樹,英文為Document Of Object,簡(jiǎn)稱DOM樹。

Why

為什么要這么做?一方面是因?yàn)椤懊嫦驅(qū)ο蟆本幊痰膹?qiáng)大,幾乎所有在操作系統(tǒng)之上構(gòu)建的大型軟件都離不開這一思想和工具,另一方面是因?yàn)闃湫谓Y(jié)構(gòu)是對(duì)一切復(fù)雜層級(jí)系統(tǒng)的抽象,一本書的結(jié)構(gòu)、一個(gè)家族的人員關(guān)系、一個(gè)公司的上下級(jí)關(guān)系、甚至一個(gè)國家的治理模式,實(shí)際上都是一種樹形結(jié)構(gòu)。

所以,基于這些思想和工具來構(gòu)建復(fù)雜文檔的結(jié)構(gòu)也就可以理解了。

DOM API分類

DOM API就是DOM樹開放給開發(fā)者的接口,是對(duì)系統(tǒng)能力的延伸,大致會(huì)包含4個(gè)部分。

  • 節(jié)點(diǎn):DOM樹形結(jié)構(gòu)中的節(jié)點(diǎn)相關(guān)API;
  • 事件:觸發(fā)和監(jiān)聽事件相關(guān)API;
  • Range:操作文字范圍相關(guān)API;
  • 遍歷:遍歷DOM需要的API。

節(jié)點(diǎn)

主要的DOM樹節(jié)點(diǎn)的繼承關(guān)系如下:

DOM節(jié)點(diǎn)繼承關(guān)系

使用代碼驗(yàn)證:


    alert(document.body.constructor.name) // "HTMLBodyElement"
    alert(document.body) // [object HTMLBodyElement]

    document.body instanceof HTMLBodyElement // true
    document.body instanceof HTMLElement // true
    document.body instanceof Element // true
    document.body instanceof Node // true
    document.body instanceof EventTarget //true
    
    HTMLInputElement.prototype.__proto__ === HTMLElement.prototype //true
    HTMLAnchorElement.prototype.__proto__ === HTMLElement.prototype // true

完整的繼承關(guān)系如下所示:

DOM節(jié)點(diǎn)繼承關(guān)系

Node

Node是DOM樹繼承關(guān)系的根節(jié)點(diǎn),相當(dāng)于一個(gè)“抽象類”,提供了操作DOM的基礎(chǔ)能力,主要有下面這些API:

  • 關(guān)系節(jié)點(diǎn)
    • parentNode,父節(jié)點(diǎn);
    • childNodes,孩子節(jié)點(diǎn);
    • firstChild,第一個(gè)孩子節(jié)點(diǎn);
    • lastChild,最后一個(gè)孩子節(jié)點(diǎn);
    • nextSibling,下一個(gè)兄弟節(jié)點(diǎn);
    • previousSibling,上一個(gè)兄弟節(jié)點(diǎn)。
  • 操作節(jié)點(diǎn)
    • appendChild,在后面添加孩子節(jié)點(diǎn);
    • insertBefore,在某個(gè)節(jié)點(diǎn)之前添加;
    • removeChild,刪除某個(gè)節(jié)點(diǎn);
    • replaceChild,替換某個(gè)節(jié)點(diǎn)
    • cloneNode,復(fù)制一個(gè)節(jié)點(diǎn),如果傳入?yún)?shù)true,則會(huì)連同子元素做深拷貝。
  • 比較關(guān)系
    • compareDocumentPosition,是一個(gè)用于比較兩個(gè)節(jié)點(diǎn)中關(guān)系;
    • contains,檢查一個(gè)節(jié)點(diǎn)是否包含另一個(gè)節(jié)點(diǎn);
    • isEqualNode,檢查兩個(gè)節(jié)點(diǎn)是否完全相同;
    • isSameNode,檢查兩個(gè)節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn),實(shí)際上在JavaScript中可以用“===”;
  • 新建節(jié)點(diǎn)(DOM標(biāo)準(zhǔn)規(guī)定文檔節(jié)點(diǎn)必須由create方法創(chuàng)建,而不能用JavaScript的new創(chuàng)建)
    • createElement
    • createTextNode
    • createCDATASection
    • createComment
    • createProcessingInstruction
    • createDocumentFragment
    • createDocumentType

Element

Node提供了在樹形結(jié)構(gòu)中操作節(jié)點(diǎn)的能力,但是很多時(shí)候我們需要的Element元素,是Node的子類,對(duì)應(yīng)了HTML中的標(biāo)簽,即有子節(jié)點(diǎn),又有屬性。

屬性

innerHTML

表示元素后代的HTML序列,設(shè)置新元素將會(huì)先刪除老元素,再創(chuàng)建新元素。

如果設(shè)置一個(gè)非法HTML值,瀏覽器會(huì)進(jìn)行修正。

    document.body.innerHTML = '<b>test';
    alert( document.body.innerHTML ); 
    // <b>test</b>

outerHTML

該節(jié)點(diǎn)的所有HTML內(nèi)容,是它自己和innerHTML的合集。

注意,修改outerHTML并不會(huì)改變?cè)撛氐闹?,因?yàn)檫@樣操作不符合“面向?qū)ο蟆钡奶卣鳎焊冈負(fù)碛凶釉?,只有父元素才能“操作”子元素。顯然,要修改outerHTML的值,只有outerHTML的父元素通過innerHTML來修改。

data

innerHTML適用于所有Element節(jié)點(diǎn)。但是有些節(jié)點(diǎn)不是Element,比如Text,Comment,這時(shí)候可以使用data屬性獲取,或者用nodeValue獲取。

    <body>
    Hello
    <!-- Comment -->
    <script>
        let text = document.body.firstChild;
        alert(text.data); // Hello

        let comment = text.nextSibling;
        alert(comment.data); // Comment
    </script>
    </body>

關(guān)系屬性

類似Node中的屬性,只是變成了Element子類。

  • parentElement,父Element節(jié)點(diǎn);
  • children,所有Element子節(jié)點(diǎn);
  • firstElementChild,第一個(gè)Element子節(jié)點(diǎn);
  • lastElementChild,最后一個(gè)Element子節(jié)點(diǎn);
  • nextElementSibling,下一個(gè)兄弟Element節(jié)點(diǎn);
  • previousElementSibling,上一個(gè)兄弟Element節(jié)點(diǎn)。

Others

  • nodeName: 對(duì)應(yīng)Node,所有繼承自Node的子類都有;
  • tagName: 對(duì)應(yīng)Element,只有繼承自Element的才有;
  • textContent: 獲取所有后代元素的Text值;
  • hidden: 是否顯示,和style="display:none"左右相同;
  • value: 對(duì)應(yīng)<input>, <select> and <textarea>的值;
  • href: 對(duì)應(yīng)鏈接的href值;
  • ……

方法

操作屬性

  • getAttribute
  • setAttribute
  • removeAttribute
  • hasAttribute
  • getAttributeNode
  • setAttributeNode

此外,還可以像訪問property一樣,訪問attribute,比如document.body.attributes.class = “a” 等效于 document.body.setAttribute(“class”, “a”)。

查找元素

  • querySelector
  • querySelectorAll
  • getElementById
  • getElementsByName
  • getElementsByTagName
  • getElementsByClassName

需要注意,getElementById、getElementsByName、getElementsByTagName、getElementsByClassName,這幾個(gè)API的性能高于querySelector。

另外,由getElementsByName、getElementsByTagNamegetElementsByClassName獲取的并非一個(gè)靜態(tài)數(shù)組,而是一個(gè)可動(dòng)態(tài)更新的集合。

遍歷

DOM API中還提供了NodeIterator 和 TreeWalker 來遍歷樹。

NodeIterator

document.createNodeIterator(root, whatToShow); 實(shí)際上,由于性能等問題,工作中很少用到。

參數(shù)

  • root: 文檔根節(jié)點(diǎn)
  • whatToShow: 可被訪問節(jié)點(diǎn)
    • NodeFilter.SHOW_ALL:所有節(jié)點(diǎn)
    • NodeFilter.SHOW_ELEMENT:元素節(jié)點(diǎn)
    • NodeFilter.SHOW_TEXT:文本節(jié)點(diǎn)
    • NodeFilter.SHOW_COMMENT:注釋節(jié)點(diǎn)
    • NodeFilter.SHOW_DOCUMENT:文檔節(jié)點(diǎn)
  • filter: 過濾器,根據(jù)需求返回下面這些:
    • NodeFilter.FILTER_ACCEPT
    • NodeFilter.FILTER_REJECT: 跳過當(dāng)前節(jié)點(diǎn)和所有后代節(jié)點(diǎn)
    • NodeFilter.FILTER_SKIP: 僅僅跳過當(dāng)前節(jié)點(diǎn)

比如,如果要訪問所有鏈接,可以用下面的filter

    var filter = function(node) {
        return node.tagName === 'ARTICLE' ? NodeFilter.FILTER_REJECT : 
                node.tagName === 'A' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
    }

基本用法:

    var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ALL, filter, false);
    var node;
    while(node = iterator.nextNode())
    {
        console.log(node);
    }

TreeWalker

document.createTreeWalker(root, whatToShow);

參數(shù)、用法與document.createNodeIterator類似。但是,TreeWalker多了在DOM樹上自由移動(dòng)當(dāng)前節(jié)點(diǎn)的能力,一般來說,這種API用于“跳過”某些節(jié)點(diǎn),或者重復(fù)遍歷某些節(jié)點(diǎn)。

    var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false)
    var node;
    while(node = walker.nextNode()) {
        if(node.tagName === "p")
            node.nextSibling();
        console.log(node);
    }

遍歷DOM樹

深度優(yōu)先遍歷


    function deepWalk(el, action) {
        if (el) {
            action(el.nodeName)
            deepWalk(el.firstElementChild, action)
            deepWalk(el.nextElementSibling, action)
        }
    }

    deepWalk(document.documentElement, console.log)

廣度優(yōu)先遍歷


    var nodes = [document.documentElement]

    function breadthWalk(action) {
        if (nodes.length > 0) {
            let el = nodes.shift()
            action(el.nodeName)
            
            nodes = [...nodes, ...el.children]

            breadthWalk(action)
        }
    }

    breadthWalk(console.log)

Range

Range API主要用在富文本編輯領(lǐng)域,平時(shí)使用較少。它表示一個(gè)范圍,以文字為最小單位,不過也可以通過offset獲取部分文字。Range
API的性能更好,但是用起來比較麻煩。

比如,對(duì)于這段HTML:

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

它的DOM結(jié)構(gòu)如下:

  • P
    • text: Example
    • I
      • text: italic
    • text: and
    • B
      • text: bold

如果要選擇而前兩個(gè)字,也就是 Example: <i>italic</i> , 用如下代碼就可以選取。


    let p = document.getElementById("p")
    let range = new Range()

    range.setStart(p, 0)
    range.setEnd(p, 2)

    // 顯示選取,和按鼠標(biāo)右鍵選擇效果一樣
    document.getSelection().addRange(range);

如果要選擇 ample: <i>italic</i> and <b>bol , 就需要offset。


    let p = document.getElementById("p")
    let range = new Range()

    range.setStart(p.firstChild, 2);
    range.setEnd(p.querySelector('b').firstChild, 3);

    document.getSelection().addRange(range)

更多詳細(xì)指南請(qǐng)看javascript.info。

事件

概述

一般來說,事件來自輸入設(shè)備,我們平時(shí)能夠接觸到的輸入設(shè)備有三類:

  • 鍵盤
  • 鼠標(biāo)
  • 觸摸屏

觸摸屏和鼠標(biāo)又有一定的共性,它們被稱作pointer設(shè)備,所謂pointer設(shè)備,是指它的輸入最終會(huì)被抽象成屏幕上面的一個(gè)點(diǎn)。但是觸摸屏和鼠標(biāo)又有一定區(qū)別,它們的精度、反應(yīng)時(shí)間和支持的點(diǎn)的數(shù)量都不一樣。

所有現(xiàn)代UI系統(tǒng),都來自WIMP(Window Icon Menu Pointer)系統(tǒng),以Window、Icon、Menu、Pointer這四種基本互動(dòng)元素為基礎(chǔ),構(gòu)建出了一個(gè)具有完備性的人機(jī)交互系統(tǒng)。WIMP最初由施樂公司發(fā)明,后來被蘋果和微軟用在了自家產(chǎn)品上,最終演變?yōu)楫?dāng)今UI系統(tǒng)的樣子。

關(guān)于WIMP,喬布斯和蓋茨之間還有一段小故事:

WIMP是由Alan Kay主導(dǎo)設(shè)計(jì)的,這位巨匠,同時(shí)也是面向?qū)ο笾负蚐malltalk語言之父。

喬布斯曾經(jīng)受邀參觀施樂,他見到當(dāng)時(shí)的WIMP界面,認(rèn)為非常驚艷,不久后就領(lǐng)導(dǎo)蘋果研究了新一代麥金塔系統(tǒng)。

后來,在某次當(dāng)面對(duì)話中,喬布斯指責(zé)比爾蓋茨抄襲了WIMP的設(shè)計(jì),蓋茨淡定地回答:“史蒂夫,我覺得應(yīng)該用另一種方式看待這個(gè)問題。這就像我們有個(gè)叫施樂的有錢鄰居,當(dāng)我闖進(jìn)去想偷走電視時(shí),卻發(fā)現(xiàn)你已經(jīng)這么干了。”

捕獲和冒泡

在HTML DOM API中,有兩種事件傳播機(jī)制:捕獲Capture和冒泡Bubble。很多人都知道捕獲是從外向內(nèi)傳播,而冒泡是從內(nèi)向外傳播。

但是,為什么是這樣的?

實(shí)際上點(diǎn)擊事件來自觸摸屏或者鼠標(biāo),鼠標(biāo)點(diǎn)擊并沒有位置信息,但是一般操作系統(tǒng)會(huì)根據(jù)位移的累積計(jì)算出來,跟觸摸屏一樣,提供一個(gè)坐標(biāo)給瀏覽器。

那么,把這個(gè)坐標(biāo)轉(zhuǎn)換為具體的元素上事件的過程,就是捕獲過程了。而冒泡過程,則是符合人類理解邏輯的:當(dāng)你按電視機(jī)開關(guān)時(shí),你也按到了電視機(jī)。

所以,計(jì)算機(jī)為了響應(yīng)事件,必須能夠獲取到精確的位置信息,這個(gè)過程必然是從外向內(nèi)、從大到小去做命中測(cè)試hitTest,這就是捕獲。而冒泡更加符合我們?nèi)祟惖男袨槟J?,所有事件或行為一定是先在小范圍?nèi)被感知,然后才在更大范圍內(nèi)被感知。

捕獲Capture

當(dāng)多個(gè)元素形成嵌套關(guān)系,其中的元素接收到輸入事件時(shí),此事件首先會(huì)被最外層的元素獲取,然后不斷向內(nèi)傳播。如圖所示(來自quirksmode.org):

               | |
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------

根據(jù)DOM Event標(biāo)準(zhǔn),整個(gè)傳播過程有三個(gè)階段:

  1. Capturing phase: the event goes down to the element.
  2. Target phase: the event reached the target element.
  3. Bubbling phase: the event bubbles up from the element.
事件傳播過程

實(shí)際上,捕獲模式用的比較少,除過一些特殊場(chǎng)景,一般都不會(huì)用到。在事件注冊(cè)APIaddEventListener(type, listener, useCapture)中,第三個(gè)參數(shù)useCapture默認(rèn)為false。

冒泡Bubble

與捕獲相反,當(dāng)接收到輸入事件時(shí),首先會(huì)被最里面的元素獲取,然后不斷向外傳播。如下圖所示:

               / \
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
-----------------------------------

比如這段代碼:

    <form onclick="alert('form')">FORM
        <div onclick="alert('div')">DIV
            <p onclick="alert('p')">P</p>
        </div>
    </form>

執(zhí)行結(jié)果

  • 點(diǎn)擊最里層的 p,那么會(huì)連續(xù)顯示三個(gè)彈窗,分別為p、div和form;
  • 點(diǎn)擊中間的 div,那么會(huì)連續(xù)顯示兩個(gè)彈窗,分別為div和form;
  • 點(diǎn)擊最外層的 form,只出現(xiàn)一個(gè)彈窗,顯示form。

如果要阻止事件向上傳播,可用event.stopPropagation()或者event.stopImmediatePropagation().

    <body onclick="alert(`the bubbling doesn't reach here`)">
        <button onclick="event.stopPropagation()">Click me</button>
    </body>

在IE9之前,只支持冒泡,但是IE9之后和其他瀏覽器對(duì)兩種模式都支持。

event.target

event.target表示觸發(fā)事件的所有元素中,嵌套層次最深的那一個(gè)元素。event.currentTarget則是運(yùn)行事件回調(diào)函數(shù)的元素, event.currentTarget = this 。

焦點(diǎn)

焦點(diǎn)系統(tǒng)控制鍵盤事件。當(dāng)不論操作系統(tǒng)還是瀏覽器引入多窗口UI系統(tǒng)時(shí),必須有一種機(jī)制用來區(qū)分到底是哪個(gè)窗口正在接收輸入設(shè)備的輸入input事件,這一機(jī)制就是焦點(diǎn)系統(tǒng)的核心職責(zé)。當(dāng)一個(gè)窗口獲取到焦點(diǎn)時(shí),表示它正在接收輸入設(shè)備(比如鍵盤、鼠標(biāo))的輸入事件,這時(shí)其他窗口會(huì)自動(dòng)釋放焦點(diǎn)。否則,所有的窗口就會(huì)失控。

在桌面瀏覽器中,可以用Tab鍵切換到下一個(gè)可聚焦的元素,用shift + Tab可以切到上一個(gè)。瀏覽器也提供了API來操作焦點(diǎn):

    // 獲取
    document.body.focus();
    // 釋放
    document.body.blur();

總結(jié)

瀏覽器DOM API為我們提供了操作DOM的能力,這大大提高了瀏覽器的功能性,為開發(fā)人員編寫可交互應(yīng)用提供了更多可能性。具體而言,可分為4類:

  • 節(jié)點(diǎn)。節(jié)點(diǎn)API為操作DOM樹提供了基本的支持,比如增刪改查節(jié)點(diǎn),獲取各種關(guān)系節(jié)點(diǎn),以及節(jié)點(diǎn)屬性等等;
  • 遍歷。作為樹形結(jié)構(gòu)系統(tǒng),遍歷操作是不可避免的,不過這些遍歷API用的并不多,主要是性能和易用性欠佳;
  • Range。Range為富文本領(lǐng)域的應(yīng)用提供了很多便利,不斷功能強(qiáng)大,而且性能出眾;
  • 事件。輸入輸出設(shè)備的引入,擴(kuò)展了計(jì)算機(jī)系統(tǒng)的能力,而事件就是對(duì)這些能力的抽象。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容