被人忽視的 DOM API

在框架盛行的年代,還有多少人記得在沒有框架時我們?nèi)绾慰刂?dom 的行為呢?作者本人也一直忽視了這方面的學(xué)習(xí),直到面試問到這個問題,下決心好好認(rèn)識認(rèn)識這個 dom api。

node、document 和 element

在學(xué)習(xí) dom api 時對這三者還是挺混亂的。理一下他們之間的關(guān)系。

node

node 是一個接口,像 document 和 element 都是繼承這個接口的。這個接口提供了 dom 節(jié)點的獲取和操作方法。
node 有許多類型,下圖列出了一些 node 的類型碼。由圖可見 element 的類型碼為 1,文本節(jié)點類型碼為 3,注釋節(jié)點類型碼為 8,document 的類型碼為 9。

node type

這讓我想到了 vue.js 中出現(xiàn)多次的代碼:if (child.nodeType === 3) { ... } 其實就是判斷當(dāng)前節(jié)點是否為文本節(jié)點。

element

從上面的內(nèi)容可知,element 就是一個特殊的 node(nodeType == 1),其實 element 就是 HTML 各類標(biāo)簽,如 <p><div> 這類有特殊含義的,能夠攜帶一些特殊屬性的節(jié)點。所以說 element 可以用 node 的所有 api。

document

同理,document 也是一個特殊的 node,它與 element 的不同之處在于 document 通常是 DOM 節(jié)點,即包含有 head 和 body 元素的一個 node。

參考:Difference between Node object and Element object? - Stack Overflow

api 學(xué)習(xí)

首先我發(fā)現(xiàn)一點:所有 dom 操作的起點都是使用 document 去獲取各種類型的 node (集合)然后再去執(zhí)行各類 dom 操作的行為!
由于 document 操作的 api 真的很多,所以我選取我想了解的部分學(xué)習(xí)了。在這里我學(xué)習(xí) DOM API 的目的是:

學(xué)習(xí) document 對象操作 dom 的方式,擁有脫離框架(jquery、vue等)來操作 dom 的能力。

獲取節(jié)點

  • Document.documentElement 返回 document 的直屬后代元素。
  • Document.activeElement 返回當(dāng)前正在操作的元素
  • Document.body 返回當(dāng)前文檔的 <body> 元素。與此類似的還有 Document.head 和 Document.scripts 兩個屬性返回當(dāng)前文檔的 <head><script> 元素。
  • Document.getElementByClassName() 返回有給定樣式名的元素列表
  • Document.getElementByTagName() 返回有給定標(biāo)簽名的元素列表
  • document.getElementById() 返回一個對識別元素的對象引用
  • document.querySelector() 返回文檔中第一個匹配指定選擇器的元素
  • document.querySelectorAll() 返回一個匹配指定選擇器的元素節(jié)點列表
  • Node.childNodes 返回一個包含了該節(jié)點所有子節(jié)點的實時的 NodeList 是“實時的”意思是,如果該節(jié)點的子節(jié)點發(fā)生了變化,NodeList 對象就會自動更新。
  • Node.firstChild & Node.lastChild 返回該節(jié)點的第一個子節(jié)點或最后一個子節(jié)點,如果該節(jié)點沒有子節(jié)點則返回 null。
  • Node.previousSibling & Node.nextSibling 返回與該節(jié)點同級的上一個或下一個節(jié)點,如果沒有返回null。
  • Node.ownerDocument 返回這個元素屬于的 Document 對象 。
  • Node.parentNode 返回一個當(dāng)前結(jié)點 Node 的父節(jié)點 。
  • Node.parentElement 返回一個當(dāng)前節(jié)點的父節(jié)點 Element

操作節(jié)點

  • Document.createComment() 創(chuàng)建一個新的注釋節(jié)點并返回它
  • Document.createDocumentFragment() 創(chuàng)建一個新的文檔片段
  • Document.createElement() 用給定的標(biāo)簽名創(chuàng)建一個新的元素。
  • Document.createTextNode() 創(chuàng)建一個文字節(jié)點
  • Document.write() 向文檔中寫入內(nèi)容(與之有類似功能的是 Document.writeln() 不同之處在于后面多了個換行符。)
  • Element.innerHTML 設(shè)置或返回元素的內(nèi)容
  • Node.textContent 獲取或設(shè)置一個標(biāo)簽內(nèi)所有子結(jié)點及其后代的文本內(nèi)容。
  • Node.appendChild() 向元素添加新的子節(jié)點,作為最后一個子節(jié)點。
  • Node.cloneNode() 克隆元素(方法中傳參為deep,如果deep為true則深拷貝。)
  • Node.insertBefore() 在指定已有節(jié)點前插入新節(jié)點(沒有 insertAfter 方法??梢允褂?insertBefore 方法和 nextSibling 來模擬它。)
  • Node.normalize() 合并元素中相鄰文本節(jié)點
  • Node.removeChild() 從元素中移除子節(jié)點
  • Node.replaceChild() 替換元素中的子節(jié)點

