【JS】395-重溫基礎:事件
- 2019 年 11 月 5 日
- 筆記
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章 事件