在以前,操作DOM是一件非常麻煩的事情,雖然現(xiàn)在已經(jīng)有類似React、Vue、Angular等框架幫助我們更容易地構(gòu)建界面。但是我們?nèi)匀挥斜匾獙W(xué)習(xí)原生DOM的操作方式來擴(kuò)展我們的知識面,并且可以來應(yīng)對一些不使用框架的場景,經(jīng)過長時(shí)間的發(fā)展,現(xiàn)在的DOM API也變得更加優(yōu)雅簡潔了。
元素選擇
單個(gè)元素
// 返回一個(gè) HTMLElement
document.querySelector(selectors)
它提供類似jQuery的$()選擇器方法,非常方便,我們可以這樣使用它:
document.querySelector('.class-name') // 根據(jù) class 選擇
document.querySelector('#id') // 根據(jù) id 選擇
document.querySelector('div') // 根據(jù) 標(biāo)簽 選擇
document.querySelector('[data-test="input"]') // 根據(jù)屬性來選擇
document.querySelector('div + p > span') // 多重選擇器
多個(gè)元素
// 返回一個(gè) NodeList
document.querySelectorAll('li') // 選擇所有標(biāo)簽為 <li> 的元素
如果要使用Array的數(shù)組方法,需要先轉(zhuǎn)成普通數(shù)組,可以這樣做:
// 使用擴(kuò)展運(yùn)算符
const arr = [...document.querySelectorAll('li')]
// 使用 Array.from 方法
const arr = Array.from(document.querySelectorAll('li'))
但是它和getElementsByTagName或getElementsByClassName是有區(qū)別的,getElementsByTagName或getElementsByClassName返回的是一個(gè)HTMLCollection,它是動態(tài)的,比如當(dāng)我們移除掉document中被選取的某個(gè)li標(biāo)簽,所返回的HTMLCollection中相應(yīng)的li標(biāo)簽也會被移除,它具有實(shí)時(shí)性。
而querySelectorAll是靜態(tài)的,移除document文檔流中被選取的某個(gè)li標(biāo)簽,不會影響返回的NodeList,它沒有實(shí)時(shí)性。
HTMLCollection 和 NodeList 的異同
-
HTMLCollection是元素的集合(只包含元素) -
NodeList是文檔節(jié)點(diǎn)的集合(包含元素也包含其它節(jié)點(diǎn)) -
HTMLCollection是動態(tài)集合,節(jié)點(diǎn)變化會反映到返回的集合中 -
NodeList是靜態(tài)集合,節(jié)點(diǎn)的變化不會影響返回的集合 -
HTMLCollection實(shí)例對象可以通過id或name屬性引用節(jié)點(diǎn)元素 -
NodeList只能使用數(shù)字索引引用
選擇范圍
我們可以限制選擇的范圍,而不至于每次都在document上進(jìn)行選擇,可以這樣做:
// 只獲取 #container 下的所有 li 標(biāo)簽
const container = document.querySelector('#container')
container.querySelectorAll('li')
進(jìn)一步封裝
我們可以封裝成類似jQuery的寫法,用$進(jìn)行選擇:
const $ = document.querySelector.bind(document)
$('#container')
const $$ = document.querySelectorAll.bind(document)
$$('li')
這里注意,我們需要使用bind把this的指向綁定到document上,否則直接把函數(shù)賦值給變量獲取到的是一個(gè)普通函數(shù),會導(dǎo)致this指向window
向上選擇DOM
我們還可以獲取某個(gè)Element的最近父元素,通過使用closest方法
// 獲取距離 li 標(biāo)簽最近的上級 div 標(biāo)簽
document.querySelector('li').closest('div')
// 再更上一層,獲取最近的上級名為 content 的元素
document.querySelector('li').closest('div').('.content')
添加元素
這里假設(shè)我們要添加這樣一個(gè)元素
<a href="/home" class="active">Home</a>
在過去,我們需要這樣來添加元素
const link = document.createElement('a')
a.setAttribute('href', '/home')
a.className = 'active'
a.textContent = 'Home'
document.body.appendChild(link)
在有了jQuery后,我們可以這樣來添加元素
// 一句就能搞定
$('body').append('<a href="/home" class="active">Home</a>')
現(xiàn)在,我們可以借助insertAdjacentHTML來實(shí)現(xiàn)類似jQuery的方法
document.body.insertAdjacentHTML('beforeend', '<a href="/home" class="active">Home</a>')
這里需要傳入兩個(gè)參數(shù),第一個(gè)參數(shù)是插入的位置,第二參數(shù)是插入的HTML片段,位置可選參數(shù)如下:
-
beforebegin插入某個(gè)元素之前 -
afterbegin插入到第一個(gè)子元素之前 -
beforeend插入到最后一個(gè)子元素之后 -
afterend插入到元素之后
<!-- beforebegin -->
<div>
<!-- afterbegin -->
content
<!-- beforeend -->
</div>
<!-- afterend -->
通過這個(gè)API,可以更方便地指定插入位置。假如要把a標(biāo)簽插入到div之前,我們以前需要這樣做:
const link = document.createElement('a')
const div = document.querySelector('div')
div.parentNode.insertBefore(link, div)
而現(xiàn)在直接指定位置就可以了
const div = document.querySelector('div')
div.insertAdjacentHTML('beforebegin', '<a></a>')
還有兩個(gè)相似的方法,但第二個(gè)元素傳入的不是HTML字符串,而是傳一個(gè)元素或文本
const link = document.createElement('a')
const div = document.querySelector('div')
// 插入元素
div.insertAdjacentElement('beforebegin', link)
插入文本
// 插入文本
div.insertAdjacentText('afterbegin', 'content')
移動元素
上面介紹的insertAdjacentElement也可以移動文檔流上的一個(gè)元素,假如有這樣的HTML片段:
<div class="first">
<h1>Title</h1>
</div>
<div class="second">
<h2>Subtitle</h2>
</div>
我們需要把h2標(biāo)簽插入到h1標(biāo)簽下面
// 分別獲取兩個(gè)元素
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')
// 指定把 h2 插入到 h1 下面
h1.insertAdjacentElement('afterend', h2)
注意,這是移動,而非拷貝,此時(shí)的HTML變成:
<div class="first">
<h1>Title</h1>
<h2>Subtitle</h2>
</div>
<div class="second">
<!-- h2 標(biāo)簽被移動了 -->
</div>
元素替換
我們可以直接使用replaceWith方法,通過這個(gè)方法,可以創(chuàng)建一個(gè)元素來進(jìn)行替換,也可以選擇一個(gè)已有元素進(jìn)行替換,后者會移動被選擇的元素,而非拷貝。
someElement.replaceWith(otherElement)
<!-- 替換前 -->
<div class="first">
<h1>Title</h1>
</div>
<div class="second">
<h2>Subtitle</h2>
</div>
// 選擇 h1 和 h2
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')
// 用 h2 替換掉 h1
h1.replaceWith(h2)
<!-- 替換后 -->
<div class="first">
<!-- h1 被 h2 替換 -->
<h2>Subtitle</h2>
</div>
<div class="second">
<!-- h2 被移動 -->
</div>
移除一個(gè)元素
只需要調(diào)用remove()方法就可以了
const container = document.querySelector('#container')
container.remove()
// 以前的移除方法
const container = document.querySelector('#container')
container.parentNode.removeChild(container)
使用原生HTML片段創(chuàng)建元素
從上面可以了解到insertAdjacentHTML方法可以幫助我們插入HTML字符串到指定的位置,假如我們要先創(chuàng)建元素,而不是需要立即插入。
這時(shí)就需要借助DomParser對象,它可以解析HTML和XML來創(chuàng)建一個(gè)DOM元素,它提供了parseFromString方法進(jìn)行創(chuàng)建并返回解析后的元素。
const createElement = domString => new DOMParser().parseFromString(domString, 'text/html').body.firstChild
const a = createElement('<a href="/home" class="active">Home</a>')
元素匹配
matches
matches可以幫助我們判斷某個(gè)元素是否和選擇器相匹配。
<p class="foo">Hello world</p>
const p = document.querySelector('p')
p.matches('p') // true
p.matches('.foo') // true
p.matches('.bar') // false, 不存在 class 名為 bar
contains
也可以使用contains方法判斷是否包含某個(gè)子元素:
<div class="container">
<h1 class="title">Foo</h1>
</div>
<h2 class="subtitle">Bar</h2>
const container = document.querySelector('.container')
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')
container.contains(h1) // true
container.contains(h2) // false
compareDocumentPosition
使用node.compareDocumentPosition(otherNode)方法可以幫助我們確定某個(gè)元素的確切位置,它會返回?cái)?shù)字來指示位置,返回值的意思如下,如果滿足多個(gè)條件,會返回相加值:
- 1: 比較的元素不在同一個(gè)
document上 - 2:
otherNode在node之前 - 4:
otherNode在node之后 - 8:
otherNode包裹node - 16:
otherNode被node包裹
<div class="container">
<h1 class="title">Foo</h1>
</div>
<h2 class="subtitle">Bar</h2>
const container = document.querySelector('.container')
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')
// 20: h1 被 container 所包裹,并且在 container 之后 16 + 4 = 20
container.compareDocumentPosition(h1)
// 10: container 包裹 h1,并且在 h1 之前 8 + 2 = 10
h1.compareDocumentPosition(container)
// 4: h2 在 h1 的后面
h1.compareDocumentPosition(h2)
// 2: h1 在 h2 的前面
h2.compareDocumentPosition(h1)
MutationObserver
我們還可以使用MutationObserver來監(jiān)聽DOM樹的變動
// 當(dāng)監(jiān)聽到元素的變動就會調(diào)動 callback 方法
const observer = new MutationObserver(callback)
然后我們需要使用observer方法監(jiān)聽某個(gè)node的變化,否則不會監(jiān)聽,它接收兩個(gè)參數(shù),第一個(gè)參數(shù)是監(jiān)聽目標(biāo),第二個(gè)參數(shù)是監(jiān)聽選項(xiàng)。
const target = document.querySelector('#container')
const observer = new MutationObserver(callback)
observer.observe(target, options)
當(dāng)發(fā)生變化時(shí),就會調(diào)用callback方法,此時(shí),我們就可以在callback中監(jiān)聽變化,并監(jiān)聽callback的mutations類型進(jìn)行相應(yīng)地處理:
具體的配置及其含義可以參考文檔MutationObserver
// step1: 獲取元素
const target = document.querySelector('#container')
// step2: 編寫回調(diào)函數(shù),處理邏輯
const callback = (mutations, observer) => {
mutations.forEach(mutation => {
switch (mutation.type) {
case 'attributes':
// 通過 mutation.attribute 獲取改變的 attribute
// 通過 mutation.oldValue 獲取舊值
break
case 'childList':
// 通過 mutation.addedNodes 獲取添加的節(jié)點(diǎn)
// 通過 mutation.removedNodes 獲取移除的節(jié)點(diǎn)
break
}
})
}
// step3: 傳入 callback 并實(shí)例化
const observer = new MutationObserver(callback)
// step4: 開始監(jiān)聽并根據(jù)需求設(shè)置監(jiān)聽選項(xiàng)
observer.observe(target, {
attributes: true, // 監(jiān)聽 attribute 變化
attributeFilter: ['foo'], // 只監(jiān)聽屬性包含 foo,需要先把 attribute 設(shè)置為 true
attributeOldValue: true, // 發(fā)生改變時(shí),記錄 attribute 之前的值
childList: true // 監(jiān)聽元素的添加和刪除
})
當(dāng)完成監(jiān)聽時(shí),可以通過observer.disconnect()方法來中止監(jiān)聽,并且可以在之前通過takeRecords()來處理未傳遞的MutationRecord。
const mutations = observer.takeRecords()
callback(mutations)
observer.disconnect()
小結(jié)
通過上述這些強(qiáng)大的API,可以非常方便地對 DOM 進(jìn)行操作,滿足各種不同的需求,此外,還有一些沒有介紹到的,比如IntersectionObserver可以監(jiān)聽目標(biāo)元素和文檔視窗的交叉狀態(tài)來實(shí)現(xiàn)圖片懶加載。所以,在使用框架進(jìn)行開發(fā)時(shí),我們也需要深入理解 DOM,這樣才可以對整個(gè) DOM 結(jié)構(gòu)有更清晰的認(rèn)識,更好地發(fā)揮它們的潛力,優(yōu)雅地實(shí)現(xiàn)各種效果。
參考文檔: