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

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

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

這里注意腾供,我們需要使用bindthis的指向綁定到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對象艳狐,它可以解析HTMLXML來創(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: 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樹的變動

// 當監(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)聽callbackmutations類型進行相應地處理:

具體的配置及其含義可以參考文檔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)各種效果封救。

參考文檔:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捣作,隨后出現(xiàn)的幾起案子誉结,更是在濱河造成了極大的恐慌,老刑警劉巖券躁,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惩坑,死亡現(xiàn)場離奇詭異,居然都是意外死亡也拜,警方通過查閱死者的電腦和手機以舒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來慢哈,“玉大人蔓钟,你說我怎么就攤上這事÷鸭” “怎么了滥沫?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長键俱。 經(jīng)常有香客問我佣谐,道長,這世上最難降的妖魔是什么方妖? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任狭魂,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雌澄。我一直安慰自己斋泄,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布镐牺。 她就那樣靜靜地躺著炫掐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪睬涧。 梳的紋絲不亂的頭發(fā)上募胃,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音畦浓,去河邊找鬼痹束。 笑死,一個胖子當著我的面吹牛讶请,可吹牛的內(nèi)容都是我干的祷嘶。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼夺溢,長吁一口氣:“原來是場噩夢啊……” “哼论巍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起风响,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤嘉汰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后状勤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鞋怀,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年荧降,在試婚紗的時候發(fā)現(xiàn)自己被綠了接箫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡朵诫,死狀恐怖辛友,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剪返,我是刑警寧澤废累,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站脱盲,受9級特大地震影響邑滨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜钱反,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一掖看、第九天 我趴在偏房一處隱蔽的房頂上張望匣距。 院中可真熱鬧,春花似錦哎壳、人聲如沸毅待。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尸红。三九已至,卻和暖如春刹泄,著一層夾襖步出監(jiān)牢的瞬間外里,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工特石, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盅蝗,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓县匠,卻偏偏與公主長得像风科,于是被迫代替她去往敵國和親撒轮。 傳聞我的和親對象是個殘疾皇子乞旦,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

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