JavaScript Note V
DOM(Document Object Model)操作與事件處理
5-1 什麼是DOM
文件物件模型DOM ( Document Object Model ),是瀏覽器把 HTML 文件(Document)內的各個標籤定義成物件(Object Model),變成一叫做 DOM 的結構。
這些物件模型最終會形成一個樹狀結構(DOM Trees)。Document是根節點,代表網頁的本身,每一個部分叫做「節點 (node)」。
JavaScript 可以修改 「物件模型」 也就是修改結構中任意節點。
開放給外部程式來操作的介面叫做 API (application programming interface, 應用程式介面),因此可以說 DOM 是瀏覽器開放給 JavaScript 的 API。
瀏覽器也是物件模型
在 document
物件模型的上層還有 window
,也就是「瀏覽器物件模型 BOM(Browser Object Model)」的根節點。
先載入 HTML,再載入 JavaScript
<script src="app.js"></script>
的位置放在 </body>
之前是因為要先把 HTML 解析完且DOM tree 做好以後,再去讀取 JavaScript 來修改 DOM。
5–2 DOM節點選取
querySelector & querySelectorAll (用 CSS 選擇器來找元素 ,也可用標籤找尋 )
document.querySelector('.card-body ul')
如果要選出很多元素,可以用 querySelectorAll:
document.querySelectorAll('li')
會回傳一個類似陣列的 NodeList,和 JavaScript 裡的陣列不一樣,不能新增刪除元素,只有幾個簡單的唯讀操作,查看長度 length
、遍歷內容 forEach
、使用 index 來存取特定項目。
getElementBy
早期的語法大多是 getElementBy 系列,語法是用字串來搜尋,不是 CSS 選擇器;如果是複數的話有加 s:
document.getElementById('my-recipe')
// 類似 document.querySelector('#my-recipe')document.getElementsByClassName('card-body')
// 類似 document.querySelectorAll('.card-body')
Practice
1. 圖片的網址: document.querySelector('.my-card img')
2. 選出節點的內容:
document.querySelector('.my-name').innerHTML
或 document.querySelector('.my-name').innerText
或 document.querySelector('.my-name').textContent
。
innerText和 textContent的功能一樣,只能處理文字,而 innerHTML可以存取到 HTML 結構。
3. 選出 HTML 屬性的值:
document.querySelector('.my-card img').src
4. 選出重覆結構裡的某項目
document.querySelectorAll('.static-board tr')[1]
或('.static-board tbody tr')[0]
5–3 修改節點
Step 1 新增節點
let h1 = document.createElement('h1')
Step 2 抽換節點內容
NODE.innerHTML = "htmlContent"
- 會解析 HTML 標籤NODE.innerText = "textContent"
- 不會解析 HTML 標籤(只處理文字)
h1.innerHTML = "This sentence is created by JavaScript"
Step 3 將節點插入 DOM Tree
透過 appendChild
、insertBefore
或 replaceChild
等方法將新元素加入至指定的位置。
container.appendChild(h1)
補充:Modern style
現代的 JavaScript 有推出一套新的語法,試圖簡化選取節點的流程,如以下所示:
刪除節點
parentElement.removeChild(NODE)
NODE.remove()
操作 CSS,例如:
NODE.classList
- 查看目前所有 class 名稱,會回傳類似陣列的清單NODE.classList.add(className1, className2)
- 加入一個或多個樣式NODE.classList.remove(className1, className2)
- 刪除樣式NODE.className = className
- 如果樣式只有一個,也可以直接寫入
DOM 也有提供
style
屬性,例如:
NODE.style.backgroundColor
NODE.style.borderStyle
使用方法是把現有的 CSS 屬性改成 JavaScript 的 camel case 命名風格。雖然你可以透過 DOM API 取代 CSS,但為了讓程式碼一目瞭然地被歸類在它們應該在的位置,建議還是先寫好 CSS 文件,再透過 classList
等方法去修改樣式。
5–4 遍歷(traverse)周邊 DOM 節點
選出周邊的元素節點
5–5 運用 Template Literal 渲染動態網頁
模版字符串(template literal)就是使用反引號``,例如:
let component_es6 = `
<header>
<div class='banner'>
<img src="img1.jpg>
</div>
</header>
`
渲染動態資料
/ 資料
let movies = [{
title: 'The Avengers',
image: 'https://bit.ly/2NQOG6H',
rating: 0
},
{
title: 'Our Times',
image: 'https://bit.ly/2OsGmv2',
rating: 0
},
{
title: 'Aquaman',
image: 'https://bit.ly/2zmcLxo',
rating: 0
}]
// 函式
function displayMovieList (data) {
let htmlContent = `
<table class="table">
<thead>
<tr>
<th>Image</th>
<th>Title</th>
<th>Rating</th>
<th></th>
</tr>
</thead>
<tbody>
`
data.forEach((data) => {
htmlContent += `
<tr>
<td>
<img src = ${data.image} width = "70" class="img-thumbnail" >
</td>
<td>${data.title}</td>
<td>
<span class="fa fa-thumbs-up"></span>
<span class="fa fa-thumbs-down px-2"></span>
<span>${data.rating}</span>
</td>
<td>
<button class="btn btn-sm btn-danger">X</button>
</td>
</tr>
`
})
htmlContent += `
</tbody>
</table>
`
dataPanel.innerHTML = htmlContent
}// 主程式
const dataPanel = document.querySelector('#data-panel')
displayMovieList(movies)
5–6 設置事件
事件處理器 Event Handler & 事件監聽器Event Listener
- 觸發事件的 HTML 元素(Element)
- 事件處理器(Event Listener)
- 事件類型(click、submit)
- 觸發的函式叫事件處理器(event handler)
// listener
element.addEventListener('click', greeting)
// handler
function greeting() {
alert('handling event')
}
addEventListener()可以傳入三個參數:
- 事件名稱 (event name),如
'click'
,下文會有清單介紹 - handler,要啟動的 function,也可以直接使用匿名函式 (見下例)
- useCapture,切換 capturing 機制,一般不會用到。 (見章節5-8),
如果函式不複雜,handler 也可以直接寫在監聽器裡
// listener
element.addEventListener('click', function () {
alert('handling event')
})
使用 addEventListener()的好處
- 可以將多個 handler 綁定到同一個元素上,其他寫法不行
- 觸發事件的元素和監聽對象的不必要是同一個,例如在前面的案例中,我們監聽的是父元素
#controller
,實際被點擊的元素是子元素btn
。
補充:寫在 HTML 屬性裡
有時候,你會看見有人直接把 handler 放在 HTML 屬性裡
<input value="Click me" onclick="greeting()" type="button">
這是早期的作法,當時 JavaScript 除了製作網頁特效以外,幾乎沒有其他的功能,所以直接在 HTML 屬性呼叫函式也挺直覺的。
拆除事件
element.removeEventListener("click", handler)
拆除事件必須使用命名函式的寫法來指定 hanlder (如 greeting
),若使用匿名函式,就無從找到同一個事件。
常見事件
1. 滑鼠事件
click
- 鼠標點擊元素mousemove
- 鼠標滑過元素mouseout
- 鼠標離開元素
2. 鍵盤事件
keydown
- 點擊且長按一個鍵時keyup
- 放開按鍵時
3. 表單事件
submit
- 提交表單時focus
- 點擊某個輸入框時input
- 輸入框內容改變時
4. document 事件
DOMContentLoaded
- 當 HTML 下載完成並完整的建立 DOM 模型時觸發
就和我們總是在 </body>
前引入 JavaScript 檔案一樣,如果 HTML 還沒下載完就先進行 DOM 操作,勢必會遇到奇怪問題。在實務上除了注意檔案引入位置,在 JavaScript 時還會再包一層 DOMContentLoaded
,確保萬無一失。
5-7 實作範例
this
- 監聽器綁定的對象是ul#my-todo
event.target
- 觸發事情的來源,也就是你點擊到的按鈕圖示
// Delete and check
list.addEventListener('click', function (event) {
console.log(this)
console.log(event.target)
if (event.target.classList.contains('delete')) {
let li = event.target.parentElement
li.remove()
} else if (event.target.tagName === 'LABEL') { // add here
event.target.classList.toggle('checked')
}
})
Delete
classList.contains('delete')
classList
等 DOM 指令回傳的是類似陣列的集合,但他們不是陣列。
也可以寫 event.target.matches('.delete')
,是用 CSS 選擇器來篩選 event.target
。
Check
target.tagName ==='LABEL'
觸發事件的物件的 tag name 是 LABEL
,就套用 .checked
樣式。
需要注意的是,當我們使用 tagName 來查詢標籤名稱時,回傳值會一律大寫,所以要寫成 LABEL
。
上述這種由父元素統一監聽,再分頭委派的實作技巧,稱為「事件委派 (event delegation)」,事件委派又以事件傳遞的 capturing & bubbling 模式為基礎。
5–8 傳遞機制:Bubbling & Capturing
網頁元素接收事件的順序,有兩種個方向的傳遞:
由下而上的,稱為事件冒泡 (event bubbling)
由上而下的,稱為事件捕獲 (event capturing)
事件冒泡 Event Bubbling
事件冒泡指的是「從啟動事件的節點 (event.target) 開始,逐層往上傳遞」,直到整個網頁的根節點
document
為止。
例如在 Todo List 裡監聽刪除事件時,click 事件發生在刪除按鈕 i.delete
上,但由於事件為往上傳遞,因此 ul
也收到了 click 事件,因而啟動了監聽器裡的 handler。
可以使用event.stopPropagation()來阻止事件的傳播
事件委派的設計以 bubbling 機制為基礎 (假設子元素會向上報告),如果沒有特別需要,不應刻意制止 bubbling 事件。
事件捕獲 Event Capturing
Capture 機制很少在實作中被運用,因此他通常是隱藏起來的。然而這是事件執行原理的一部分,依照 W3C DOM Events 標準的說明,當你點擊某個元素時,瀏覽器會先從最高層開始 capturing,一路前往到事件 target,再向上 bubbling
由於元素之間有這樣的上下回報機制,我們的事件才能順利地動起來。
實作上,幾乎不會遇到情境需要運用 capture,但刻意想打開時,可以運用事件監聽器的第三個參數。這個參數用來切換 capture,預設值為 false
,刻意想打開時,可以設定為 true
:
element.addEventListener("click", handler, useCapture)
5–9 Q & A
HTMLCollection 和 NodeList 兩者都是 DOM Nodes 的集合(collections),底下都含有其他的 DOM 節點(Nodes),可以說在一般操作上是屬於幾乎相同的東西。
主要差異在可以包含的節點種類不同:
NodeList 可以包含元素節點(Element nodes)、文字節點(Text nodes)
HTMLCollection 只能包含元素節點
不同的 DOM 操作語法會回傳不同種類的結點集合:
- querySelectorAll、node.childNodes 會返回 NodeList
- getElementsBy*、node.children 和 會返回 HTMLCollection
nextElementSibling & nextSibling
- nextElementSibling
只會跑 element node - nextSibling
會跑過所有 node ( text node 、 element node 、 comment node … )
— — 內容節錄自ALPHA Camp學習資源 — —