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) ```