本文是 重溫基礎 系列文章的第二十篇。
這是第三個基礎系列的第一篇,歡迎持續關注呀!
重溫基礎 系列的【初級】和【中級】的文章,已經統一整理到我的【Cute-JavaScript】的JavaScript基礎系列中。
今日感受:電影有時候看的是緣分。
系列目錄:
- 【複習資料】ES6/ES7/ES8/ES9資料整理(個人整理)
- 【重溫基礎】1-14篇
- 【重溫基礎】15.JS物件介紹
- 【重溫基礎】16.JSON物件介紹
- 【重溫基礎】17.WebAPI介紹
- 【重溫基礎】18.相等性判斷
- 【重溫基礎】19.閉包
本章節複習的是JS中的事件,事件冒泡捕獲代理模擬等等。
前置知識:
JavaScript與HTML的互動式通過事件來實現的,是文件或瀏覽器視窗中發生的一些特定的互動瞬間。
1.事件流
事件流描述的是從頁面中接收事件的順序,通常有這樣兩種完全相反的事件流概念:事件冒泡流(IE團隊提出)和事件捕獲流(網景團隊提出)。
1.1 事件冒泡
冒泡事件(Event Bubbling):事件開始時由最具體的元素接收(文件中巢狀層次最深的那個節點),然後逐層向上傳播到較為不具體的節點(文件),看下示例程式碼:
<!DOCTYPE html>
<html>
<head>
<title>leo 事件冒泡</title>
</head>
<body>
<div id="leo">點選</div>
</body>
</html>
複製程式碼
點選頁面中<div>
元素,這個click
事件就會按照下面順序傳播:
<div>
<body>
<html>
document
由此可見,元素繫結的事件會通過DOM樹向上傳播,每層節點都會發生,直到document物件
,如圖展示了冒泡過程:
1.2 事件捕獲
事件捕獲(Event Capturing):讓不太具體的節點更早接收事件,而最具體的節點最後接收事件,即在事件到達預定目標之前捕獲到,看下示例程式碼(HTML程式碼和前面一樣),事件捕獲的過程是這樣的:
document
<html>
<body>
<div>
看得出,document物件
最新接收事件,然後沿DOM樹依次向下,直到最後的實際目標<div>
元素,如圖展示了捕獲過程:
注意:由於老版本的瀏覽器不支援,因此很少人使用事件捕獲,不過如果特殊需求還是可以使用事件捕獲,建議還是使用事件冒泡。
1.3 DOM事件流
“DOM2級事件”規定的事件流包含三個階段:事件捕獲階段,處於目標階段和事件冒泡階段。
事件捕獲為截獲事件提供機會,然後實際的目標接收到事件,最後事件冒泡,對事件作出響應。按照前面的HTML程式碼,整個流程是這樣的:
在DOM事件流中,實際目標(<div>
元素)在捕獲階段不接收事件,即在捕獲階段,事件從document物件
到<html>
再到<body>
後就停止,進入“處於目標”階段,事件在<div>
元素上發生,然後才進入冒泡階段,將事件傳回給文件。
注意:目前主流瀏覽器都支援DOM事件流,只有IE8和之前版本不支援。
2.事件處理
事件處理,即響應某個事件。我們把事件處理的函式,稱為“事件處理程式”。
事件處理程式的名稱一般都以on
開頭,如click
事件的事件處理程式就是onclick
,load
事件的事件處理程式就是onload
。
我們將事件處理程式,分為這麼幾類:
- HTML事件處理程式
- DOM0級事件處理程式
- DOM2級事件處理程式
- IE事件處理程式
- 跨瀏覽器事件處理程式
2.1 HTML事件處理程式
某個元素支援的事件,都可以用一個與相應事件處理程式同名的HTML特性來指定,這個特性的值應該是能夠執行的JavaScript程式碼。比如:
<input type="button" value="點選" onclick="alert('hello leo');">
複製程式碼
也可以把需要執行的具體事件單獨定義出來,可以放置與單獨.js
檔案,也可以在文件內用<script>
標籤引入:
function fun(){
alert('hello leo');
}
複製程式碼
<input type="button" value="點選" onclick="fun()">
複製程式碼
我們通過這樣指定事件處理程式,可以有一個區域性變數event
來獲取事件物件本身,在這個函式內部,this
值等於這個變數event
。
<input type="button" value="點選" onclick="fun(event)">
複製程式碼
另外,HTML中指定事件處理程式,會有2個缺點:
- 存在時間差
可能出現這樣的情況:HTML元素觸發事件,但是事件處理程式還未定義(函式的定義在HTML最底下定義),就會出現報錯,這與HTML程式碼載入順序有關。 - 作用域鏈的異常 由於不同瀏覽器JavaScript引擎遵循的識別符號解析規則存在差異,導致訪問非限定物件成員時出錯,表現為事件處理程式的作用域鏈在不同瀏覽器結果不同。
- HTML和JavaScript程式碼緊密耦合 這常常就是很多開發人員放棄HTML事件處理程式的原因。
2.2 DOM0級事件處理程式
通過賦值形式,將一個函式賦值給一個事件處理程式屬性。每個元素(包含window
和document
)都有自己的事件處理屬性,這些屬性通常全部小寫,如onclick
,將這種屬性的值設定成一個函式,就可以指定事件處理程式:
var leo = document.getElementById('leo');
leo.onclick = function(){
alert('hello leo!');
}
複製程式碼
使用DOM0級方法指定事件處理程式,被認為是元素的方法。此時的事件處理程式是在元素的作用域執行,那麼,this就引用當前元素,可以訪問元素的任何屬性和方法:
var leo = document.getElementById('leo');
leo.onclick = function(){
alert(this.id); // "leo"
}
複製程式碼
我們也可以通過設定事件處理程式屬性來刪除DOM0級的事件處理程式。
leo.onclick = null;
複製程式碼
2.3 DOM2級事件處理程式
有2個方法:
- 新增事件處理程式:
addEventListener()
- 刪除事件處理程式:
removeEventListener()
所有的DOM節點都包含這兩個方法,並且它們都接收三個引數:
- 處理的事件名稱
- 事件處理程式的函式
- 布林值(true:事件捕獲階段呼叫,false:事件冒泡階段呼叫)
var leo = document.getElementById('leo');
leo.addEventListener('click',function(){
alert(this.id); // "leo"
},false);
複製程式碼
與DOM0級方法一樣,這裡的事件處理程式也會是在元素的作用域中執行,因此this也是指向元素,可以訪問元素的任何屬性和方法。
使用DOM2級方法,可以新增多個事件處理程式,並按照新增順序觸發:
var leo = document.getElementById('leo');
leo.addEventListener('click',function(){
alert(this.id); // "leo"
},false);
leo.addEventListener('click',function(){
alert('hello leo!'); // "hello leo!"
},false);
複製程式碼
注意:通過addEventListener()
新增的事件只能通過removeEventListener()
移除,並且兩者傳入的引數一致,這就意味著通過addEventListener()
新增的匿名函式不能被移除,看下面程式碼:
var leo = document.getElementById('leo');
leo.addEventListener('click',function(){
alert(this.id); // "leo"
},false);
// 沒有效果
leo.removeEventListener('click',function(){
// do some thing
},false);
複製程式碼
沒有效果是因為這兩個方法傳入的函式,是完全不同的,為了達到刪除事件處理程式的效果,我們可以將處理函式單獨定義出來:
var leo = document.getElementById('leo');
var fun = function(){
alert(this.id);
}
leo.addEventListener('click',fun,false);
// 有效果
leo.removeEventListener('click',fun,false);
複製程式碼
2.4 IE事件處理程式
IE實現兩種方法: attachEvent()
和detachEvent()
。這兩個方法接收相同的兩個引數:事件處理程式名稱和事件處理程式函式。
由於IE8和更早版本只支援事件冒泡,所以通過attachEvent()
新增的事件處理程式會被新增到冒泡階段。
var leo = document.getElementById('leo');
leo.attachEvent('onclick',function(){
alert('hello leo'); // "leo"
},false);
// attachEvent也支援新增多個事件處理程式
leo.attachEvent('onclick',function(){
alert('hello world'); // "leo"
},false);
複製程式碼
注意:這裡的第一個引數是onclick
而不是DOM的addEventListener()
的click
。
IE的attachEvent()和DOM0級方法區別:
兩者事件處理程式的作用域不同。
- DOM0級方法,作用域在所屬元素的作用域。
- attachEvent(),作用域在全域性作用域,即
this
指向window
。
和DOM0級方法一樣,detachEvent()
只能移除使用attachEvent()
新增的方法,為了避免無法移除,也是需要將處理的函式單獨定義出來:
var leo = document.getElementById('leo');
var fun = function(){
alert(this.id);
}
leo.attachEvent('onclick',fun,false);
// 有效果
leo.detachEvent('onclick',fun,false);
複製程式碼
2.6 跨瀏覽器事件處理程式
在做跨瀏覽器事件處理程式,我們可以有兩種方式:
- 使用能夠隔離瀏覽器差異的JavaScript庫
- 單獨手寫一個處理方法
我們單獨受寫一個處理方法也不難,只需關注好事件冒泡階段,我們可以建立一個方法,區分使用DOM0級方法、DOM2級方法或IE方法即可,預設採用DOM0級方法。
var my_event = {
addMyEvent:function(element, type, handler){
if(element.addEventListener){
element.addEventListener(type, handler, false);
}else if(element.attachEvent){
element.attachEvent('on' + type, handler);
}else{
element['on' + type] = handler;
}
};
removeMyEvent:function(element, type, handler){
if(element.removeEventListener){
element.removeEventListener(type, handler, false);
}else if(element.detachEvent){
element.detachEvent('on' + type, handler);
}else{
element['on' + type] = null;
}
}
}
複製程式碼
3.事件物件
當觸發一個DOM上的事件時,都會產生一個事件物件event
,並作為引數傳入事件處理程式,這個物件包含所有與事件相關的資訊。包括導致事件的元素、事件型別等其他資訊。
3.1 DOM中的事件物件
無論指定事件處理程式時使用什麼方法(DOM0級方法或DOM2級方法),都會傳入event
物件:
var leo = document.getElementById('leo');
leo.onclick = function(event){
alert(event.type); // 'click'
}
leo.addEventListener('click',function(event){
alert(event.type); // 'click'
},false);
複製程式碼
event
物件包含與建立它的特定事件相關的屬性和方法,常常有如下成員:
我們可以使用event
中的物件和方法:
- 阻止事件的預設行為
var leo = document.getElementById('leo');
leo.onclick = function(event){
// 只有當 event 中的 cancelable 屬性為true的事件
event.preventDefault();
}
複製程式碼
- 立即停止事件在DOM的傳播
通過呼叫event.stopPropagation()
方法避免彈框出現兩次。
var leo = document.getElementById('leo');
leo.onclick = function(event){
alert('leo');
event.stopPropagation();
}
document.body.onclick = function(event){
alert('hello leo');
}
複製程式碼
3.2 IE中的事件物件
訪問IE中的事件物件event
,方法有多種,取決於事件處理程式的方法:
- DOM0級方法,使用
window.event
var leo = document.getElementById('leo');
leo.onclick = function(){
var event = window.event;
alert(event.type); // 'click'
}
複製程式碼
- IE的
attachEvent
方法,event
作為引數傳入(也可以使用window.event
)
var leo = document.getElementById('leo');
leo.attachEvent('onclick',function(event){
alert(event.type); // 'click'
},false);
複製程式碼
3.3 跨瀏覽器的事件物件
雖然DOM和IE中event
物件不同,但我們也可以和前面的 跨瀏覽器事件處理程式 處理一樣,通過他們之間的區別,實現對映:
var my_event = {
myAddFun : function(element, type, handler){
// do some thing
},
// 返回對event的引用
getMyEvent : function(event){
return event?event:window.event;
},
// 返回事件的目標
getMyTarget : function(event){
return event.target || event.srcElement;
},
// 取消事件的預設行為
preventDefault : function(event){
if(event.preventDefault){
event.preventDefault();
}else {
event.returnValue = false;
}
},
myRemoveFun : function(element, type, handler){
// do some thing
},
// 阻止事件流
stopPropagetion : function(event){
if(event.stopPropagetion){
event.stopPropagetion();
}else {
event.cancelBubble = true;
}
}
}
var leo = document.getElementById('leo');
leo.onclick = function(event){
alert('leo');
event = my_event(event);
my_event.stopPropagation(event);
}
leo.onclick = function(event){
alert('hello world');
}
複製程式碼
4.事件型別
Web瀏覽器中的事件型別有很多,DOM3級事件規定有以下幾類事件型別:
- UI事件:當使用者與頁面上元素互動時觸發;
- 焦點事件:當元素失去或獲取焦點時觸發;
- 滑鼠事件:當使用者通過滑鼠在頁面操作時觸發;
- 滾輪事件:當使用滑鼠滾輪(或類似裝置)時觸發;
- 文字事件:當在文件中輸入文字時觸發;
- 鍵盤事件:當使用者通過鍵盤操作時觸發;
- 合成事件:當為IME輸入字元時觸發;
- 變動事件:當底層DOM結構變動時觸發;
具體每個方法的詳細介紹,可以檢視**W3school HTML DOM Event 物件**
或者檢視《JavaScript高階程式設計(第三版)》的第362頁開始。
我後面會單獨整理一篇,介紹這些事件的文章。
5.事件委託
簡單理解就是講一個響應事件委託到另一個元素。
事件委託利用事件冒泡,指定一個事件處理程式來管理某一型別的所有事件,比如我們通過一個函式來代替其他三個函式:
<ul id="btn">
<li id="btn1">按鈕1</li>
<li id="btn2">按鈕2</li>
<li id="btn3">按鈕3</li>
</ul>
複製程式碼
var btn1 = document.getElementById('btn1');
var btn2 = document.getElementById('btn2');
var btn3 = document.getElementById('btn3');
// my_event 在前面定義了
my_event.myAddFun(btn1, 'click', function(event){
alert('btn1點選');
});
my_event.myAddFun(btn2, 'click', function(event){
alert('btn2點選');
});
my_event.myAddFun(btn3, 'click', function(event){
alert('btn3點選');
});
複製程式碼
下面我們在DOM樹層級更高的元素上新增一個事件處理函式,來做事件委託,我們這麼重寫這段程式碼:
var btn = document.getElementById('btn');
my_event.myAddFun(btn, 'click', function(event){
event = my_event.getMyEvent(event);
var target = my_event.getMyTarget(event);
switch(target.id){
case "btn1":
alert('btn1點選');
break;
case "btn2":
alert('btn2點選');
break;
case "btn3":
alert('btn3點選');
break;
}
});
複製程式碼
最適合採用事件委託技術的事件包括:click
/mousedown
/mouseup
/keyup
/keydown
/keypress
,雖然mouseover
和mouseout
事件也有冒泡,但因為不好處理它們並且經常需要重新計算元素位置。
可以看出,事件委託有以下優點:
- 減少記憶體消耗
- 動態繫結事件
6.事件模擬
JavaScript的事件模擬主要用來在任意時刻觸發特定事件。
6.1 DOM中的事件模擬
在document
物件上使用createEvent()
方法建立一個event
物件。
createEvent()
接收一個引數,即要建立的事件型別的字串。
DOM2級中,所有這些字串都使用英文複數形式,DOM3級中都變成單數,也可以是下面中的字串:
- UIEvents : 一般化的UI事件(滑鼠和鍵盤事件都繼承自UI事件)(DOM3級中
UIEvent
) - MouseEvents : 一般化的滑鼠事件(DOM3級中
MouseEvent
) - MutationEvents : 一般化的DOM滾動事件(DOM3級中
MutationEvent
) - HTMLEvents : 一般化的HTML事件(DOM3級中
HTMLEvent
)
建立event
之後,我們需要使用dispatchEvent()
方法去觸發這個事件,需要傳入一個引數,引數是需要觸發事件的event物件。
所有支援事件的DOM節點都支援這個方法。事件觸發之後,事件就能照樣冒泡並引發響應事件處理程式的執行。
6.1.1 模擬滑鼠事件
使用createEvent()
方法傳入MouseEvents
建立一個滑鼠事件,返回的物件有一個initMouseEvent()
方法,用於指定與該滑鼠事件相關的資訊,有15個引數:
- type : 字串,表示觸發的事件型別,如
click
- bubble : 布林值,表示是否冒泡,為了精確模擬滑鼠事件,通常設定為true
- cancelable :布林值,表示是否可以取消,為了精確模擬滑鼠事件,通常設定為true
- view : 與事件關聯的檢視,基本都設定為
document.defaultView
- detail : 整數,與事件有關的詳細資訊,基本設定為0
- screenX : 整數,事件相對螢幕的X座標
- screenY : 整數,事件相對螢幕的Y座標
- clientX : 整數,事件相對視口的X座標
- clientY : 整數,事件相對視口的Y座標
- ctrlKey : 布林值,表示是否按下Ctrl鍵,預設false
- altKey : 布林值,表示是否按下Alt鍵,預設false
- shiftKey : 布林值,表示是否按下Shift鍵,預設false
- metaKey : 布林值,表示是否按下Meta鍵,預設false
- button : 整數,表示按下哪個滑鼠鍵,預設0
- relatedTarget : 物件,表示與事件相關的物件,只在
mouseover
和mouseout
時使用
案例:
var btn = document.getElementById('btn');
var myEvent = document.createEvent('MouseEvents');
myEvent.initMouseEvent(
'click', true, true, document.defaultView,
0, 0, 0, 0, 0,
false, false, false, false, 0, null
)
btn.dispatchEvent(myEvent);
複製程式碼
6.1.2 模擬鍵盤事件
DOM3級規定,使用createEvent()
方法傳入KeyboardEvent
建立一個鍵盤事件,返回的物件有一個initKeyEvent()
方法,有8個引數:
- type : 字串,表示觸發的事件型別,如
keydown
- bubble : 布林值,表示是否冒泡,為了精確模擬鍵盤事件,通常設定為true
- cancelable :布林值,表示是否可以取消,為了精確模擬鍵盤事件,通常設定為true
- view : 與事件關聯的檢視,基本都設定為
document.defaultView
- key : 整數,表示按下的鍵的鍵碼
- localtion : 整數,表示按下哪裡的鍵,預設0表示主鍵盤,1表示左,2表示右,3表示數字鍵盤,4表示移動裝置(即虛擬鍵盤),5表示手柄
- modifiers : 字串,空格分隔的修改件列表,如"shift"
- repeat : 整數,在一行中按了多少次這個鍵
由於DOM3級不提倡使用keypress
事件,因此只能用這個方式來模擬keyup
/keydown
事件。
模擬按住Shift和A鍵的案例:
var btn = document.getElementById('btn');
var myEvent;
// 以DOM3級方式建立
if(document.implementation.hasFeature('KeyboardEvents', '3.0')){
myEvent = document.createEvent('KeyboardEvent');
myEvent.initKeyboardEvent(
'keydown', true, true, document.defaultView,
'a', 0, 'Shift', 0
);
}
btn.dispatchEvent(myEvent);
複製程式碼
6.1.3 模擬其他事件
如模擬變動事件和HTML事件。
- 模擬變動事件
通過createEvent()
傳入MutationEvents
引數建立,返回一個initMutationEvent()
方法,這個方法接收引數包括:type
/bubbles
/cancelable
/relatedNode
/preValue
/newValue
/attrName
/attrChange
,下面模擬一個案例:
var btn = document.getElementById('btn');
var myEvent = document.createEvent('MutationEvents');
myEvent.initMutationEvent(
'DOMNodeInserted', true, false, someNode, '', '', '', 0
);
btn.dispatchEvent(myEvent);
複製程式碼
- 模擬HTML事件
通過createEvent()
傳入HTMLEvents
引數建立,返回一個initEvent()
方法,下面模擬一個案例:
var btn = document.getElementById('btn');
var myEvent = document.createEvent('HTMLEvents');
myEvent.initEvent('focus', true, false);
btn.dispatchEvent(myEvent);
複製程式碼
6.1.4 自定義DOM事件
通過createEvent()
傳入CustomEvent
引數建立,返回一個initCustomEvent()
方法,有4個引數:
- type : 字串,表示觸發的事件型別,如
keydown
- bubble : 布林值,表示是否冒泡,為了精確模擬鍵盤事件,通常設定為true
- cancelable :布林值,表示是否可以取消,為了精確模擬鍵盤事件,通常設定為true
- detail : 物件,任意值,儲存在
event
物件的detail
屬性中
案例:
var btn = document.getElementById('btn');
var myEvent;
// my_event在前面定義 2.6 跨瀏覽器事件處理程式
my_event.addMyEvent(btn, 'myevent', function(event){
alert('btn detail ', event.detail);
});
my_event.addMyEvent(document, 'myevent', function(event){
alert('document detail ', event.detail);
});
// 以DOM3級方式穿件
if(document.implementation.hasFeature('CustomEvents', '3.0')){
myEvent = document.createEvent('CustomEvent');
myEvent.initCustomEvent(
'myevent', true, false, 'hello leo',
);
btn.dispatchEvent(myEvent);
}
複製程式碼
6.2 IE中的事件模擬
IE8及之前的版本模擬事件和DOM中模擬思路相似:想建立event物件
再指定資訊,最後觸發。
區別在於,IE中使用document.createEventObject()
方法建立event物件
,並且不接收引數,返回一個通用event物件
,我們要做的就是給這個event物件
新增資訊,最後在目標上呼叫fireEvent()
方法,並接受兩個引數(事件處理程式的名稱和event物件
)。
在呼叫fireEvent()
方法會自動新增srcElement
和type
屬性,我們需要手動新增其他屬性,下面模擬一個click事件:
var btn = document.getElementById('btn');
var myEvent = document.createEventObject();
myEvent.screenX = 100;
myEvent.screenY = 0;
myEvent.clientX = 100;
myEvent.clientY = 0;
myEvent.ctrlKey = false;
myEvent.altKey = false;
myEvent.shiftKey = false;
myEvent.button = 0;
btn.fireEvent('onclick', event);
複製程式碼
參考文章
- 《JavaScript高階程式設計》第13章 事件
本部分內容到這結束
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推薦 | github.com/pingan8787/… |
JS小冊 | js.pingan8787.com |
歡迎關注我的微信公眾號【前端自習課】