avatar

目錄
JavaScript 操作 DOM 及事件傳遞

Window 及 Document

window 是指瀏覽器視窗(最上層的全域物件),包含了 DOM。支援分頁的瀏覽器中,每一個分頁標籤都擁有自己的 window 物件。其本身有許多屬性跟方法。例如:

// 跳出提示視窗

window.alert('Hello world')

// 延遲某段時間(單位為毫秒)後,再執行一次 callback

window.setTimeout(callback, 5000)

// 固定延遲了某段時間,不斷循環執行 callback

windwo.setInterval(callback, 5000)

// 開啟一個新視窗

window.open()

// 頁面載入時觸發

window.onload()

// 歷史列表

window.history

// 視窗中載入的 Document 物件

window.document

// 記錄瀏覽器 (broswer) 相關資訊

window.navigator

window.navigator 屬性如下:

屬性 值
appCodeName 設定的 code 名稱
appName 使用瀏覽器的官方名稱
appVersion 使用瀏覽器的版本
buildID 使用瀏覽器的 ID,ID 的格式為 “YYYYMMDDHH”
cookieEnabled 瀏覽器是否可以寫入 cookie
language 瀏覽器的語系編碼
onLine 瀏覽器是否有連線
oscpu 作業系統及 CPU 名稱
platform 平台(作業系統)名稱
plugins 所有瀏覽器安裝的附加元件
… …

ps.使用時前面 window 可省略不寫

document 是指當前的 HTML 頁面。而 DOM (Document Object Model) 文件物件模型,是瀏覽器將 HTML 的標籤元素轉換成 JavaScript 可操作的物件。DOM 中的每個 HTML 元素就是一個節點。而 document 是根節點。

取得 DOM 元素

取得單一元素

document.getElementById('id')

const el = document.querySelector('#id')

or

const el = document.querySelector('.class')

取得多個元素

// 取得一整個 input 陣列的值,取值要加陣列索引

const inputElement = document.getElementsByTagName('div');

// 取得所有符合的元素節點列表(非陣列)

const allEl = document.querySelectorAll('.class')

// 轉成陣列後,就可用陣列方法

Array.from(allEl).foreach(element => { ... })

// 動態插入一個新的 div
const el = document.createElement('div') document.body.appendChild(el)

ps. querySelector() 不能查詢動態更新的元素,要用 getElement 的方法

屬性(properties、attributes)處理

// 檢查是否有該屬性

document.querySelector('a').hasAttribute('href');

// 取得屬性值

document.querySelector('a').getAttribute('href');

// 設定屬性

document.querySelector('a').setAttribute('href', 'Link');

ps.這些方式會讓瀏覽器進行重繪(redraw),拖慢效能。除非必要再使用。改用以下方式來處理

// 取得屬性

const value = el.value

// 設定屬性,直接指定

el.value = 'hi'

// 設定多個,用 Object.assign()

Object.assign(el.{ value: 'hello', id: 'world' })

// 刪除屬性

el.value = null

ps.目前大多都直接將屬性集合寫在 class 類別再使用。

類別(class)處理

// 增加類別,可多個

el.classList.add('abc','abc1')

// 移除類別,可多個

el.classList.remove('abc')

// 切換類別,元素上沒有這類別時,就新增類別;如果元素已經有了這類別,就刪除。

el.classList.toggle('abc')

// 檢查是否有此類別

el.classList.contains('abc')

// 查詢類別數量

el.classList.length

// 取得所有類別

const all = el.classList

// 修改類別(有 - 用[ ]語法)

el.style.background = 'gray';
el.style['background-color'] = '#000';

// 直接讀寫類別

el.style.cssText = 'font-size: 16x; color: gray;';

// 取得屬性值

window.getComputedStyle(el).getPropertyValue('font-size')

DOM 處理

// 建立元素節點

const divEl = document.createElement('div(tagName)')

// 建立文字節點

const TextNode = document.createTextNode('hello world')

// 在 el 元素的最後新增 el2 元素

el.appendChild(el2)

// NODE.insertBefore(newNode, refNode),將新節點 newNode 插入至指定的 refNode 節點的前面

el.insertBefore(el2, el3)

// 在 el 元素裡的 el3 元素之後新增 el2 元素

el.insertBefore(el2, el3.nextSibling)

// 替換,將 el 內的 oldNode 替換成 newNode

el.replaceChild(newNode, oldNode);

// 複製

const cloneEl = el.cloneNode()

// 移除(需要從父元素移除)

parentEl.removeChild(el)

// 移除本身元素

el.parentNode.removeChild(el)

// 修改元素的內容(包含子元素內容)

el.innerHTML = '<div><h1>hello world</h1></div>'

// 可增進效能的 document.createDocumentFragment(),DocumentFragment 不是真實的 DOM 結構,不會影響目前文件,所以不會導致回流(reflow)

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ul = document.getElementById("List");

// 建立 DocumentFragment
const fragment = document.createDocumentFragment();

for (let i = 0; i < 2; i++){
// 產生 li,加入文字後加入 fragment
let li = document.createElement("li");
li.appendChild(document.createTextNode("No." + (i+1)));
fragment.appendChild(li);
}

// 最後將 fragment 加在 ul 後
ul.appendChild(fragment);

事件

事件資訊 function(e)

event 資訊會放在 callback function 裡面的第一個參數,通常都是取名 event 或簡寫 e,可當成是物件,內有各種此事件的參數值。此 callback 執行的時機是事件觸發後。e.eventPhase 是表示事件在那個階段(Phase)被觸發,用數字來表示(1:CAPTURING 2:TARGET 3:BUBBLING)

