今天要來談談SPA監聽事件的處理,由於篇幅的關係,文章分成上下兩篇,那麼就開始今天的主題吧。我們知道Javascript可以監聽瀏覽器事件,而且可以針對不同事件類型進行監聽。比如要對某個按鈕增加點擊的監聽事件Event Listeners時,可以使用addEventListener:
const button = document.querySelector('#button')
//Event Listeners
button.addEventListener('click',function(){
console.log('button clicked')
})
或是Inline events的onclick事件綁定:
const button = document.querySelector('#button')
//Inline events
button.onclick = function () {
console.log('button clicked')
}
上面兩種方式都可以對目標加入監聽事件,如果這在SPA中會怎麼實現呢?
「簡單阿~不就在render後等標出現再增加監聽嘛」
讓我們試試在SPA裡實做,首先在Post.js裡的render裡加入一個按鈕,為了可以對button目標增加監聽,在元件渲染後的mount裡使用addEventListener加入監聽事件如下:
src/pages/Post.js
export const Post = {
mount: () => {
const button = document.querySelector('#button')
//對button增加監聽
button.addEventListener('click', function () {
console.log('button clicked')
})
},
render: () => {
const content = `
<div class="container">
<div class="row">
<div class="col-md">
<h1>Post page</h1>
<div>This is post page.</div>
<button class="btn btn-primary" id="button">click me</button>
</div>
</div>
</div>
`
return App.render(content)
},
}
來看看這一段執行的結果:
「console有跑出紀錄來,看起來一切正常,沒什麼難嘛」
但如果要對10個目標進行監聽呢?總不會要自己手動增加10個吧?若是有100個不就超級麻煩,這樣以後哪記得自己加了什麼監聽,除了代碼不太好維護,而且你知道的,過多的監聽事件也會影響效能執行。
「那對body增加監聽囉,使用Event Delegation事件的捕捉與冒泡」
好的,讓我們再次看看mount裡如何實做,這次換成對父層元件body增加監聽:
src/pages/Post.js
export const Post = {
mount: () => {
//對body增加監聽
document.querySelector('body').addEventListener('click', function (e) {
//判斷event target id
if (e.target.id === 'button') {
console.log('button clicked')
}
})
},
render: () => {
//...
可以發現這裡只針對父層的body新增一個監聽事件,搭配事件捕捉與冒泡,用流程控制判斷觸發事件目標的id,不像前面的例子重複對目標新增事件。當你點擊按鈕後切換頁面回來再次點擊時,發生了奇怪的事情:
「咦?為什麼console的內容會越來越多,而且每切回來一次就多增加一筆?」
這是因為剛剛在mount裡對body增加監聽,每造訪Post頁面就執行一次addEventListener,連同之前增加的監聽都一起累計了。
「那為什麼在多頁開發切換頁面就不會發生這個問題?」
那是因為多頁的網站在切換時javascript生命週期會重新起算,單頁應用可不是這樣子,新增的監聽事件會隨著生命週期延續,切換頁面後還是會繼續存在。
這時你依舊不死心,想到了移除監聽事件的方法removeEventListener,如果使用這個方法,把先前增加的eventListener給移除呢?
src/pages/Post.js
export const Post = {
mount: () => {
const handler = function (e) {
//判斷event target id
if (e.target.id === 'button') {
console.log('button clicked')
}
}
//移除監聽
document.querySelector('body').removeEventListener('click', handler)
//對body增加監聽
document.querySelector('body').addEventListener('click', handler)
},
render: () => {
//...
這是你以為的移除監聽事件:
很不幸地,結果看到原來的eventListener還是一樣會累加,什麼都沒改變。原來removeEventListener其實是要對上次造訪mount裡的handler進行移除監聽,但在這邊獲得的handler是這次造訪新宣告的,移除的監聽handler不一樣造成這樣的結果。
「那麼使用Inline events的onclick事件綁定handler總可以了吧?」
我們再來看看這會是什麼樣:
src/pages/Post.js
export const Post = {
mount: () => {
const handler = function (e) {
//判斷event target id
if (e.target.id === 'button') {
console.log('button clicked')
}
}
//對body綁定監聽
document.querySelector('body').onclick = handler
},
render: () => {
//….
「這樣總沒問題了吧?」
嗯,現在的確沒問題了。某天主管走到你旁邊,希望你在同一頁做一個新功能,搭配按鈕執行監聽事件。當你自信滿滿地在別的程式碼區段,一樣對body綁定onclick事件,好景不常,你發現之前的監聽事件都失效了,只剩下新功能的onclick事件,因為對同一個目標綁定多個Inline events,最後綁定的Inline events會覆蓋掉之前的。
「呃...難道沒有一個有效的監聽事件方法使用在SPA嗎?」
透過上述冗長的例子可以發現,這些方法似乎都無法完美實做出來,要嘛針對目標一個一個綁定,程式碼會非常難維護且影響效能,要嘛對body綁定然後使用Inline events,但只要再增加事件綁定會覆蓋掉之前的,到底有沒有什麼方式可以幫我們在SPA裡有效實現事件的監聽呢?
仔細想想看,假設有個專門註冊所有元件監聽事件的方法,當元件內有新增監聽事件時會進行註冊,到了別的頁面會找到前一個元件註冊過的監聽事件並進行移除,這樣是不是結合了不重複綁定及不覆蓋handler這兩種優點呢?
明天我們會繼續談談這個部份,並實做有效處理與管理監聽事件的方法,明天見!
參考資料:
1.從ES6開始的JavaScript學習生活-事件處理
2.addEventListener vs onclick
3.Event Delegation — 事件委派介紹 與 觸發委派的回呼函數
4.Event Delegation 事件委派