件處理
客戶端js程序采用異步事件驅動編程模型。在這種情況下當文檔,瀏覽器,元素發生一些事情的時候,會產生事件。
舉例 當瀏覽器加載完文檔以后會觸發一個事件。該事件會有一個函數進行處理,即回調函數
這種只不單單用于web界面,所有使用圖形界面的應用程序都采用了這種方式。
事件分類
依賴于設備的輸入事件
有些事件和特定輸入設備直接相關。比如鼠標和鍵盤。
touchmove 當觸點在觸控平面上時發生該事件
獨立于設備的輸入事件
click事件表示激活了鏈接的事件。通過鼠標,按鈕或者移動設備上的觸摸觸發該事件
用戶界面事件
通常用于HTML表單元素,包括文本輸入域獲取鍵盤焦點的focus事件,提交按鈕將會觸發submit事件
狀態變化事件
不是由用戶活動由網絡或者瀏覽器活動觸發,表示某種生命周期或相關狀態的變化。
online 返回瀏覽器的聯網狀態
特定的api事件
一些web api會有自己的事件類型
拖放的api dragstart 當用戶拖動一個元素,或者選擇一個文本的時候觸發該事件
計時器和錯誤處理程序
計時器在指定的時間后觸發該事件,錯誤處理程序,try catch 對應于一個響應,會有異步進行拋出
傳統事件類型
表單事件
當提交表單和重置表單時會觸發submit和reset事件,當用戶和類按鈕(包括單選和復選)交互的時,將會發生click事件,當用戶輸入文字,選擇選項或選擇復選框改變相應的表單元素的狀態時,將會觸發change事件,通過鍵盤改變焦點的表單元素在得到和失去焦點時將會觸發focus和blur事件。
通過事件處理程序能取消submit和reset事件的默認操作。某些click事件也是如此,focus和clur事件不會冒泡,但其他所有表單事件都可以。
無論用戶何時輸入文字,都會觸發input事件。
window事件
window事件是指事件的發生與瀏覽器窗口本身而非窗口中顯示的任何特定文檔內容相關。
load事件
load事件與文檔和其所有外部資源(圖片)完全加載并顯示給用戶時將會觸發。
unload事件
unload事件,當用戶離開當前文檔轉向其他文檔時將會觸發。
unload事件處理程序可以用于保存用戶的狀態,但其不能取消用戶轉向其他地方。
beforeunload事件
此事件將會詢問用戶是否確定離開當前頁面。如果beforeunload的回調函數返回一個字符串,那么在新頁面加載之前,字符串將會出現在展示給用戶確認的對話框上,這樣用戶將會有機會取消跳轉停留在當前頁上
注意;該事件僅僅是在當前頁面的跳轉更改等,轉換標簽不會觸發該事件
onerror屬性
此為一個window對象的屬性。在js出錯的時候將會觸發其
其他
像img元素這樣的替換元素也能為其注冊load和error事件處理程序。當外部資源完全加載或發生阻塞加載錯誤時將會觸發該事件。某些瀏覽器也支持about事件,當圖強因為用戶停止加載進程而導致失敗的時候也會觸發該事件。
focus和blur事件也為window事件,當瀏覽器窗口從操作系統中得到或失去鍵盤焦點的時候將會觸發該事件
當用戶調整瀏覽器窗口大小或滾動其會觸發resize和scroll事件,scroll事件也能在任何可以滾動的文檔元素上觸發,例如css的的overflow屬性也能觸發。
鼠標事件
當用戶在文檔上移動和單擊鼠標時都會產生鼠標事件。這些事件在鼠標指針所對應的最深嵌套元素上觸發。但其會冒泡直到文檔的最頂層。
clientX和clientY屬性指定了鼠標在窗口坐標中的位置。button和which屬性指定了按下的鼠標鍵是哪個。
當鼠標輔助鍵按下的時候,對應的屬性為altkey和ctrlkey和metakey和shiftkey會設置為true,對于click事件,detail屬性指定了其是單擊,雙擊,還是三擊。
每當用戶移動和拖動鼠標時,會觸發mousemove事件,當用戶按下或釋放鼠標按鍵的時候觸發mousedown和mouseup事件。
在mousedown和mouseup事件隊列之后,瀏覽器也會觸發click事件,如果用戶在很短的時間內單擊兩次鼠標,則第二個事件為dblclic事件,當單擊鼠標右鍵時,瀏覽器會顯示上下文菜單,在顯示菜單之前,也會觸發contextmenu事件,如果取消這個事件將會阻止菜單的顯示,該事件為獲得鼠標右擊通知的最簡單方法。
當用戶移動鼠標指針從而使它懸停到新元素上時,瀏覽器就會在該元素上觸發mouseover事件,當鼠標移動指針從而使它不在懸停在某個元素上時,瀏覽器會觸發mouseout事件,(該事件有relatedTarget屬性指明這個過程涉及的其他元素)
當用戶滾動鼠標的時候,瀏覽器觸發mousewheel事件,傳遞事件對象屬性指定輪子轉動的大小和方向。
鍵盤事件
當鍵盤聚焦到web瀏覽器時,用戶每次按下或釋放鍵盤上的按鍵時都會產生事件,鍵盤快捷鍵葉同樣能被瀏覽器和操作系統吃掉,此時對js事件處理程序不可見,無論任何文檔元素獲取鍵盤焦點都會觸發鍵盤事件,并會冒泡到window對象,
觸摸屏和移動設備事件
用戶旋轉設備的時會產生orientationchange事件,有一個縮放和旋轉手勢,當手勢開始時將會生成getsturestart事件,手勢結束時將會生成gestureend事件。在這兩個事件之間是跟蹤手勢過程的gesturechange事件隊列,將事件傳遞的事件對象屬性為scale和rotation
握緊手勢的scale值小于1.0
撐開手勢的scale的值大于1.0
rotation為事件開始時手指旋轉的角度。以度為單位正值表示順時針方向旋轉
當手指觸摸屏幕的時候將會觸發touchstart事件,當手指移動時會觸發touchmove事件,當手指離開屏幕時會觸發touchend事件,觸摸事件傳遞的事件對象有一個changedTouches屬性,該屬性為一個類數組對象,其每個元素都描述觸摸的位置。
當設備允許用戶從豎屏旋轉到橫屏模式時會在window對象上觸發orientationchanged事件。該對象的orientation屬性能給出當前方位,其值為0, 90, 180, 或 -90
注冊事件處理程序
設置js對象屬性為事件處理程序
事件處理程序屬性的名字由on后面跟著事件名組成。onclick,onchange,onload,onmouseover
onload 當對象的資源被加載的時候,該對象的onload事件將會被觸發,然后將表單的提交的onsubmit事件和一個處理函數進行綁定
onsubmit 在表單提交的時候,將會觸發該事件
下方栗子演示一個提交的時候表單驗證的過程
其中validate函數為一個自定義的表單驗證函數,其函數的參數this指向elt
window.onload = () => {
// 查找一個<form>元素
var elt = document.getElementById("shipping_address");
// 注冊事件處理程序函數
// 在表單提交之前調用該函數
elt.onsubmit = () => { return validate(this); };
}
設置HTML標簽屬性為事件處理程序
<button onclick="alert('Thank you');">點擊這里</button>
點擊按鈕會彈出一個對話框
addEventListener()
為事件綁定一個處理的函數
<script>
var b = document.getElementById("my button");
b.onclick = () => { alert("Thanks for clicking me!"); };
b.addEventListener("click", () => { alert("Thanks again!"); }, false); // 最后一個參數為是否進行冒泡
</script>
上方會彈出兩個對話框,一個觸發了onclick事件,一個觸發了addEventListener注冊的click事件。
注冊的click處理的函數,將會按照注冊的順序依次不斷的調用。(可以注冊多個處理程序)。
document.removeEventListener("mousemove", handleMouseMove, true);
移出在mousemove上注冊的事件,處理函數為handLenMouseMove,在事件上直接注冊,沒有冒泡的過程。
事件處理程序的調用
事件處理程序運行的環境
this關鍵字指的是事件的目標。
事件處理程序的作用域
這是個坑的集散地,表單上有一個HTML事件處理程序想要引用window的location對象,必須使用window.location,不能使用location,因為該location為表單作用域鏈上的location
事件處理程序的返回值
如果返回false則告訴瀏覽器不要執行這個事件的默認操作。
調用順序
通過設置對象屬性,HTML屬性注冊的處理程序一直優先調用
使用addEventList注冊的處理程序,按照DOM的順序進行調用
使用attachEvent()注冊的事件按照隨機的順序調用
事件傳播
當事件目標為window對象或其他一些單獨對象,瀏覽器會簡單的調用對象上的處理程序響應事件。
調用目標元素上注冊的事件處理函數
在調用目標元素上注冊的事件處理函數的時候,該事件會冒泡到DOM樹的樹跟,調用目標的父元素的事件處理程序,即可以在共同的祖先元素上注冊一個處理程序來處理所有的事件
例如:在form元素上注冊change事件處理程序取代在表單的每個元素上注冊change事件處理程序。
原因:冒泡
注意:load事件,(load當資源加載完成以后,將會觸發load事件,不單單指整個文檔)其會在Document對象上停止冒泡不會傳播到window對象,只有整個文檔都加載完成的時候將會觸發window對象的load事件
捕獲
事件的第一階段 捕獲 發生在目標處理程序調用程序之前 addEventener()把一個布爾值作為其第三個參數,如果為true那么事件處理程序被注冊為捕獲事件處理程序,會在事件傳播的第一個階段調用。事件傳播的捕獲階段像是反向的冒泡,,將會最先調用window對象的捕獲處理程序,然后將會調用document對象的捕獲處理程序,接著是body對象的捕獲處理程序,逐漸按照DOM樹往下,直到調用事件目標的父元素的捕獲事件捕獲。
事件取消
通過調用事件對象的preventDefault()方法取消事件的默認操作。
文檔加載事件
web應用需要web瀏覽器通知它們文檔加載完畢和為操作準備就緒的時間。
當文檔準備就緒的時候調用函數
事件冒泡和傳播
事件冒泡屬于微軟的,向上
事件傳播屬于網景瀏覽器(懷舊,一個時代,可惜已經不存在了),正好相反。
后來w3c將這兩種給統一了,規定任何事件首先向下傳播直到遇到目標元素,如果沒有遇到冒泡元素,將會不斷的向上冒泡進行返回。
件被看作是 JavaScript 與網頁之間交互的橋梁,當事件發生時,可以通過 JavaScript 代碼(函數)執行相關的操作。例如,用戶可以通過鼠標拖曳登錄框,改變登錄框的顯示位置;或者在閱讀文章時,選中文本后自動彈出分享、復制選項。本章將對 DOM 中的事件進行詳細講解。
事件可被理解為是 JavaScript 偵測到的行為,這些行為指的就是頁面的加載、鼠標單擊頁面、鼠標滑過某個區域、按下鍵盤等具體的動作,它對實現網頁的交互效果起著重要的作用。在深入學習事件時,需要對一些非常基本又相當重要的概念有一定的了解。
事件處理程序指的就是 JavaScript 為響用戶行為所執行的程序代碼。例如,用戶單擊 button 按鈕時,這個行為就會被 JavaScript 中的click 事件偵測到;然后讓其自動執行,為 click 事件編寫的程序代碼,如在控制臺輸出“按鈕被單擊了”。
事件驅動是指,在 Web 頁面中 JavaScript 的事件,偵測到的用戶行為(如鼠標單擊、鼠標移入等),并執行相應的事件處理程序的過程。
事件綁定指的是為某個元素對象的事件綁定事件處理程序。在 DOM 中提供了3種事件的綁定方式。下面將針對以3種事件綁定方式的語法以及各自的區別進行詳細講解。
事件的行內綁定式是通過HTML標簽的屬性設置實現的,具體語法格式如下。
<div onclick="alert('handle click')"></div>
在上述語法中,div 可以是任意的HTML標簽,如 <button>、<input>標簽等;事件是由 on 和事件名稱組成的一個 HTML 屬性,如單擊事件對應的屬性名為 onclick;事件的處理程序指的是 JavaScript 代碼,如匿名函數等。
需要注意的是,由于開發中提倡 JavaScript 代碼與 HTML 代碼相分離。因此,不建議使用行內綁定事件。
動態的綁定方式很好地解決了JavaScript代碼與HTML代碼混合編寫的問題。在JavaScript代碼中,為需要事件處理的 DOM 元素對象,添加事件與事件處理程序。具體語法格式如下。
div.onclick = function handleClick () {
console.log('handle click');
};
在上述語法中,事件的處理程序一般都是匿名函數或有名的函數。在實際開發中,相對于行內綁定來說,事件的動態綁定的使用居多。
行內綁定與動態綁定除了實現的語法不同以外,本質是相同的,都是給 DOM 元素綁定了一個 onclick 屬性。
為了給同一個 DOM 對象的同一個事件添加多個事件處理程序,DOM中引入了事件流的概念,可以讓DOM對象通過事件監聽的方式實現事件的綁定。由于不同瀏覽器采用的事件流實現方式不同,事件監聽的實現存在兼容性問題。通常根據瀏覽器的內核可以劃分為兩大類,一類是早期版本的IE瀏覽器(如IE6~8),一類遵循W3C標準的瀏覽器(以下簡稱標準瀏覽器)。
接下來,將根據不同類型的瀏覽器,分別介紹事件監聽的實現方式。
(1)早期版本的IE瀏覽器
在早期版本的IE瀏覽器中,事件監聽的語法格式如下。
DOM對象.attachEvent(type,callback);
在上述語法中,參數 type 指的是為 DOM 對象綁定的事件類型,它是由 on 與事件名稱組成的,如 onclick。。參數 callback 表示事件的處理程序。
(2)標準瀏覽器
標準瀏覽器包括IE8版本以上的IE瀏覽器(如IE9~11),新版的Firefox、Chrome等瀏覽器。具體語法格式如下。通過這種方式我們可以給元素注冊多個事件處理函數,而 btn.onclick = fn 是賦值操作只能設置一個事件處理函數。
DOM對象.addEventListener(type, callback, [capture]);
在上述語法中,參數 type 指的是 DOM 對象綁定的事件類型,它是由事件名稱設置的,如 click。參數 callback 表示事件的處理程序。參數 capture 默認值為 false,這個屬性后面單獨介紹,一般情況我們都使用它的默認值。
現在 IE 瀏覽器已經被淘汰,所以我們不需要再去記憶 attachEvent() 的用法,但是我們需要了解過去,過去在使用這種方式注冊事件的時候需要處理瀏覽器的兼容性,下面我們演示下:
function addEventListener(element, type, listener) {
// 能力檢測: 就是要看當前的瀏覽器是否支持此標簽對象的屬性或是方法
if (element.addEventListener) {
element.addEventListener(type, listener, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, listener);
} else {
element['on' + type] = listener;
}
}
在事件處理函數中的 this 指向當前觸發該事件的 DOM 元素。
link.onclick = function handleLink () {
photo.src = this.href;
p.textContent = this.title;
return false;
};
但是通過行內綁定注冊的事件,調用的函數中 this 指向的是 window。
<button onclick="handle()">按鈕</button>
<script>
function handle () {
// 此處的 this 指向 window
console.log(this);
}
</script>
行內綁定事件 onclick="handle()" 中的 "" 雙引號內部其實可以看做是一個匿名函數,"" 雙引號內部的這個匿名函數才是事件處理函數,在事件處理函數中又調用了 handle() 方法。
<!-- 此處的 this 指向的是觸發事件的對象 button -->
<button onclick="handle(this)">按鈕</button>
<script>
function handle (btn) {
// 此處的 this 指向 window
console.log(btn);
}
</script>
綁定事件的元素可以解除綁定,例如:我們可以讓按鈕點擊一次之后解除事件綁定。三種綁定事件的解除事件的方式不同,下面我們分別來介紹。
行內綁定事件和動態綁定事件本質上都是給 DOM 元素設置 onclick 屬性,對應的解除綁定事件的方式都是把 onclick 屬性重新設置為 null。
當按鈕執行完點擊事件的處理程序后立即解除事件的綁定
<button onclick="handle(this)">按鈕</button>
<script>
function handle (btn) {
alert('Hello');
btn.onclick = null;
}
</script>
btn.onclick = function handle () {
this.onclick = null;
};
標準綁定事件使用 addEventListener(type, callback, [capture]); 方法,對應的解除綁定使用的方法是 removeEventListener(type, callback, [capture]),需要注意的是,如果注冊的事件需要解除的話,使用 addEventListener() 注冊事件的時候,傳入的 callback 不能是匿名函數,因為解除事件綁定的時候還需要引用這個函數。
const div = document.querySelector('#div');
div.addEventListener('click', handle);
function handle () {
alert('hello');
this.removeEventListener('click', handle);
}
我們已經會使用 addEventListener(type, callback, [capture]),方法給元素注冊事件,但是這個方法的第三個參數的作用我們還不清楚,下面我們就來介紹該方法的第三個參數,這里我們需要先來學習 DOM 中的事件流(事件模型)。
DOM (文檔對象模型)結構是一個樹型結構,當一個HTML元素產生一個事件時,該事件會在元素結點與根節點之間按特定的順序傳播,路徑所經過的節點都會收到該事件,這個傳播過程可稱為 DOM 事件流。
事件順序有兩種類型:事件捕捉和事件冒泡。
這是 IE 瀏覽器對事件模型的實現,也是最容易理解的。冒泡,顧名思義,事件像個水中的氣泡一樣一直往上冒,直到頂端。
從DOM 樹型結構上理解,就是事件由葉子節點沿祖先結點一直向上傳遞直到根節點;從瀏覽器界面視圖 HTML 元素排列層次上理解就是事件由具有從屬關系的觸發事件的元素一直傳遞到根元素直到文檔對象。
addEventListener(type, callback, [capture]),該方法的第三個參數為 false 的時候設置觸發事件的方式為事件冒泡,該參數默認為 false。
一般情況下,我們都會使用事件冒泡的方式注冊事件。
const outer = document.querySelector('#outer');
const inner = document.querySelector('#inner');
outer.addEventListener('click', function () {
console.log('點擊了 outer');
}, false);
inner.addEventListener('click', function () {
console.log('點擊了 inner');
}, false);
document.body.addEventListener('click', function () {
console.log('點擊了 body');
}, false);
document.addEventListener('click', function () {
console.log('點擊了 document');
}, false);
window.addEventListener('click', function () {
console.log('點擊了 window');
}, false);
執行結果:
使用行內綁定和動態綁定事件的方式默認使用的是事件冒泡。
Netscape 的實現,它與冒泡型剛好相反,由 DOM 樹最頂層元素一直到觸發事件的元素。
addEventListener(type, callback, [capture]),該方法的第三個參數為 true 的時候設置觸發事件的方式為事件捕獲。
const outer = document.querySelector('#outer');
const inner = document.querySelector('#inner');
outer.addEventListener('click', function () {
console.log('點擊了 outer');
}, true);
inner.addEventListener('click', function () {
console.log('點擊了 inner');
}, true);
document.body.addEventListener('click', function () {
console.log('點擊了 body');
}, true);
document.addEventListener('click', function () {
console.log('點擊了 document');
}, true);
window.addEventListener('click', function () {
console.log('點擊了 window');
}, true);
執行結果:
使用行內綁定和動態綁定事件的方式無法使用事件捕獲。
我們已經對上面兩個不同的事件模型進行了解釋和對比。DOM 標準同時支持兩種事件模型,即事件捕獲與事件冒泡,但是,事件捕獲先發生。兩種事件流都會觸發 DOM 中的所有對象,從 document對象開始,也在 document 對象結束(大部分兼容標準的瀏覽器會繼續將事件是捕捉/冒泡延續到window 對象)。
如圖:首先是捕獲傳遞事件,接著是冒泡傳遞,所以,如果一個處理函數既注冊了捕獲型事件的監聽,又注冊冒泡型事件監聽,那么在 DOM 事件模型中它就會被調用兩次。
DOM 標準的事件模型最獨特的性質是,文本節點也會觸發事件(在IE不會)。
事件委托,通俗地來講,就是把一個元素的處理事件的函數委托到另一個元素。
一般來講,會把一個或者一組元素的事件委托到它的父層或者更外層元素上,真正綁定事件的是外層元素,當事件響應到需要綁定的元素上時,會通過事件冒泡機制從而觸發它的外層元素的綁定事件上,然后在外層元素上去執行函數。
舉個例子,比如一個宿舍的同學同時快遞到了,一種方法就是他們都傻傻地一個個去領取,還有一種方法就是把這件事情委托給宿舍長,讓一個人出去拿好所有快遞,然后再根據收件人一一分發給每個宿舍同學。
在這里,取快遞就是一個事件,每個同學指的是需要響應事件的 DOM 元素,而出去統一領取快遞的宿舍長就是代理的元素,所以真正綁定事件的是這個元素,按照收件人分發快遞的過程就是在事件執行中,需要判斷當前響應的事件應該匹配到被代理元素中的哪一個或者哪幾個。
下面我們來做一個練習,為下面的每一個 li 注冊點擊事件,當點擊當前 li 的時候打印 li 中的文本內容。
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
首先我們用傳統的方式來實現,先獲取到頁面上所有的 li,然后遍歷所有的 li,給每一個 li 注冊點擊事件,這里使用 addEventListener() 注冊事件的時候省略了第三個參數,默認為 false,事件冒泡的方式。
這樣做不好的地方有兩點,第一:我們需要為每一個 li 元素創建一個新的事件處理函數,每次創建都需要銷毀時間和內存。第二:當點擊按鈕往 ul 中添加新的 li 元素的時候需要給新創建的 li 注冊點擊事件。
const lis = document.querySelectorAll('#list li');
lis.forEach(function (li) {
li.addEventListener('click', function () {
console.log(this.textContent)
});
});
下面我們使用事件委托的方式優化上面的代碼,把點擊事件注冊給父元素 ul,當點擊 li 的時候通過事件冒泡把點擊事件傳遞給父元素 ul。
const ul = document.querySelector('#list');
ul.addEventListener('click', function () {
console.log('test');
// 此處的 this 是注冊事件的元素 ul
console.log(this);
});
代碼改完之后點擊 li,這段代碼確實可以執行,但是我們的目標是打印 li 之間的內容,而通過打印發現此處的 this 不是我們想要的當前點擊的 li,而是注冊事件的元素 ul。所以這里需要強調一點,在注冊事件的時候,事件源是注冊事件的對象。
那如何獲取當前觸發事件的元素 li 呢?當事件被觸發的時候事件處理函數會接收一個參數,這個參數叫做事件對象,事件對象可以提供觸發事件的時候相關的數據,下一小節詳細介紹,這里我們先用事件對象解決當前的問題,事件對象中有一個 target 屬性,這個屬性就是當前觸發事件的對象。
在 IE 瀏覽器中獲取事件對象的方式不同,IE 中是通過 window.event 獲取事件對象,以前在獲取事件對象的時候還要處理瀏覽器兼容性問題,IE 瀏覽器現在已經被淘汰所以瀏覽器兼容性的處理我們就不再演示。
const ul = document.querySelector('#list');
// 事件參數(對象) e
ul.addEventListener('click', function (e) {
// e.target 觸發事件的元素
console.log(e.target.textContent);
// 注冊事件的元素
console.log(this);
});
到這里這個案例就完成了,我們再來擴展下這個案例,如果想要點擊特定的 li 來觸發事件該如何實現?
<ul id="list">
<li>item 1</li>
<li class="cls">item 2</li>
<li class="cls">item 3</li>
......
<li>item n</li>
</ul>
如上代碼,如果想點擊具有特性類樣式或者特定 id 的元素觸發事件,可以通過判斷當前點擊的元素 e.target 的類樣式或者 id 屬性進行判斷。
if (e.target.className === 'cls') {
// ....
}
但是如果想像 CSS 選擇器一樣更加靈活的匹配的話,上面的判斷不夠靈活,這里可以使用 元素.matches(選擇器) 來匹配特定元素。當元素匹配指定的選擇器返回 true。
const ul = document.querySelector('#list');
ul.addEventListener('click', function (e) {
// matches 方法,當元素匹配指定的選擇器返回 true
if (e.target.matches('.cls')) {
console.log(e.target.textContent);
}
});
利用事件冒泡的特性,將本應該注冊在子元素上的處理事件注冊在父元素上,這樣點擊子元素時發現其本身沒有相應事件就到父元素上尋找作出相應。這樣做的優勢有:1. 減少內存消耗,避免重復創建相同事件處理函數,只需要把多個子元素的事件委托給父元素。2.隨時可以添加子元素,添加的子元素會自動有相應的處理事件。
const tbody = document.querySelector('tbody');
tbody.addEventListener('click', function (e) {
// 注冊事件的元素 tbody
// console.log(this);
// 觸發事件的元素(你點擊的那個元素)
// console.log(e.target)
// 判斷元素是否是指定的元素
// console.log(e.target.matches('.del'))
if (e.target.matches('.del')) {
e.target.parentNode.parentNode.remove();
}
});
每當觸發一個事件,就會產生一個事件對象 event,該對象包含著所有與事件有關的信息。包括導致事件的元素、事件的類型以及其他與特定事件相關的信息。上一小節中我們使用事件對象獲取觸發事件的元素。
例如:鼠標操作產生的 event中會包含鼠標位置的信息;鍵盤操作產生的event中會包含與按下的鍵有關的信息。
所有瀏覽器都支持 event 對象,但支持方式不同,在標準 DOM 中 event 對象必須作為唯一的參數傳給事件處理函數,在 IE 中 event 是 window 對象的一個屬性。
成員 | 描述 | 備注 |
type | 觸發的事件名稱 | |
eventPhase | 事件流在傳播階段的位置 | |
target | 觸發事件的元素 | |
srcElement | target 的別名,老版本的 IE 中使用 | |
clientX / clientY | 基于瀏覽器的可視區域,鼠標坐標值 | 可配合固定定位,基于窗口定位 |
pageX / pageY | 基于整個頁面,頁面滾動有關,鼠標在頁面的坐標值 | 可配合絕對定位,基于頁面定位 |
key | 獲取按鍵輸入 | |
preventDefault() | 取消默認行為 | |
stopPropagation() | 阻止事件冒泡 |
const img = document.querySelector('#img');
document.addEventListener('mousemove', function (e) {
// 鼠標位置 - 圖片大小的一半
img.style.left = e.clientX - 96 / 2 + 'px';
img.style.top = e.clientY - 80 / 2 + 'px';
});
設置樣式,讓 body 的高度等于 1500px(垂直方向出現滾動條),滾動條下拉這時候移動鼠標,圖片的縱向位置跟鼠標脫離。
原因是 clientX 和 clientY 獲取的是鼠標在當前可視區域的位置。如果出現滾動條的話可以通過 pageX 和 pageY 獲取鼠標在當前文檔中的位置。
const img = document.querySelector('#img');
document.addEventListener('mousemove', function (e) {
img.style.left = e.pageX - 96 / 2 + 'px';
img.style.top = e.pageY - 80 / 2 + 'px';
});
這里獲取圖片大小的時候寫的是具體值,將來圖片替換后,還需要改變這里的大小。我們可以使用 getComputedStyle() 獲取圖片的大小。
const img = document.querySelector('#img');
img.addEventListener('load', function () {
const style = window.getComputedStyle(img, null);
const imgWidth = parseInt(style.width);
const imgHeight = parseInt(style.height);
document.addEventListener('mousemove', function (e) {
img.style.left = e.pageX - imgWidth / 2 + 'px';
img.style.top = e.pageY - imgHeight / 2 + 'px';
});
});
注意:這里需要在 img 標簽加載完畢后獲取圖片的大小,否則獲取到的圖片大小是 0,因為 load 事件代表圖片被加載,否則的話代碼從上到下執行到這個位置,圖片還沒有被下載回來,這個時候獲取圖片的大小是 0。
#img {
width: 100px;
position: absolute;
left: 0;
top: 0;
}
.toLeft {
transform: rotateY(180deg);
}
const img = document.querySelector('#img');
let x = 0;
let y = 0;
document.addEventListener('keydown', function (e) {
switch (e.key) {
case 'ArrowLeft':
x -= 10;
img.classList.add('toLeft');
break;
case 'ArrowRight':
x += 10;
img.classList.remove('toLeft');
break;
case 'ArrowUp':
y -= 10;
break;
case 'ArrowDown':
y += 10;
break;
}
img.style.left = x + 'px';
img.style.top = y + 'px';
});
// contextmenu 鼠標右鍵事件
document.addEventListener('contextmenu', function(e) {
// 禁止點擊的默認行為,即顯示上下文菜單
e.preventDefault()
});
// 禁止選中文字事件
document.addEventListener('selectstart', function(e) {
// 禁止選中文字的默認行為,即不能選中文字
e.preventDefault()
})
const loginBg = document.querySelector('#bg');
const loginLink = document.querySelector('#link');
const loginBox = document.querySelector('#login');
const closeBtn = document.querySelector('#closeBtn');
const loginTitle = document.querySelector('#title');
loginLink.addEventListener('click', function () {
loginBox.style.display = 'block';
loginBg.style.display = 'block';
});
closeBtn.addEventListener('click', function () {
loginBox.style.display = 'none';
loginBg.style.display = 'none';
});
// 拖動事件的三個過程:鼠標按下 mousedowm,鼠標移動 mousemove,鼠標松開 mouseup
const style = window.getComputedStyle(loginBox, null);
// 模態框跟著鼠標走的原理
loginTitle.addEventListener('mousedown', function (e) {
const loginLeft = parseInt(style.left);
const loginTop = parseInt(style.top);
// 步驟一:當鼠標按下時,需要立即得到鼠標在盒子中的坐標
var x = e.pageX - loginLeft;
var y = e.pageY - loginTop;
// 為整個頁面添加鼠標移動事件
document.addEventListener('mousemove', move);
function move(e) {
// 步驟二:模態框的left和top等于鼠標在頁面的坐標減去鼠標在盒子內的坐標
// 注意:一定要加上px
login.style.left = e.pageX - x + 'px';
login.style.top = e.pageY - y + 'px';
}
// 步驟三:鼠標松開時取消整個頁面的鼠標移動事件
document.addEventListener('mouseup', function (e) {
document.removeEventListener('mousemove', move);
});
});
在這之前我們已經使用過了單擊事件、鼠標經過和鼠標離開的事件,瀏覽器給我們提供的事件種類非常多,下面我們列出一些常用的事件,使用的方式都是一樣的。
描述 | 事件名稱 |
鼠標單擊 | click |
鼠標雙擊 | dblclick |
鼠標移入 | mouseover |
鼠標移出 | mouseout |
鼠標移動 | mousemove |
獲取焦點 | focus |
失去焦點 | blur |
鍵盤按下 | keydown |
鍵盤彈起 | keyup |
不能識別功能鍵 ctrl、alt 等 | keypress |
文本框的輸入事件 | input |
const search = document.querySelector('#search');
function hanldeFocus(e) {
if (e.key === 's') {
search.focus();
e.preventDefault();
}
}
document.addEventListener('keydown', hanldeFocus);
search.addEventListener('focus', function () {
document.removeEventListener('keydown', hanldeFocus);
});
search.addEventListener('blur', function () {
document.addEventListener('keydown', hanldeFocus);
});
么是事件
我想你很可能聽說過事件驅動, 但是事件驅動到底是什么?為什么說瀏覽器是事件驅動的呢?為什么 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')}
這種方法有兩個缺點
btn.onclick = functionA;btn.onclick = functionB;
這樣只有functionB有效,這可以通過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。 這個對象有一些很有用的屬性和方法,下面舉幾個常用的屬性和方法。
事件傳播
前面講到了事件默認是綁定到冒泡階段的,如果你顯式令useCapture為true,則會綁定到捕獲階段。
事件捕獲很有意思,以至于我會經常出事件的題目加上一點事件傳播的機制,讓候選人進行回答,這很能體現一個人的水平。了解事件的傳播機制,對于一些特定問題有著非常大的作用。
一個Element上綁定的事件觸發了,那么其實會經過三個階段。
從最外層即HTML標簽開始,檢查當前元素有沒有綁定對應捕獲階段事件,如果有則執行,沒有則繼續往里面傳播,這個過程遞歸執行直到觸達觸發這個事件的元素為止。
偽代碼:
上面已經提到了,這里省略了。
從觸發這個事件的元素開始,檢查當前元素有沒有綁定對應冒泡階段事件,如果有則執行,沒有則繼續往里面傳播,這個過程遞歸執行直到觸達HTML為止。
偽代碼:
上述的過程用圖來表示為:
如果你不希望事件繼續冒泡,可以用之前我提到的stopPropagation。
偽代碼:
事件代理
利用上面提到的事件冒泡機制,我們可以選擇做一些有趣的東西。 舉個例子:
我們有一個如下的列表,我們想在點擊對應列表項的時候,輸出是點擊了哪個元素。
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事件模型。 事件循環和事件循環也有千絲萬縷的聯系,如果有時間,我會出一篇關于時間循環的文章。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。