e.g.

click 滑鼠點擊事件

  • e.target // 被點擊到的元素
  • e.screenX // 滑鼠離視窗左邊的距離
  • e.screenY // 滑鼠離視窗上邊的距離

keydown 鍵盤按下按鍵

  • e.key // 按鍵號碼

事件監聽

監控標籤元素發生某件事(例如被點擊…)之後要進行的動作,若要改變這個事件要在什麼階段觸發,可以在 el.addEventListener() 加上第三個參數(可不加,不加為預設),為 boolean 值(true:捕獲、false(預設):冒泡)。

// 單一元素監聽

<button id="btn" onclick="console.log('Hello');">Click</button>

ps.不建議用上述方式綁定(框架例外)

Code
1
2
3
el.addEventListener('click', function (event) {
...
},false)

//監聽多個元素,event.target 取得觸發目標元素

Code
1
2
3
4
5
Array.from(allEl).forEach(element => {
element.addEventListener('change', function (event) {
console.log(event.target.value)
})
})

// 解除事件監聽(註冊)

Code
1
2
3
el.removeEventListener('click', function(){
console.log('HI');
}, false);

ps. 目前在 Vue & React 等網頁框架中,如果是使用內建的語法註冊事件監聽,它們都會自動在無用的時候移除,可以放心使用。但若是自己寫事件監聽,務必要記得移除。

傳遞機制(捕獲 CAPTURING 與冒泡 BUBBLING)

假設有一個列表,組成為 ul 跟 多個 li,當點了其中的一個 li。也相當於點擊了 ul。因為 li 被 ul 包住了。這樣一來 ul 及 li 的所監聽事件都將觸發。

事件傳遞順序是:
先捕獲:從根節點往下傳遞到 target,過程中觸發各別元素的捕獲階段事件監聽。
再冒泡:從 target 事件觸發後再逐層向上回到根節點過程中事件依序被觸發。

當事件傳到 target 本身時,沒有分捕獲跟冒泡。執行順序按照 addEventListener的順序而定,先添加的先執行,後添加的後執行。

取消事件傳遞 e.stopPropagation()

不會再把事件傳遞給上或下一個節點,看是捕獲跟冒泡機制。但若是同一個節點上,不只一個 listener 的話,還是會執行。

取消相同事件 e.stopImmediatePropagation()

當元素有綁定多個同樣的事件時,監聽將會按照註冊的先後順序被呼叫,若其中一個監聽事件呼叫了 e.stopImmediatePropagation(),將會阻止所有後面的綁定事件。

阻止預設動作 e.preventDefault()

取消瀏覽器的元素預設行為,跟事件傳遞無關。以下是比較常用的情況:

<form> 的 submit // 阻止送出表單
<a> 的 click 事件 // 阻止轉址
<input> 的 keypress 事件 // 阻止輸入按鍵

事件代理、委派(Event delegation)

一種利用Event Bubbling,加上事件有兩個特別屬性:

target & currentTarget

target: 觸發的位置
currentTarget: 綁定處理器的位置(可加可不加)

而能減少監聽器數目的方法。

為了處理大量的事件監聽跟動態新增元素(appendChild)的事件監聽,可以透過事件傳遞的機制,將子元素事件監聽器交由父元素代理。

例如列表(ul、li)的監聽,可將監聽機制設定在 ul 上,而不是內部的每一個 li。如此一來不管 li 數量有多少,都可以利用事件傳遞機制(冒泡)來做事件監聽。

e.g.

Code
1
2
3
4
5
6
7
8
9
10
<div class="parent">
<div class="child" data-name="a"></div>
<div class="child" data-name="b"></div>
<div class="child" data-name="c"></div>
<div class="subitem" data-name="d"></div>
</div>

$('.parent').on('click', '.child', function(){
console.log($(this).data('name'));
});

當點擊不同的小區塊時,就會 console 出個別的名字,例如:a、b 或c。

實作方法是將 click 事件綁在 parent 上,藉由Event Bubbling來傳遞給 child,而非直接將事件綁定在 child 上。

優點:是可減少監聽器的數目。
缺點:是由於需要判斷哪些子元素是我們有興趣的項目,而必須多寫一些程式碼做判斷。

在上面的例子,我們加上一個filter 「.child」,表示只對有 「.child」節點有興趣,而沒有加上 「.child」的節點則不被影響,例如 click「.subitem」這個節點之後就不會 console 它的名字。

補充:
window.onload 與 $(document).ready 的使用差異。可以參考 https://ithelp.ithome.com.tw/articles/10092601 此文章內容。基本上建議使用 $(document).ready。

參考:
https://ithelp.ithome.com.tw/articles/10191867
https://www.fooish.com/javascript/dom/css.html
https://jmln.tw/blog/2017-07-07-vanilla-javascript-dom-manipulation.html
http://fstoke.me/blog/?p=2487
https://yakimhsu.com/project/project_w7_DOM.html
https://yakimhsu.com/projectproject_w7_eventListener.html
https://blog.techbridge.cc/2017/07/15/javascript-event-propagation/
https://medium.com/schaoss-blog/前端三十-07-js-瀏覽器-dom-元素的事件代理是指什麼-95f1e8f311db
https://medium.com/schaoss-blog/什麼捕獲冒泡-你是魚嗎-聊聊瀏覽器-dom-的事件傳遞-b44454690661
https://pydoing.blogspot.com/2011/10/javascript-window-navigator.html
https://ithelp.ithome.com.tw/articles/10191970
https://cythilya.github.io/2015/07/08/javascript-event-delegation/

繁