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/