如何優(yōu)雅地操作DOM

在以前,操作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'))

但是它和getElementsByTagNamegetElementsByClassName是有區(qū)別的,getElementsByTagNamegetElementsByClassName返回的是一個(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í)例對象可以通過idname屬性引用節(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')

這里注意,我們需要使用bindthis的指向綁定到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對象,它可以解析HTMLXML來創(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: otherNodenode之前
  • 4: otherNodenode之后
  • 8: otherNode包裹node
  • 16: otherNodenode包裹
<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)聽callbackmutations類型進(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)各種效果。

參考文檔:

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

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

  • ??DOM(文檔對象模型)是針對 HTML 和 XML 文檔的一個(gè) API(應(yīng)用程序編程接口)。 ??DOM 描繪...
    霜天曉閱讀 3,877評論 0 7
  • 前言:盡管現(xiàn)在有很多優(yōu)秀的框架,大大簡化了我們的DOM操作,但是我們?nèi)匀灰獙W(xué)好DOM知識來寫原生JS,從根本上去理...
    長鯨向南閱讀 2,075評論 0 0
  • 基本概念 DOM DOM 是 JavaScript 操作網(wǎng)頁的接口,全稱為“文檔對象模型”(Document Ob...
    許先生__閱讀 936評論 0 1
  • 一、基本概念 1.1、DOM DOM是JS操作網(wǎng)頁的接口,全稱為“文檔對象模型”(Document Object ...
    周花花啊閱讀 3,460評論 0 6
  • 用多了 jQuery 也會有點(diǎn)忘記 原生JavaScript 是如何操作 DOM 的,在此總結(jié): 什么是DOM?如...
    藍(lán)醇閱讀 1,010評論 0 0

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