其中 Document 的 createXXX 方法還有一些其他不常用的,如需使用請查閱 MDN。

其他常用屬性和方法

  • Element.classList 返回元素的 class 集合。
  • EventTaget.addEventListener() 注冊監(jiān)聽事件
  • Node.nodeType 返回該節(jié)點的類型碼
  • Node.nodeValue 返回或設(shè)置當(dāng)前節(jié)點的值。
  • Node.compareDocumentPosition() 比較當(dāng)前節(jié)點與任意文檔中的另一個節(jié)點的位置關(guān)系。
  • Node.contains() 傳入的節(jié)點是否為該節(jié)點的后代節(jié)點。
  • Node.hasChildNodes() 是否擁有子節(jié)點
  • Node.isEqualNode() 檢查兩個元素是否相等
  • Node.isSameNode() 檢查兩個元素是否為相同的節(jié)點

以上內(nèi)容均參考了 MDN 上的內(nèi)容:

寫個操作 DOM 的例子

接下來就使用這些 API 來進行一些 DOM操作。

獲取各個位置的節(jié)點。

這里寫了個小demo:

<div id="container">
    <div>
        <h1>get dom</h1>
        <ul id="list">
            <li><span>hello world 1</span></li>
            <li><span>hello world 2</span></li>
            <li><span>hello world 3</span></li>
            <li><span>hello world 4</span></li>
            <li><span>hello world 5</span></li>
        </ul>
    </div>
    <br/>
    <div>
        <button>commit</button>
    </div>
</div>

<script>
    var container = document.getElementById('container')
    console.log('列出所有node', container.childNodes)
    var h1 = document.getElementsByTagName('h1')[0]
    console.log('獲取h1后的元素', h1.nextSibling)
    var uldiv = container.firstChild
    while (uldiv && uldiv.nodeType != 1) {
        uldiv = uldiv.nextSibling
    }
    var ul = uldiv.lastChild
    while (ul && ul.nodeType == 3) {
        ul = ul.previousSibling
    }
    console.log('獲取ul中第一個元素內(nèi)容', ul.firstChild)
    var doc = h1.ownerDocument
    console.log('獲取當(dāng)前 Document 對象', doc)
    var li1 = ul.firstChild
    console.log('獲取li的父級節(jié)點', li1.parentElement)
    var button = document.getElementsByTagName("button")[0]
    button.onclick = log
    button.focus()
    button.click()
    console.log('獲取正在操作的元素', document.activeElement)

    function log(){
        console.log('button is clicked')
    }
</script>

最后返回結(jié)果如圖:

返回結(jié)果

由于在 chrome 中空格、換行算是文本節(jié)點。所以獲取最后元素的時候總是會獲取到那些文本節(jié)點上去。這個要注意的。所以我在代碼中使用 nodeType == 1 來區(qū)分是否為元素。
在上面例子中查找了各種關(guān)系的元素,解決日常元素獲取問題應(yīng)該不難了。

實踐創(chuàng)建node、插入node和刪除node。

<div>
    <div id="container">
        <h2 id="child">Hello Child</h2>
    </div>
    <br/>
    <div id="buttonGroup"></div>
</div>

<script>
    // 父節(jié)點向子節(jié)點插入元素
    function appendChild(){
        var container = document.getElementById("container")
        var text = document.createElement("h2")
        text.textContent = 'Hello New Child'
        container.appendChild(text)
    }
    // 子節(jié)點獲取父節(jié)點,在父節(jié)點后插入元素
    function appendParent(){
        var child = document.getElementById('child')
        var parent = child.parentElement
        var text = document.createElement("h1")
        text.textContent = 'Hello Parent'
        var root = parent.parentElement

        root.insertBefore(text, parent.nextSibling)
    }
    // 在當(dāng)前元素前插入元素
    function appendPre(){
        var child = document.getElementById('child')
        var text = document.createElement("h2")
        text.textContent = 'Hello Pre Child'
        child.parentElement.insertBefore(text, child)
    }
    // 在當(dāng)前元素后插入元素
    function appendNext(){
        var child = document.getElementById('child')
        var text = document.createElement("h2")
        text.textContent = 'Hello Next Child'
        child.parentElement.insertBefore(text, child.nextSibling)
    }
    // 移除父元素中最后一個子元素
    function removeEle(){
        var container = document.getElementById('container')
        if (container.lastChild) {
            container.removeChild(container.lastChild)
        }
    }
    // 替換父元素中的子元素
    function replaceEle(){
        var child = document.getElementById("child")
        var newNode = document.createElement('div')
        newNode.innerHTML = "<button>button</button>hello new node replaced"
        var parent = child.parentElement
        parent.replaceChild(newNode, child)
    }
    // 創(chuàng)建按鈕組
    var ButtonGroup = document.getElementById("buttonGroup")
    var EventList = [ 
        "appendChild", 
        "appendParent", 
        "appendPre", 
        "appendNext", 
        "removeEle", 
        "replaceEle" 
    ]

    var ButtonArr = []
    for (var key of EventList) {
        var btn = document.createElement('button')
        btn.textContent = key
        btn.onclick = eval(key)
        ButtonArr.push(btn)
    }
    for (var b of ButtonArr) {
        ButtonGroup.appendChild(b)
    }
</script>

以上代碼實現(xiàn)了在各個位置插入元素元素的刪除替換,點擊此處查看運行結(jié)果。

簡單實現(xiàn) v-for、v-text、v-html、v-on 和 v-model 這些功能。

好吧,作為一個 Vue.js 愛好者,繞不開的想到了 Vue.js 操作 DOM 的一些功能。這里就試著簡單實現(xiàn)下(不涉及 Virtual DOM,只是單純的 DOM 修改)。
如果對 Vue 命令不了解可以去官網(wǎng)看看這些指令的用法。

<html>
    <head>
        <meta charset="UTF-8">
        <title>Hello Ele</title>
    </head>
    <body>
        <div id="container">
            <h1>v-text</h1>
            <span>{{ message }}</span>
            <h1>v-html</h1>  
            <div v-html="messagespan"></div>          
            <h1>v-model</h1>
            <input v-model="message"/>
            <h1>v-on</h1>
            <input id="myInput" v-on:blur="blur" v-on:focus="focus"/>
            <h1>v-for</h1>
            <ul></ul>
        </div>
        
        <script>
            // v-text
            var message = "Hello World"
            var messagespan = "<span>Hello World</span>"
          
            var spans = document.getElementsByTagName("span")
            for (var span of spans) {
                if (span.textContent == "{{ message }}") {
                    span.textContent = message
                }
            }
            // v-html
            var container = document.getElementById("container")
            var divs = container.getElementsByTagName("div")
            for(var div of divs){
                if (div.getAttribute("v-html") == "messagespan") {
                    div.innerHTML = messagespan
                }
            }
            // v-model
            var inputs = container.getElementsByTagName("input")
            for (var input of inputs) {
                if (input.getAttribute("v-model") == "message") {
                    input.setAttribute("value", message)
                }
            }
            // v-on
            var myInput = document.getElementById("myInput")
            myInput.onfocus = eval(myInput.getAttribute("v-on:focus"))
            myInput.onblur = eval(myInput.getAttribute("v-on:blur"))

            function focus(){
                myInput.setAttribute("value", "focus")
            }

            function blur() {
                myInput.setAttribute("value", "blur")
            }
            // v-for
            var liContents = [
                "jack",
                "rose",
                "james",
                "wade",
                "jordan"
            ]

            var liElementList = []
            for(var content of liContents) {
                var li = document.createElement("li")
                li.innerHTML = `<label><input type="checkbox"/><span>${content}</span></label>`
                liElementList.push(li)
            }
            var ul = container.getElementsByTagName("ul")[0]
            for (var liEle of liElementList) {
                ul.appendChild(liEle)
            }
        </script>
    </body>
</html>

點擊此處看效果。最后結(jié)果如圖:

顯示結(jié)果

簡單實現(xiàn)了 Vue.js 指令的這些功能,其實在 Vue.js 源碼中也是用了這些 dom 操作的 api 來做的。
更多 Vue 源碼中的 DOM 操作可以看下我的 《Vue.js 源碼學(xué)習(xí)六 —— VNode虛擬DOM學(xué)習(xí)》這篇文章中。

最后

無論什么框架,其實都是萬變不離其宗。最終都是用最基礎(chǔ)的 API 來實現(xiàn)的各種功能。所以學(xué)好基礎(chǔ)知識是非常重要的~
PS:還是 MDN 靠譜,w3school 的資料雖然也挺多,但是感覺不是很靠譜……以后查資料盡量去 MDN 英文網(wǎng)站去查(中文網(wǎng)站翻譯有些問題)。

打個廣告

鏈家上海研發(fā)中心招聘前端、后端、測試。
機會不多,需要內(nèi)推機會的請將簡歷發(fā)送至 dingxiaojie001@ke.com。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ??DOM(文檔對象模型)是針對 HTML 和 XML 文檔的一個 API(應(yīng)用程序編程接口)。 ??DOM 描繪...
    霜天曉閱讀 3,866評論 0 7
  • 原生DOM接口挺多的,需要花點時間研究下,不過先把基礎(chǔ)整好,后面框架估計好學(xué)點。 1. DOM是啥 1.1 知識回...
    吳少在coding閱讀 1,879評論 0 7
  • ??DOM 1 級主要定義的是 HTML 和 XML 文檔的底層結(jié)構(gòu)。 ??DOM2 和 DOM3 級則在這個結(jié)構(gòu)...
    霜天曉閱讀 1,599評論 1 3
  • 當(dāng)下很流行一句話:“人丑就要多讀書”,就為了這句話,朵拉兒跟媽媽整整冷戰(zhàn)了一個月。朵拉兒正是進入了高考倒計時...
    胡岱閱讀 374評論 0 0
  • 同意啊你說話費
    暢華南閱讀 83評論 0 0

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