瀏覽器事件模型

什麼是事件

我想你極可能據說過事件驅動, 可是事件驅動究竟是什麼?爲何說瀏覽器是事件驅動的呢?爲何 NodeJS 也是事件驅動的 ? 二者是一回事麼?html

實際上不論是瀏覽器仍是 Nodejs 都是事件驅動的,都有本身的事件模型。在這裏,咱們只講解瀏覽器端的事件模型,若是對 Nodejs 事件模型感興趣的,請期待個人 Nodejs 部分的講解。node

事件驅動通俗地來講就是什麼都抽象爲事件react

  • 一次點擊是一個事件
  • 鍵盤按下是一個事件
  • 一個網絡請求成功是一個事件
  • 頁面加載是一個事件
  • 頁面報錯是一個事件
  • ...

瀏覽器依靠事件來驅動APP運行下去,若是沒有了事件驅動,那麼APP會直接從頭至尾運行完,而後結束,事件驅動是瀏覽器的基石。面試

本篇文章不講解事件循環的內容,事件循環部分會在本章的其餘章節講解,敬請期待。瀏覽器

一個簡單的例子

其實現實中的紅綠燈就是一種事件,它告訴咱們如今是紅燈狀態,綠燈狀態,仍是黃燈狀態。 咱們須要根據這個事件本身去完成一些操做,好比紅燈和黃燈咱們須要等待,綠燈咱們能夠過馬路。網絡

WeChatf64afbca051509ddcf379077490e9a2e.png

下面咱們來看一個最簡單的瀏覽器端的事件:併發

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')
}
複製代碼

這種方法有兩個缺點

  1. 不能添加多個同類型的handler
btn.onclick = functionA;
btn.onclick = functionB;
複製代碼

這樣只有functionB有效,這能夠經過addEventListener來解決。

  1. 不能控制在哪一個階段來執行,這個會在後面將事件捕獲/冒泡的時候講到。這個一樣能夠經過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的合成事件,感興趣的能夠看下這幾篇文章。

雖然咱們不多時候會接觸到原生的事件,可是瞭解一下事件對象,事件機制,事件代理等仍是頗有必要的,由於框架的事件系統至少在這方面仍是一致的,這些內容咱們接下來就會講到。

事件對象

全部的事件處理函數在被瀏覽器執行的時候都會帶上一個事件對象,舉個例子:

function handleClick(e) {
  console.log(e);
}  

btn.addEventListener('click', handleClick);
複製代碼

這個e就是事件對象,即event object。 這個對象有一些頗有用的屬性和方法,下面舉幾個經常使用的屬性和方法。

  • 屬性

    1. target
    2. x, y等位置信息
    3. timeStamp
    4. eventPhase

    ...

  • 方法

    1. preventDefault 用於阻止瀏覽器的默認行爲,好比a標籤會默認進行跳轉,form會默認校驗併發送請求到action指定的地址等
    2. stopPropagation 用於阻止事件的繼續冒泡行爲,後面講事件傳播的時候會提到。

    ...

事件傳播

前面講到了事件默認是綁定到冒泡階段的,若是你顯式令useCapture爲true,則會綁定到捕獲階段。

事件捕獲頗有意思,以致於我會常常出事件的題目加上一點事件傳播的機制,讓候選人進行回答,這很能體現一我的的水平。瞭解事件的傳播機制,對於一些特定問題有着很是大的做用。

一個Element上綁定的事件觸發了,那麼其實會通過三個階段。

  • 第一個階段 - 捕獲階段

從最外層即HTML標籤開始,檢查當前元素有沒有綁定對應捕獲階段事件,若是有則執行,沒有則繼續往裏面傳播,這個過程遞歸執行直到觸達觸發這個事件的元素爲止。

僞代碼:

function capture(e, currentElement) {
	if (currentElement.listners[e.type] !== void 0) {
		currentElement.listners[e.type].forEach(fn => fn(e))
	}


	// pass down
	if (currentElement !== e.target) {
		// getActiveChild用於獲取當前事件傳播鏈路上的子節點
		capture(e, getActiveChild(currentElement, e))
	} else {
		bubble(e, currentElement)
	}
}

// 這個Event對象由引擎建立
capture(new Event(), document.querySelector('html'))

複製代碼
  • 第二個階段 - 目標階段

上面已經提到了,這裏省略了。

  • 第三個階段 - 冒泡階段

觸發這個事件的元素開始,檢查當前元素有沒有綁定對應冒泡階段事件,若是有則執行,沒有則繼續往裏面傳播,這個過程遞歸執行直到觸達HTML爲止。

僞代碼:

function bubble(e, currentElement) {
	if (currentElement.listners[e.type] !== void 0) {
		currentElement.listners[e.type].forEach(fn => fn(e))
	}
	// returning
	if (currentElement !== document.querySelector('html')) {
		bubble(e, currentElement.parent)
	}
}

複製代碼

上述的過程用圖來表示爲:

若是你不但願事件繼續冒泡,能夠用以前我提到的stopPropagation

僞代碼:

function bubble(e, currentElement) {
	let stopped = false;
	function cb() {
		stopped = true;
	}
	if (currentElement.listners[e.type] !== void 0) {
		currentElement.listners[e.type].forEach(fn => {
			fn({
				...e,
				stopPropagation: cb
			});
			if (stopped) return;
		})
	}
	// returning
	if (currentElement !== document.querySelector('html')) {
		bubble(e, currentElement.parent)
	}
}

複製代碼

事件代理

利用上面提到的事件冒泡機制,咱們能夠選擇作一些有趣的東西。 舉個例子:

咱們有一個以下的列表,咱們想在點擊對應列表項的時候,輸出是點擊了哪一個元素。

HTML代碼:

<ul>
	<li>1</li>
	<li>2</li>
	<li>3</li>
	<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事件模型。 事件循環和事件循環也有千絲萬縷的聯繫,若是有時間,我會出一篇關於時間循環的文章。

相關文章
相關標籤/搜索