前言:之前的上傳圖片用到了event.target,但是后來(lái)仔細(xì)思考了一下,自己對(duì)event.target,this,event.currentTarget的區(qū)別完全不清楚,然后發(fā)現(xiàn)越學(xué)越多,為了搞清楚這個(gè)問(wèn)題,把DOM事件,事件流,事件捕獲,事件冒泡等全部學(xué)了一遍,收獲頗豐,特別總結(jié)記錄下來(lái)。
事件
事件是文檔或者瀏覽器窗口中發(fā)生的,特定的交互瞬間。
事件是用戶或者瀏覽器自己執(zhí)行的動(dòng)作,比如click(用戶左鍵單擊鼠標(biāo)),load(頁(yè)面加載完成),JavaScript可以通過(guò)綁定觸發(fā)事件來(lái)和DOM進(jìn)行交互(當(dāng)然也可以直接操作DOM),由于在底層JavaScript和DOM是獨(dú)立的,所以多次綁定進(jìn)行綁定操作會(huì)影響頁(yè)面的性能(后邊會(huì)提到解決方案:事件委托)。
事件流
事件流描述的是從頁(yè)面中接收事件的順序。
以下開(kāi)始以例子詳細(xì)分析事件流。
HTML:
<div>
<ul>
<li></li>
</ul>
</div>
CSS:
*{
margin: 0;
padding: 0;
}
div{
width: 300px;
height: 300px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -150px;
margin-left: -150px;
background-color: #2578b5;
border-radius: 50%;
}
ul{
width: 200px;
height: 200px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -100px;
margin-left: -100px;
background-color: #f2de76;
border-radius: 50%;
}
li{
list-style: none;
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -50px;
background-color: #afc8ba;
border-radius: 50%;
}
效果如下:
如圖所示,思考一下:如果我們點(diǎn)擊內(nèi)層圓,就僅僅點(diǎn)擊了內(nèi)層圓嗎?
很明顯,我們不止點(diǎn)擊了內(nèi)層圓,而且點(diǎn)擊了中層圓,外層圓,html,body和document。這就引出了事件流的詳細(xì)定義:
事件發(fā)生時(shí)會(huì)在元素節(jié)點(diǎn)與根節(jié)點(diǎn)之間按照特定的順序傳播,路徑所經(jīng)過(guò)的所有節(jié)點(diǎn)都會(huì)收到該事件,這個(gè)傳播過(guò)程即DOM事件流。
那么問(wèn)題又來(lái)了,如果我們給這些元素都綁定事件,那么這些事件的執(zhí)行順序是什么?
兩種事件流模型
- 冒泡型事件流:事件的傳播是從最特定的事件目標(biāo)到最不特定的事件目標(biāo)。即從DOM樹(shù)的葉子到根。
- 捕獲型事件流:事件的傳播是從最不特定的事件目標(biāo)到最特定的事件目標(biāo)。即從DOM樹(shù)的根到葉子。
例子中的事件傳播順序:
- 在冒泡型事件流中,是li > ul > div > body > html > document。
- 在捕獲型事件流中,是document > html > body> div > ul > li。
實(shí)際中的事件流并沒(méi)有完全按照標(biāo)準(zhǔn)事件流實(shí)現(xiàn), 所有現(xiàn)代瀏覽器都支持事件冒泡,但在具體實(shí)現(xiàn)中略有差別:
- IE5.5及更早版本中事件冒泡會(huì)跳過(guò)html元素(從body直接跳到document)。
- IE9、Firefox、Chrome、和Safari則將事件一直冒泡到window對(duì)象。
- IE9、Firefox、Chrome、Opera、和Safari都支持事件捕獲。盡管DOM標(biāo)準(zhǔn)要求事件應(yīng)該從document對(duì)象開(kāi)始傳播,但這些瀏覽器都是從window對(duì)象開(kāi)始捕獲事件的。
- 由于老版本瀏覽器不支持,很少有人使用事件捕獲。建議使用事件冒泡。
實(shí)際使用中的DOM事件流
之所以會(huì)存在兩種事件流,是由于微軟和網(wǎng)景之間的競(jìng)爭(zhēng)造成的,幸運(yùn)的是,W3C決定組合使用這兩種方法,并且大多數(shù)新瀏覽器都遵循這兩種事件流方式,所以現(xiàn)在完整的DOM事件流分為3個(gè)階段:
- 事件捕獲階段
- 目標(biāo)階段(事件在目標(biāo)上發(fā)生并處理,但事件處理被認(rèn)為發(fā)生在冒泡階段)
- 事件冒泡階段
盡管理論上(DOM2中規(guī)定)事件捕獲階段不會(huì)涉及事件目標(biāo),但是IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都會(huì)在捕獲階段觸發(fā)事件對(duì)象上的事件。結(jié)果,就是有兩次機(jī)會(huì)在目標(biāo)對(duì)象上面操作事件。
并非所有的事件都會(huì)經(jīng)過(guò)冒泡階段 。所有的事件都要經(jīng)過(guò)捕獲階段和處于目標(biāo)階段,但是有些事件會(huì)跳過(guò)冒泡階段,如獲得輸入焦點(diǎn)的focus事件和失去輸入焦點(diǎn)的blur事件。
在網(wǎng)上找到了一張?jiān)蛨D,但是出處沒(méi)找到,感謝無(wú)名氏同學(xué)。
默認(rèn)情況下,事件使用冒泡事件流,不使用捕獲事件流。然而,在現(xiàn)代瀏覽器中(IE9+,Chrome,F(xiàn)irefox),你可以顯式的指定使用捕獲事件流,方法是在注冊(cè)事件時(shí)傳入useCapture參數(shù),將這個(gè)參數(shù)設(shè)為true。
下面我們來(lái)測(cè)試綁定事件的不同方式會(huì)有什么區(qū)別。
DOM事件綁定
DOM0
通過(guò)javascript制定事件處理程序的傳統(tǒng)方式,就是將一個(gè)函數(shù)賦值給一個(gè)事件處理屬性。
優(yōu)點(diǎn):當(dāng)前所有瀏覽器均支持,簡(jiǎn)單且具有跨瀏覽器的優(yōu)勢(shì)。
缺點(diǎn):一個(gè)事件處理程序只能對(duì)應(yīng)一個(gè)處理函數(shù)。
下面我們用之前的同心圓例子對(duì)DOM0事件綁定進(jìn)行測(cè)試:
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.onclick = function(){
console.log("div");
}
ul.onclick = function(){
console.log("ul");
}
li.onclick = function(){
console.log("li");
}
此時(shí)我們點(diǎn)擊內(nèi)層圓,控制臺(tái)輸出如下:
可以看到,輸出的順序是從內(nèi)到外,所以可以得出結(jié)論,DOM0綁定事件是在冒泡階段執(zhí)行的。
刪除DOM0事件處理程序,只要將對(duì)應(yīng)事件屬性置為null即可。如將div上綁定事件刪除:div.onclick = null
。
另外DOM0還有個(gè)很神奇的特性,如果我們像這樣綁定div的點(diǎn)擊事件:
div.onclick = function(){
console.log(this);
}
那么輸出的this是div,也就是執(zhí)行該方法的DOM對(duì)象,但是如果定義一個(gè)函數(shù),然后在HTML中綁定,那么輸出的this是window,這是由于在HTML中綁定相當(dāng)于動(dòng)態(tài)綁定,所以定義函數(shù)的this永遠(yuǎn)都是window,不會(huì)隨著上下文改變。
DOM1
DOM1級(jí)主要定義的是HTML和XML文檔的底層結(jié)構(gòu)。DOM2和DOM3級(jí)別則在這個(gè)結(jié)構(gòu)的基礎(chǔ)上引入了更多的交互能力,也支持了更高級(jí)的XML特性。為此DOM2和DOM3級(jí)分為許多模塊(模塊之間具有某種關(guān)聯(lián)),分別描述了DOM的某個(gè)非常具體的子集。
DOM2
DOM2級(jí)事件綁定方式指定了,添加事件綁定程序和刪除事件綁定程序的方法。
addEventListener(ev,fn,useCapture);
removeEventListener(ev,fn,useCapture);
優(yōu)點(diǎn):可以在同一DOM對(duì)象綁定多個(gè)相同事件,可以控制是在捕獲階段觸發(fā)還是在冒泡階段觸發(fā)。
缺點(diǎn):IE8及以下不支持這種寫(xiě)法,而是使用獨(dú)有的綁定多事件方法,所以需要自己寫(xiě)兼容模式(之后會(huì)提到)。
還是之前同心圓的例子來(lái)測(cè)試。
第三個(gè)參數(shù)為空(即默認(rèn)的false)的情況:
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.addEventListener("click",function(){
console.log("div");
});
ul.addEventListener("click",function(){
console.log("ul");
});
li.addEventListener("click",function(){
console.log("li");
});
點(diǎn)擊內(nèi)層圓,結(jié)果如下:
第三個(gè)參數(shù)為true的情況:
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.addEventListener("click",function(){
console.log("div");
},true);
ul.addEventListener("click",function(){
console.log("ul");
},true);
li.addEventListener("click",function(){
console.log("li");
},true);
點(diǎn)擊內(nèi)層圓,結(jié)果如下:
可以看到,第三個(gè)參數(shù)為空(false)則在冒泡階段觸發(fā),第三個(gè)參數(shù)為true則在捕獲階段觸發(fā)。
如果我們給同一個(gè)DOM對(duì)象同時(shí)在捕獲和冒泡階段綁定同一個(gè)類型的事件,還是同心圓的例子,給div綁定DOM0的click事件,DOM2的click事件(捕獲階段觸發(fā),冒泡階段觸發(fā)),如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.onclick = function(){
console.log("dom0 冒泡");
}
div.addEventListener("click",function(){
console.log("dom2 捕獲");
},true);
div.addEventListener("click",function(){
console.log("dom2 冒泡");
},false);
點(diǎn)擊內(nèi)層圓,輸出結(jié)果如下:
可以看到,先觸發(fā)捕獲事件,然后觸發(fā)冒泡事件,并且DOM0和DOM2的事件互不影響,誰(shuí)先綁定就先執(zhí)行。
點(diǎn)擊中層圓,輸出結(jié)果如下:
結(jié)果和點(diǎn)擊內(nèi)層圓一樣,沒(méi)毛病。
點(diǎn)擊外層圓,輸出結(jié)果如下:
很神奇,外層圓的事件順序不是按照事件流了,這是為什么呢。其實(shí)原因在于一直沒(méi)提的目標(biāo)(target)階段。我們給外層圓綁定點(diǎn)擊事件,點(diǎn)擊內(nèi)層圓,實(shí)際上的target是內(nèi)層圓,中層圓同理。但是如果我們點(diǎn)擊外層圓,外層圓自己就是target,這時(shí)就不分事件捕獲和事件冒泡了,誰(shuí)先綁定誰(shuí)先執(zhí)行。
addEventListener和removeEventListener有幾點(diǎn)需要注意:
- 如果使用匿名函數(shù)的方式執(zhí)行addEventListener,則無(wú)法使用removeEventListener刪除該綁定事件。
- 如果使用具名函數(shù)的方式addEventListener,則該函數(shù)內(nèi)部的this指向執(zhí)行該方法的DOM對(duì)象,另外匿名函數(shù)也是指向執(zhí)行該方法的DOM對(duì)象。
- 如果addEventListener和removeEventListener第三個(gè)參數(shù)不同,則不認(rèn)為是同一個(gè)事件,即removeEventListener不可以刪除addEventListener綁定的事件。
IE8及以下不支持標(biāo)準(zhǔn)的addEventListener和removeEventListener,而是使用了私有方法attachEvent和detachEvent。值得注意的是,這種方法的第一個(gè)參數(shù)需要加on。
attachEvent(ev,fn);
detachEvent(ev,fn);
使用之前同心圓的例子,在IE8測(cè)試,代碼如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.attachEvent("onclick",function(){
console.log("div");
});
ul.attachEvent("onclick",function(){
console.log("ul");
});
li.attachEvent("onclick",function(){
console.log("li");
});
結(jié)果如下:
吐槽一下,IE8及以下不支持
border-radius
屬性,所以已經(jīng)不能算是同心圓了。
由輸出結(jié)果可以看出,attachEvent會(huì)在冒泡階段觸發(fā)。
attachEvent和detachEvent也有幾點(diǎn)需要注意:
- 使用匿名函數(shù)作為第二個(gè)參數(shù)的attachEvent是無(wú)法被刪除的。
- 無(wú)論是使用具名函數(shù)還是匿名函數(shù)作為第二個(gè)參數(shù),函數(shù)內(nèi)部的this都會(huì)指向window。
其實(shí)我本身很反感兼容低版本IE的事情,也感謝我司對(duì)前端兼容性的要求是IE9+,但是畢竟不是每個(gè)公司都像我司一樣,甚至有時(shí)候都不考慮IE9了,所以還是寫(xiě)一下兼容寫(xiě)法。還是之前同心圓的例子,代碼如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
function addEvent(element,type,callback){
if(element.addEventListener){
element.addEventListener(type,callback,false);
}else if(element.attachEvent){
element.attachEvent('on' + type,callback);
}
}
addEvent(div,'click',function(){
console.log("綁定點(diǎn)擊事件1");
})
addEvent(div,'click',function(){
console.log("綁定點(diǎn)擊事件2");
})
在Chrome上輸出結(jié)果如下:
在IE8上輸出結(jié)果如下:
還是有些區(qū)別的,輸出順序這個(gè)到現(xiàn)在我還是沒(méi)想明白,很奇怪,所以如果對(duì)順序有要求,就還是放棄IE8及以下吧。
另外就是要注意,使用addEvent這個(gè)函數(shù)的時(shí)候,匿名函數(shù)的this在不同的瀏覽器是有區(qū)別的,總之IE依然是個(gè)大坑。
DOM3
DOM瀏覽器中可能發(fā)生的事件有很多種,不同事件類型具有不同的信息,DOM3級(jí)事件規(guī)定了一下幾種事件:
- UI事件,當(dāng)用戶與頁(yè)面上的元素交互時(shí)觸發(fā)。
- 焦點(diǎn)事件,當(dāng)元素獲得或者失去焦點(diǎn)時(shí)觸發(fā)。
- 鼠標(biāo)事件,當(dāng)用戶通過(guò)鼠標(biāo)在頁(yè)面上執(zhí)行操作時(shí)觸發(fā)。
- 滾輪事件,當(dāng)使用鼠標(biāo)滾輪(或類似設(shè)備)時(shí)觸發(fā)。
- 文本事件,當(dāng)在文檔中,輸入文本時(shí)觸發(fā)。
- 鍵盤(pán)事件,當(dāng)用戶通過(guò)鍵盤(pán)在頁(yè)面上執(zhí)行操作時(shí)觸發(fā)。
- 合成事件,當(dāng)為IME(Input Method Editor,輸入法編輯器)輸入字符時(shí)觸發(fā)。
- 變動(dòng)事件,當(dāng)?shù)讓覦om結(jié)構(gòu)發(fā)生變化時(shí)觸發(fā)。
DOM3級(jí)事件模塊在DOM2級(jí)事件的基礎(chǔ)上重新定義了這些事件,也添加了一些新事件。包括IE9在內(nèi)的主流瀏覽器都支持DOM2級(jí)事件,IE9也支持DOM3級(jí)事件。
另外DOM3級(jí)還定義了自定義事件,自定義事件不是由DOM原生觸發(fā)的,它的目的是讓開(kāi)發(fā)人員創(chuàng)建自己的事件。
事件流的target,currentTarget和this
說(shuō)了這么久,終于說(shuō)到了當(dāng)初寫(xiě)這篇博客的起因了,為了弄清楚target,currentTarget和this,不斷的查資料,然后發(fā)現(xiàn)不只是弄清楚了這三者的區(qū)別,還對(duì)整個(gè)事件流有了初步的認(rèn)識(shí),是時(shí)候重拾起只看完第七章的《JavaScript高級(jí)程序設(shè)計(jì)》惡補(bǔ)基礎(chǔ)了。
target在事件流的目標(biāo)階段。currentTarget在事件流的捕獲,目標(biāo)及冒泡階段。只有當(dāng)事件流處在目標(biāo)階段的時(shí)候,兩個(gè)的指向才是一樣的, 而當(dāng)處于捕獲和冒泡階段的時(shí)候,target指向被單擊的對(duì)象而currentTarget指向當(dāng)前事件活動(dòng)的對(duì)象(注冊(cè)該事件的對(duì)象)(一般為父級(jí))。this指向永遠(yuǎn)和currentTarget指向一致(只考慮this的普通函數(shù)調(diào)用)。
我們來(lái)進(jìn)行測(cè)試,還是同心圓的例子,首先只考慮W3C標(biāo)準(zhǔn)的瀏覽器(此處是Chrome),代碼如下。
JavaScript:
var div = document.querySelector("div");
div.onclick = function(ev){
console.log(ev.target.nodeName);
console.log(this.nodeName);
console.log(ev.currentTarget.nodeName);
}
點(diǎn)擊內(nèi)層圓,輸出結(jié)果如下:
點(diǎn)擊中層圓,輸出結(jié)果如下:
點(diǎn)擊外層圓,輸出結(jié)果如下:
可以得出結(jié)論,在W3C標(biāo)準(zhǔn)的瀏覽器上,ev.target指向的是事件流的target,而currentTarget和this的指向保持一致,指向當(dāng)前事件活動(dòng)的對(duì)象。
如果涉及到兼容性問(wèn)題,兼容IE8,那么在IE8上會(huì)有一些問(wèn)題,首先需要使用target的兼容寫(xiě)法,其次IE8的event對(duì)象上是沒(méi)有currentTarget屬性的。
因?yàn)楦鱾€(gè)瀏覽器的事件對(duì)象不一樣, 把主要的事件對(duì)象的屬性和方法列出來(lái):
屬性/方法 | 介紹 |
---|---|
bubble | 表明事件是否冒泡 |
cancelable | 表明是否可以取消冒泡 |
currentTarget | 當(dāng)前時(shí)間程序正在處理的元素, 和this一樣的 |
defaultPrevented | false ,如果調(diào)用了preventDefualt這個(gè)就為真了 |
detail | 與事件有關(guān)的信息(滾動(dòng)事件等等) |
eventPhase | 如果值為1表示處于捕獲階段, 值為2表示處于目標(biāo)階段,值為三表示在冒泡階段 |
target or srcElement | 事件的目標(biāo) |
trusted | 為ture是瀏覽器生成的,為false是開(kāi)發(fā)人員創(chuàng)建的(DOM3) |
type | 事件的類型 |
view | 與元素關(guān)聯(lián)的window, 我們可能跨iframe |
preventDefault() | 取消默認(rèn)事件 |
stopPropagation() | 取消冒泡或者捕獲 |
stopImmediatePropagation() | (DOM3)阻止任何事件的運(yùn)行 |
IE下的事件對(duì)象是在window下的,而標(biāo)準(zhǔn)應(yīng)該作為一個(gè)參數(shù), 傳為函數(shù)第一個(gè)參數(shù)。IE的事件對(duì)象定義的屬性跟標(biāo)準(zhǔn)的不同,如:
屬性/方法 | 介紹 |
---|---|
cancelBubble | 默認(rèn)為false, 如果為true就是取消事件冒泡 |
returnValue | 默認(rèn)是true,如果為false就取消默認(rèn)事件 |
srcElement | 這個(gè)指的是target, Firefox下的也是srcElement |
言歸正傳,使用同心圓的例子,并且使用兼容寫(xiě)法,代碼如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
function addEvent(element,type,callback){
if(element.addEventListener){
element.addEventListener(type,callback,false);
}else if(element.attachEvent){
element.attachEvent('on' + type,callback);
}
}
addEvent(div,'click',function(ev){
var event = ev || window.event;
var target = event.target || event.srcElement;
console.log(target.nodeName);
if(event.currentTarget){
console.log(event.currentTarget.nodeName);
}else{
console.log("IE8及以下不支持currentTarget");
}
console.log(this.nodeName);
});
在W3C標(biāo)準(zhǔn)的瀏覽器上,點(diǎn)擊內(nèi)層圓,中層圓和外層圓,輸出結(jié)果與之前只考慮W3C標(biāo)準(zhǔn)的瀏覽器的結(jié)果相同。
在IE8上,點(diǎn)擊內(nèi)層圓,輸出結(jié)果如下:
點(diǎn)擊中層圓,輸出結(jié)果如下:
點(diǎn)擊外層圓,輸出結(jié)果如下:
可以看到,target是和W3C瀏覽器結(jié)果保持一致的,IE8及以下不支持currentTarget,
另外attachEvent的this指向window,沒(méi)有nodeName這個(gè)屬性,所以一直是undefined。