什麼是事件
我想你很可能聽說過事件驅動, 但是事件驅動到底是什麼?為什麼說瀏覽器是事件驅動的呢?為什麼 NodeJS 也是事件驅動的 ? 兩者是一回事麼?
實際上不管是瀏覽器還是 Nodejs 都是事件驅動的,都有自己的事件模型。在這裡,我們只講解瀏覽器端的事件模型,如果對 Nodejs 事件模型感興趣的,請期待我的 Nodejs 部分的講解。
事件驅動通俗地來說就是什麼都抽象為事件。
一次點選是一個事件鍵盤按下是一個事件一個網路請求成功是一個事件頁面載入是一個事件頁面報錯是一個事件...瀏覽器依靠事件來驅動APP執行下去,如果沒有了事件驅動,那麼APP會直接從頭到尾執行完,然後結束,事件驅動是瀏覽器的基石。
本篇文章不講解事件迴圈的內容,事件迴圈部分會在本章的其他章節講解,敬請期待。
一個簡單的例子
其實現實中的紅綠燈就是一種事件,它告訴我們現在是紅燈狀態,綠燈狀態,還是黃燈狀態。 我們需要根據這個事件自己去完成一些操作,比如紅燈和黃燈我們需要等待,綠燈我們可以過馬路。
下面我們來看一個最簡單的瀏覽器端的事件:
html程式碼:
<button>Change color</button>
js程式碼:
var btn = document.querySelector('button');btn.onclick = function() { console.log('button clicked')}
程式碼很簡單,我們在button上註冊了一個事件,這個事件的handler是一個我們定義的匿名函式。當用戶點選了這個被註冊了事件的button的時候,這個我們定義好的匿名函式就會被執行。
如何繫結事件
我們有三種方法可以繫結事件,分別是行內繫結,直接賦值,用addEventListener。
內聯這個方法非常不推薦
html程式碼:
<button onclick="handleClick()">Press me</button>
然後在script標籤內寫:
function handleClick() { console.log('button clicked')}直接賦值
和我上面舉的例子一樣:
var btn = document.querySelector('button');btn.onclick = function() { console.log('button clicked')}
這種方法有兩個缺點
不能新增多個同類型的handlerbtn.onclick = functionA;btn.onclick = functionB;
這樣只有functionB有效,這可以通過addEventListener來解決。
不能控制在哪個階段來執行,這個會在後面將事件捕獲/冒泡的時候講到。這個同樣可以通過addEventListener來解決。因此addEventListener橫空出世,這個也是目前推薦的寫法。
addEventListener舊版本的addEventListener第三個引數是bool,新版版的第三個引數是物件,這樣方便之後的擴充套件,承載更多的功能, 我們來重點介紹一下它。
addEventListener可以給Element,Document,Window,甚至XMLHttpRequest等繫結事件,當指定的事件發生的時候,繫結的回撥函式就會被以某種機制進行執行,這種機制我們稍後就會講到。
語法:
target.addEventListener(type, listener[, options]);target.addEventListener(type, listener[, useCapture]);target.addEventListener(type, listener[, useCapture, wantsUntrusted ]); // Gecko/Mozilla only
type是你想要繫結的事件型別,常見的有click, scroll, touch, mouseover等,舊版本的第三個引數是bool,表示是否是捕獲階段,預設是false,即預設為冒泡階段。新版本是一個物件,其中有capture(和上面功能一樣),passive和once。 once用來執行是否只執行一次,passive如果被指定為true表示永遠不會執行preventDefault(),這在實現絲滑柔順的滾動的效果中很重要。更多請參考Improving scrolling performance with passive listeners
框架中的事件
實際上,我們現在大多數情況都是用框架來寫程式碼,因此上面的情況其實在現實中是非常少見的,我們更多看到的是框架封裝好的事件,比如React的合成事件,感興趣的可以看下這幾篇文章。
React SyntheticEventVue和React的優點分別是什麼?兩者的最核心差異對比是什麼?雖然我們很少時候會接觸到原生的事件,但是了解一下事件物件,事件機制,事件代理等還是很有必要的,因為框架的事件系統至少在這方面還是一致的,這些內容我們接下來就會講到。
事件物件
所有的事件處理函式在被瀏覽器執行的時候都會帶上一個事件物件,舉個例子:
function handleClick(e) { console.log(e);} btn.addEventListener('click', handleClick);
這個e就是事件物件,即event object。 這個物件有一些很有用的屬性和方法,下面舉幾個常用的屬性和方法。
屬性 targetx, y等位置資訊timeStampeventPhase ...方法 preventDefault 用於阻止瀏覽器的預設行為,比如a標籤會預設進行跳轉,form會預設校驗併發送請求到action指定的地址等stopPropagation 用於阻止事件的繼續冒泡行為,後面講事件傳播的時候會提到。 ...事件傳播
前面講到了事件預設是繫結到冒泡階段的,如果你顯式令useCapture為true,則會繫結到捕獲階段。
事件捕獲很有意思,以至於我會經常出事件的題目加上一點事件傳播的機制,讓候選人進行回答,這很能體現一個人的水平。了解事件的傳播機制,對於一些特定問題有著非常大的作用。
一個Element上繫結的事件觸發了,那麼其實會經過三個階段。
第一個階段 - 捕獲階段從最外層即HTML標籤開始,檢查當前元素有沒有繫結對應捕獲階段事件,如果有則執行,沒有則繼續往裡面傳播,這個過程遞迴執行直到觸達觸發這個事件的元素為止。
虛擬碼:
第二個階段 - 目標階段上面已經提到了,這裡省略了。
第三個階段 - 冒泡階段從觸發這個事件的元素開始,檢查當前元素有沒有繫結對應冒泡階段事件,如果有則執行,沒有則繼續往裡面傳播,這個過程遞迴執行直到觸達HTML為止。
虛擬碼:
上述的過程用圖來表示為:
如果你不希望事件繼續冒泡,可以用之前我提到的stopPropagation。
虛擬碼:
事件代理
利用上面提到的事件冒泡機制,我們可以選擇做一些有趣的東西。 舉個例子:
HTML程式碼:
<ul>\t<li>1</li>\t<li>2</li>\t<li>3</li>\t<li>4</li></ul>
JS程式碼:
document.querySelector('ul').addEventListener('click', e => console.log(e.target.innerHTML))
線上地址
上面說了addEventListener會預設繫結到冒泡階段,因此事件會從目標階段開始,向外層冒泡,到我們綁定了事件的ul上,ul中通過事件物件的target屬性就能獲取到是哪一個元素觸發的。
“事件會從目標階段開始”,並不是說事件沒有捕獲階段,而是我們沒有繫結捕獲階段,我描述給省略了。
我們只給外層的ul綁定了事件處理函式,但是可以看到li點選的時候,實際上會打印出對應li的內容(1,2,3或者4)。 我們無須給每一個li繫結事件處理函式,不僅從程式碼量還是效能上都有一定程度的提升。
這個有趣的東西,我們給了它一個好聽的名字“事件代理”。在實際業務中我們會經常使用到這個技巧,這同時也是面試的高頻考點。
總結
事件其實不是瀏覽器特有的,和JS語言也沒有什麼關係,這也是我為什麼沒有將其劃分到JS部分的原因。很多地方都有事件系統,但是各種事件模型又不太一致。
我們今天講的是瀏覽器的事件模型,瀏覽器基於事件驅動,將很多東西都抽象為事件,比如使用者互動,網路請求,頁面載入,報錯等,可以說事件是瀏覽器正常執行的基石。
我們在使用的框架都對事件進行了不同程度的封裝和處理,除了了解原生的事件和原理,有時候了解一下框架本身對事件的處理也是很有必要的。
當發生一個事件的時候,瀏覽器會初始化一個事件物件,然後將這個事件物件按照一定的邏輯進行傳播,這個邏輯就是事件傳播機制。 我們提到了事件傳播其實分為三個階段,按照時間先後順序分為捕獲階段,目標階段和冒泡階段。開發者可以選擇監聽不同的階段,從而達到自己想要的效果。
最後我們通過一個例子,說明了如何利用冒泡機制來實現事件代理。
本文只是一個瀏覽器事件機制的科普文,並沒有也不會涉及到很多細節。希望這篇文章能讓你對瀏覽器時間有更深的理解,如果你對nodejs時間模型感興趣,請期待我的nodejs事件模型。 事件迴圈和事件迴圈也有千絲萬縷的聯絡,如果有時間,我會出一篇關於時間迴圈的文章。