在以前鞠评,操作DOM是一件非常麻煩的事情谷炸,雖然現(xiàn)在已經(jīng)有類似React疏咐、Vue、Angular等框架幫助我們更容易地構(gòu)建界面阐肤。但是我們?nèi)匀挥斜匾獙W習原生DOM的操作方式來擴展我們的知識面凫佛,并且可以來應對一些不使用框架的場景,經(jīng)過長時間的發(fā)展孕惜,現(xiàn)在的DOM API
也變得更加優(yōu)雅簡潔了愧薛。
元素選擇
單個元素
// 返回一個 HTMLElement
document.querySelector(selectors)
它提供類似jQuery的$()
選擇器方法,非常方便田柔,我們可以這樣使用它:
document.querySelector('.class-name') // 根據(jù) class 選擇
document.querySelector('#id') // 根據(jù) id 選擇
document.querySelector('div') // 根據(jù) 標簽 選擇
document.querySelector('[data-test="input"]') // 根據(jù)屬性來選擇
document.querySelector('div + p > span') // 多重選擇器
多個元素
// 返回一個 NodeList
document.querySelectorAll('li') // 選擇所有標簽為 <li> 的元素
如果要使用Array
的數(shù)組方法净捅,需要先轉(zhuǎn)成普通數(shù)組缠导,可以這樣做:
// 使用擴展運算符
const arr = [...document.querySelectorAll('li')]
// 使用 Array.from 方法
const arr = Array.from(document.querySelectorAll('li'))
但是它和getElementsByTagName
或getElementsByClassName
是有區(qū)別的,getElementsByTagName
或getElementsByClassName
返回的是一個HTMLCollection
瞄勾,它是動態(tài)的,比如當我們移除掉document
中被選取的某個li
標簽弥激,所返回的HTMLCollection
中相應的li
標簽也會被移除进陡,它具有實時性。
而querySelectorAll
是靜態(tài)的微服,移除document
文檔流中被選取的某個li
標簽趾疚,不會影響返回的NodeList
,它沒有實時性以蕴。
HTMLCollection 和 NodeList 的異同
-
HTMLCollection
是元素的集合(只包含元素) -
NodeList
是文檔節(jié)點的集合(包含元素也包含其它節(jié)點) -
HTMLCollection
是動態(tài)集合糙麦,節(jié)點變化會反映到返回的集合中 -
NodeList
是靜態(tài)集合,節(jié)點的變化不會影響返回的集合 -
HTMLCollection
實例對象可以通過id
或name
屬性引用節(jié)點元素 -
NodeList
只能使用數(shù)字索引引用
選擇范圍
我們可以限制選擇的范圍丛肮,而不至于每次都在document
上進行選擇喳资,可以這樣做:
// 只獲取 #container 下的所有 li 標簽
const container = document.querySelector('#container')
container.querySelectorAll('li')
進一步封裝
我們可以封裝成類似jQuery
的寫法,用$
進行選擇:
const $ = document.querySelector.bind(document)
$('#container')
const $$ = document.querySelectorAll.bind(document)
$$('li')
這里注意腾供,我們需要使用bind
把this
的指向綁定到document
上仆邓,否則直接把函數(shù)賦值給變量獲取到的是一個普通函數(shù),會導致this
指向window
向上選擇DOM
我們還可以獲取某個Element
的最近父元素伴鳖,通過使用closest
方法
// 獲取距離 li 標簽最近的上級 div 標簽
document.querySelector('li').closest('div')
// 再更上一層节值,獲取最近的上級名為 content 的元素
document.querySelector('li').closest('div').('.content')
添加元素
這里假設我們要添加這樣一個元素
<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
來實現(xiàn)類似jQuery
的方法
document.body.insertAdjacentHTML('beforeend', '<a href="/home" class="active">Home</a>')
這里需要傳入兩個參數(shù),第一個參數(shù)是插入的位置须肆,第二參數(shù)是插入的HTML
片段匿乃,位置可選參數(shù)如下:
-
beforebegin
插入某個元素之前 -
afterbegin
插入到第一個子元素之前 -
beforeend
插入到最后一個子元素之后 -
afterend
插入到元素之后
<!-- beforebegin -->
<div>
<!-- afterbegin -->
content
<!-- beforeend -->
</div>
<!-- afterend -->
通過這個API桩皿,可以更方便地指定插入位置。假如要把a
標簽插入到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>')
還有兩個相似的方法泄隔,但第二個元素傳入的不是HTML
字符串,而是傳一個元素或文本
const link = document.createElement('a')
const div = document.querySelector('div')
// 插入元素
div.insertAdjacentElement('beforebegin', link)
插入文本
// 插入文本
div.insertAdjacentText('afterbegin', 'content')
移動元素
上面介紹的insertAdjacentElement
也可以移動文檔流上的一個元素宛徊,假如有這樣的HTML
片段:
<div class="first">
<h1>Title</h1>
</div>
<div class="second">
<h2>Subtitle</h2>
</div>
我們需要把h2
標簽插入到h1
標簽下面
// 分別獲取兩個元素
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')
// 指定把 h2 插入到 h1 下面
h1.insertAdjacentElement('afterend', h2)
注意佛嬉,這是移動,而非拷貝闸天,此時的HTML
變成:
<div class="first">
<h1>Title</h1>
<h2>Subtitle</h2>
</div>
<div class="second">
<!-- h2 標簽被移動了 -->
</div>
元素替換
我們可以直接使用replaceWith
方法暖呕,通過這個方法,可以創(chuàng)建一個元素來進行替換苞氮,也可以選擇一個已有元素進行替換湾揽,后者會移動被選擇的元素,而非拷貝笼吟。
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>
移除一個元素
只需要調(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)建元素,而不是需要立即插入赞厕。
這時就需要借助DomParser
對象艳狐,它可以解析HTML
和XML
來創(chuàng)建一個DOM
元素,它提供了parseFromString
方法進行創(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
可以幫助我們判斷某個元素是否和選擇器相匹配毫目。
<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
方法判斷是否包含某個子元素:
<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)
方法可以幫助我們確定某個元素的確切位置,它會返回數(shù)字來指示位置诲侮,返回值的意思如下镀虐,如果滿足多個條件,會返回相加值:
- 1: 比較的元素不在同一個
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
樹的變動
// 當監(jiān)聽到元素的變動就會調(diào)動 callback 方法
const observer = new MutationObserver(callback)
然后我們需要使用observer
方法監(jiān)聽某個node
的變化,否則不會監(jiān)聽绽慈,它接收兩個參數(shù)恨旱,第一個參數(shù)是監(jiān)聽目標,第二個參數(shù)是監(jiān)聽選項坝疼。
const target = document.querySelector('#container')
const observer = new MutationObserver(callback)
observer.observe(target, options)
當發(fā)生變化時搜贤,就會調(diào)用callback
方法,此時钝凶,我們就可以在callback
中監(jiān)聽變化仪芒,并監(jiān)聽callback
的mutations
類型進行相應地處理:
具體的配置及其含義可以參考文檔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é)點
// 通過 mutation.removedNodes 獲取移除的節(jié)點
break
}
})
}
// step3: 傳入 callback 并實例化
const observer = new MutationObserver(callback)
// step4: 開始監(jiān)聽并根據(jù)需求設置監(jiān)聽選項
observer.observe(target, {
attributes: true, // 監(jiān)聽 attribute 變化
attributeFilter: ['foo'], // 只監(jiān)聽屬性包含 foo,需要先把 attribute 設置為 true
attributeOldValue: true, // 發(fā)生改變時掂名,記錄 attribute 之前的值
childList: true // 監(jiān)聽元素的添加和刪除
})
當完成監(jiān)聽時据沈,可以通過observer.disconnect()
方法來中止監(jiān)聽,并且可以在之前通過takeRecords()
來處理未傳遞的MutationRecord饺蔑。
const mutations = observer.takeRecords()
callback(mutations)
observer.disconnect()
小結(jié)
通過上述這些強大的API锌介,可以非常方便地對 DOM 進行操作,滿足各種不同的需求膀钠,此外掏湾,還有一些沒有介紹到的裹虫,比如IntersectionObserver
可以監(jiān)聽目標元素和文檔視窗的交叉狀態(tài)來實現(xiàn)圖片懶加載肿嘲。所以,在使用框架進行開發(fā)時筑公,我們也需要深入理解 DOM雳窟,這樣才可以對整個 DOM 結(jié)構(gòu)有更清晰的認識,更好地發(fā)揮它們的潛力匣屡,優(yōu)雅地實現(xiàn)各種效果封救。
參考文檔: