DOM基本功,你掌握了多少
一起養成寫作習慣!這是我參與「掘金日新計劃 · 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 裏,節點間關係可以劃分為以下兩類: - 父子節點:表示節點間的嵌套關係 - 兄弟節點:表示節點層級的平行關係,兄弟節點共享一個父節點
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. 事件冒泡階段
為什麼會有捕獲過程和冒泡過程
我們現代的 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三者位於同一層級,他們怎麼相互感知對方身上發生了什麼事情呢?
首先要創建一個本來不存在的"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) ```
- Cursor 全自動代碼生成器?你還沒用過?接下來我就來介紹智能 AI 代碼生成工具 Cursor 安裝和使用
- 小程序 語雀API一鍵搭建個人博客
- day02-複習JavaScript編寫注意事項 | 青訓營筆記
- ChatGPT 生態,毀滅人性的一次開源!
- GPT-4 VS 文心一言,百度的未來在哪裏?
- 實測GPT4!不到1小時寫完了一個小程序界面!推理能力提升能直接破譯密文?
- eggjs實現微信發送訂閲功能
- 作為前端開發,我推薦你一定要學習桌面軟件開發
- fastadmin uniapp 實現小程序接入chatGPT3.0
- 用ChatGPT和AI繪畫做一個微信小程序
- Android OpenCV (一) 基礎API 清晰度亮度識別
- 十分鐘學會開發自己的Python AI應用【OpenAI API篇】
- 基於 uni-app 搭建多端框架
- 國產工具好強大-一個可以允許小程序運行在任意APP的容器技術
- ChatGPT的一些好玩用途,有些你絕對想不到!
- uni-app vue3 vant開發微信小程序探路...
- 開發過程中使用,可以早點下班的coding小技巧
- 技術男的春天:小姐姐求助&暖男分析
- 面試官:為什麼Promise中的錯誤不能被try/catch?
- DOM基本功,你掌握了多少