使用者隨時可能點擊按鈕、按下鍵盤、或是捲動頁面。
而我們寫的 JavaScript 程式,要怎麼知道使用者做了這些操作,並且做出對應的反應?
這就是「事件處理」要解決的問題。
這篇文章會從最原始的做法開始,一步步帶你理解瀏覽器的事件處理機制。
最原始的做法:輪詢按鍵狀態
想像早期電腦的鍵盤,每個按鍵只有兩種狀態:0 代表沒有被按,1 代表正在被按。
你的程式去問鍵盤「這個鍵現在有沒有被按?」,得到的只是當下的答案,不是歷史紀錄。
問題在於,使用者按下一個鍵、然後放開,這個動作可能只持續幾十毫秒。
如果你的程式剛好在按鍵被按下的那一刻去問,會看到 1。
但只要晚一點點,按鍵已經放開了,你看到的就是 0,就好像什麼事都沒發生一樣。
也就是說,鍵盤不會主動告訴你「剛才有人按了某個鍵」,你唯一能做的就是自己不斷去問,盡量不要錯過那個時機。
這代表你的程式必須不斷地、重複地去問按鍵狀態,中間不能做太多其他事情。
一旦程式忙著執行其他計算,就有可能錯過按鍵被按下的那個瞬間。
這種做法既浪費資源,程式一旦忙起來也很容易漏掉使用者的操作。
改良做法:用佇列記錄事件
有些早期的機器採取了改良的做法。
它們把「盯著鍵盤」這件事交給硬體或作業系統來做,程式本身不需要一直守在那裡。
一旦有按鍵被按下,硬體或作業系統就會把這個事件記錄到一個「佇列」(queue)裡面。
佇列是一種資料結構,先記錄進去的事件會先被處理。
所以就算使用者在很短的時間內按了好幾個鍵,每個事件都會照順序排好,不會漏掉。
程式只要定期去檢查佇列裡有沒有新事件就好。
如果發現有事件,就做出對應的反應。
這樣比第一種做法好多了,但還是有問題。
程式仍然需要「主動」去檢查佇列,而且要夠頻繁。
從按鍵被按下,到程式真正注意到這個事件,中間有一段空窗期,在這段時間內軟體看起來就像沒有反應一樣。
輪詢(Polling):程式主動反覆去問
上面這兩種做法,不管是「不斷檢查按鍵狀態」還是「定期檢查佇列」,都屬於同一種模式,叫做輪詢(polling)。
輪詢的核心概念是:程式自己主動、反覆地去問「有事情發生嗎?有事情發生嗎?」
大多數時候去問的結果都是「沒有事件」,但這些問題還是消耗了 CPU 的時間和資源。
另一個問題是反應速度。
輪詢的間隔越長,從事件發生到程式察覺的時間就越久,使用者會感覺到操作沒有立即回應。
但間隔越短,程式就要問得越頻繁,消耗的資源也越多。
這是一個兩難,沒有辦法同時做到既省資源又反應即時。
事件處理器:讓瀏覽器主動通知你
與其讓程式不斷去問「有事情發生嗎?」,更好的做法是反過來:事件發生的時候,由系統主動通知程式。
瀏覽器就提供了這種機制。
你可以預先告訴瀏覽器:「當使用者點擊按鈕的時候,請執行這個函式。」
這個動作叫做「註冊事件處理器(register event handler)」。
什麼是「註冊」?
「註冊」就是事先登記的意思。
你的程式在執行時,瀏覽器並不知道你之後想對哪些操作做出反應,所以你需要事先告訴它:「如果這件事發生了,請執行這段程式碼。」
瀏覽器內部有一張清單,叫做 Event Listener List。
當你執行註冊這個動作,就是把你的函式寫進這張清單裡。
之後每當事件發生,瀏覽器就會查這張清單,找到對應的函式並執行它。
什麼是「事件」?
「事件」是所有使用者操作與系統狀態變化的統稱。
點擊只是其中一種,按下鍵盤、捲動頁面、網頁載入完成、網路斷線,這些全部都叫做事件。
什麼是「處理器」?
「處理器」指的就是你寫的那個函式。
在 JavaScript 裡,處理器確實就是一個 function,但我們不直接叫它「函式」,是因為「函式」只描述了它的語法形式,沒有說明它的用途。
「處理器」這個名稱,強調的是它的職責:負責應對(handle)某個事件。
有事件,就必須有對應的處理器,兩者是成對存在的。
整句話的邏輯是:「把一個負責應對事件的函式,登記到瀏覽器的監聽清單裡。」
與佇列做法的差別
註冊完之後,你的程式什麼都不用做。
不像佇列那個做法還需要定期去檢查,事件處理器是由瀏覽器在事件發生的當下直接呼叫你的函式。
這個被註冊的函式就叫做事件處理器(event handler)。
用 addEventListener 註冊事件處理器
前面說到,你可以預先告訴瀏覽器:「當使用者點擊按鈕的時候,請執行這個函式。」
那在實際的程式碼裡,這件事要怎麼做到?
在瀏覽器中,addEventListener 就是用來執行「註冊」這個動作的方法。
它的名稱直接說明了它的用途:add(加入)+ event(事件)+ listener(監聽者,也就是你寫的那個事件處理器函式),意思是「把一個事件和對應的函式,登記到瀏覽器的監聽清單裡」。
還記得前面說的 Event Listener List 嗎?
那張清單記錄的是「哪個事件」對應「哪個函式」的配對關係。
執行 addEventListener,就是把這個配對寫進清單裡的動作。
先看一個簡單的例子:
<p>點擊這個頁面來觸發事件處理器。</p>
<script>
window.addEventListener("click", () => {
console.log("You knocked?");
});
</script>這段程式碼做了什麼事?
讓我們拆開來看。
指定監聽對象
window.addEventListener(...)window 是瀏覽器提供的內建物件,代表整個瀏覽器視窗。
我們在 window 上面呼叫 addEventListener,意思是「我要在整個視窗上監聽事件」。
指定事件類型
window.addEventListener("click", ...)第一個參數是事件的名稱,這裡是 "click",代表「滑鼠點擊」事件。
指定事件處理器函式
window.addEventListener("click", () => {
console.log("You knocked?");
});第二個參數是一個函式,這就是我們的事件處理器。
當點擊事件發生時,瀏覽器會自動呼叫 () => { console.log("You knocked?"); },在 console 印出 "You knocked?"。
整個過程用一句話來說就是:
把一個事件處理器函式註冊到
window上,當click事件發生時,瀏覽器就會自動執行它。
小結
這篇文章介紹了程式處理事件的兩種方式:
- 輪詢(polling):程式主動、反覆地去檢查有沒有事件發生,缺點是浪費資源且反應不夠即時。
- 事件處理器(event handler):預先註冊一個函式,事件發生時由系統主動呼叫,是瀏覽器採用的機制。
在瀏覽器中,我們透過 addEventListener 來註冊事件處理器,它接收兩個參數:事件名稱和要執行的函式。