DOM基本功,你掌握了多少

語言: CN / TW / HK

一起養成寫作習慣!這是我參與「掘金日新計劃 · 4 月更文挑戰」的第14天,點選檢視活動詳情

理解DOM

文件物件模型 (DOM) 是HTML和XML文件的程式設計介面。它提供了對文件的結構化的表述,並定義了一種方式可以使從程式中對該結構進行訪問,從而改變文件的結構,樣式和內容。DOM 將文件解析為一個由節點和物件(包含屬性和方法的物件)組成的結構集合。簡言之,它會將web頁面和指令碼或程式語言連線起來。 ——MDN

DOM 的概念看似抽象,簡單來說就是確保開發者可以通過 JS 指令碼來操作 HTML

DOM 樹的解析

在 DOM 中,每個元素都是一個節點,節點型別細數起來可以有很多種,我們這裡強調以下 4 種:

Document

Document 就是指這份檔案,也就是這份 HTML 檔的開端。當瀏覽器載入 HTML 文件, 它就會成為 Document 物件

Element

Element 就是指 HTML 檔案內的各個標籤,像是<div>、<span>這樣的各種 HTML 標籤定義的元素都屬於 Element 型別。

Text

Text 就是指被各個標籤包起來的文字,舉個例子:<span>哈哈哈</span>,這裡的“哈哈哈”被 <span> 標籤包了起來,它就是這個 Element 的 Text。

Attribute

Attribute 型別表示元素的特性。從技術角度講,這裡的特性就是說各個標籤裡的屬性。

DOM 節點間關係

在樹狀結構的 DOM 裡,節點間關係可以劃分為以下兩類: - 父子節點:表示節點間的巢狀關係 - 兄弟節點:表示節點層級的平行關係,兄弟節點共享一個父節點

image.png

DOM節點的增刪改查

增:DOM 節點的建立

js // 首先獲取父節點 var container = document.getElementById('container') // 建立新節點 var targetSpan = document.createElement('span') // 設定 span 節點的內容 targetSpan.innerHTML = 'hello world' // 把新建立的元素塞進父節點裡去 container.appendChild(targetSpan)

刪:DOM 節點的刪除

js // 獲取目標元素的父元素 var container = document.getElementById('container') // 獲取目標元素 var targetNode = document.getElementById('title') // 刪除目標元素 container.removeChild(targetNode)

改:修改 DOM 元素

修改 DOM 元素這個動作可以分很多維度,比如說移動 DOM 元素的位置,修改 DOM 元素的屬性等。

現在需要調換 title 和 content 的位置,我們可以考慮 insertBefore 或者 appendChild。這裡給出 insertBefore 的操作示範:

js // 獲取父元素 var container = document.getElementById('container') // 獲取兩個需要被交換的元素 var title = document.getElementById('title') var content = document.getElementById('content') // 交換兩個元素,把 content 置於 title 前面 container.insertBefore(content, title) DOM 元素屬性的獲取和修改 js var title = document.getElementById('title') // 獲取 id 屬性 var titleId = title.getAttribute('id') // 修改 id 屬性 title.setAttribute('id', 'anothorTitle')

DOM 事件體系

事件流

W3C 標準約定了一個事件的傳播過程要經過以下三個階段: 1. 事件捕獲階段 2. 目標階段 3. 事件冒泡階段

image.png

為什麼會有捕獲過程和冒泡過程

我們現代的 UI 系統,都源自 WIMP 系統。WIMP 是如此成功,以至於今天很多的前端工程師會有一個觀點,認為我們能夠"點選一個按鈕",實際上並非如此,我們只能夠點選滑鼠上的按鈕或者觸控式螢幕,是作業系統和瀏覽器把這個資訊對應到了一個邏輯上的按鈕,再使得它的檢視對點選事件有反應。這就引出了:捕獲與冒泡。

實際上點選事件來自觸控式螢幕或者滑鼠,滑鼠點選並沒有位置資訊,但是一般作業系統會根據位移的累積計算出來,跟觸控式螢幕一樣,提供一個座標給瀏覽器

那麼,把這個座標轉換為具體的元素上事件的過程,就是捕獲過程了。而冒泡過程,則是符合人類理解邏輯的:當你按電視機開關時,你也按到了電視機。

所以我們可以認為,捕獲是計算機處理事件的邏輯,而冒泡是人類處理事件的邏輯。

上面講的都是pointer 事件,它是由座標控制,這裡我們也提一下鍵盤事件,也成為焦點

鍵盤事件是由焦點系統控制的,一般來說,作業系統也會提供一套焦點系統,但是現代瀏覽器一般都選擇在自己的系統內覆蓋原本的焦點系統。

焦點系統認為整個 UI 系統中,有且僅有一個"聚焦"的元素,所有的鍵盤事件的目標元素都是這個聚焦元素。

Tab 鍵被用來切換到下一個可聚焦的元素,焦點系統佔用了 Tab 鍵,但是可以用 JavaScript 來阻止這個行為。瀏覽器 API 還提供了 API 來操作焦點,如:

js document.body.focus(); document.body.blur();

事件物件

currentTarget

它記錄了事件當下正在被哪個元素接收,即正在經過哪個元素。這個元素是一直在改變的,因為事件的傳播畢竟是個層層穿梭的過程。

如果事件處理程式繫結的元素,與具體的觸發元素是一樣的,那麼函式中的 this、event.currentTarget、和 event.target 三個值是相同的。我們可以以此為依據,判斷當前的元素是否就是目標元素。

target

指觸發事件的具體目標,也就是最具體的那個元素,是事件的真正來源。

就算事件處理程式沒有繫結在目標元素上、而是繫結在了目標元素的父元素上,只要它是由內部的目標元素冒泡到父容器上觸發的,那麼我們仍然可以通過 target 來感知到目標元素才是事件真實的來源。

自定義事件

現在想實現這樣一種效果:在點選A之後,B 和 C 都能感知到 A 被點選了,並且做出相應的行為——就像這個點選事件是點在 B 和 C 上一樣。 ```js

我是A
我是B
我是C

``` 我們知道,藉助事件捕獲和冒泡的特性,我們是可以實現父子元素之間的行為聯動的。但是此處,A、B、C三者位於同一層級,他們怎麼相互感知對方身上發生了什麼事情呢?

首先要建立一個本來不存在的"clickA"事件,來表示 A 被點選了,可以這麼寫:

js var clickAEvent = new Event('clickA'); 然後完成事件的監聽和派發:

```js // 獲取 divB 元素 var divB = document.getElementById('divB') // divB 監聽 clickA 事件 divB.addEventListener('clickA',function(e){ console.log('我是小B,我感覺到了小A') console.log(e.target) })

// 獲取 divC 元素 var divC = document.getElementById('divC') // divC 監聽 clickA 事件 divC.addEventListener('clickA',function(e){ console.log('我是小C,我感覺到了小A') console.log(e.target) })

// A 元素的監聽函式也得改造下 divA.addEventListener('click',function(){ console.log('我是小A') // 注意這裡 dispatch 這個動作,就是我們自己派發事件了 divB.dispatchEvent(clickAEvent) divC.dispatchEvent(clickAEvent) })
```

事件代理

我希望做到點選每一個 li 元素,都能輸出它內在的文字內容。

```js

  • 鵝鵝鵝
  • 曲項向天歌
  • 白毛浮綠水
  • 紅掌撥清波
  • 鋤禾日當午

``` 一個比較直觀的思路是讓每一個 li 元素都去監聽一個點選動作:

js // 獲取 li 列表 var liList = document.getElementsByTagName('li') // 逐個安裝監聽函式 for (var i = 0; i < liList.length; i++) { liList[i].addEventListener('click', function (e) { console.log(e.target.innerHTML) }) } 這個時候我們可以使用事件代理:

js var ul = document.getElementById('poem') ul.addEventListener('click', function(e){ console.log(e.target.innerHTML) }) e.target 就是指觸發事件的具體目標,它記錄著事件的源頭。所以說,不管咱們的監聽函式在哪一層執行,只要我拿到這個 e.target,就相當於拿到了真正觸發事件的那個元素。拿到這個元素後,我們完全可以模擬出它的行為,實現無差別的監聽效果。

像這樣利用事件的冒泡特性,把多個子元素的同一型別的監聽邏輯,合併到父元素上通過一個監聽函式來管理的行為,就是事件代理。通過事件代理,我們可以減少記憶體開銷、簡化註冊步驟,大大提高開發效率。

事件的防抖與節流

事件節流-throttle:第一個說來算

簡單理解:節流就是在一段時間中只發生一次回撥,而且是第一次觸發的回撥,在這段時間後面觸發的都不執行。

比如在滾動事件中,我要實時地知道滾動的距離,但是我其實只要500ms知道一次滾動的距離,但是500ms我們觸發了很多次回撥,所以這就可以用節流。

現在一起實現一個 throttle:

```js // fn是我們需要包裝的事件回撥, interval是時間間隔的閾值 function throttle(fn, interval) { // last為上一次觸發回撥的時間 let last = 0

// 將throttle處理結果當作函式返回 return function () { // 保留呼叫時的this上下文 let context = this // 保留呼叫時傳入的引數 let args = arguments // 記錄本次觸發回撥的時間 let now = +new Date()

  // 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔的閾值
  if (now - last >= interval) {
  // 如果時間間隔大於我們設定的時間間隔閾值,則執行回撥
      last = now;
      fn.apply(context, args);
  }
}

} // 用throttle來包裝scroll的回撥 const better_scroll = throttle(() => console.log('觸發了滾動事件'), 1000) document.addEventListener('scroll', better_scroll) ```

事件防抖-Debounce: 最後一個人說了算

比如使用者在輸入框輸入搜尋的關鍵字,我們不能每輸入一個字就去調一次介面,所以需要在一個時間間隔中使用最後輸入的關鍵字去調一次介面即可,這就是防抖。

```js // fn是我們需要包裝的事件回撥, delay是每次推遲執行的等待時間 function debounce(fn, delay) { // 定時器 let timer = null

// 將debounce處理結果當作函式返回 return function () { // 保留呼叫時的this上下文 let context = this // 保留呼叫時傳入的引數 let args = arguments // 每次事件被觸發時,都去清除之前的舊定時器 if(timer) { clearTimeout(timer) } // 設立新定時器 timer = setTimeout(function () { fn.apply(context, args) }, delay) } } // 用debounce來包裝scroll的回撥 const better_scroll = debounce(() => console.log('觸發了滾動事件'), 1000) document.addEventListener('scroll', better_scroll) ```