由event.target引發(fā)的關(guān)于事件流的一連串思考(一)

前言:之前的上傳圖片用到了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é)。


實(shí)際使用中的DOM事件流原型圖

默認(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)輸出如下:

DOM0綁定事件測(cè)試

可以看到,輸出的順序是從內(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é)果如下:


DOM2綁定事件,第三個(gè)參數(shù)為空的情況

第三個(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é)果如下:


DOM2綁定事件,第三個(gè)參數(shù)為true的情況

可以看到,第三個(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é)果如下:


點(diǎn)擊內(nèi)層圓

可以看到,先觸發(fā)捕獲事件,然后觸發(fā)冒泡事件,并且DOM0和DOM2的事件互不影響,誰(shuí)先綁定就先執(zhí)行。

點(diǎn)擊中層圓,輸出結(jié)果如下:


點(diǎn)擊中層圓

結(jié)果和點(diǎn)擊內(nèi)層圓一樣,沒(méi)毛病。

點(diǎn)擊外層圓,輸出結(jié)果如下:


點(diǎn)擊外層圓

很神奇,外層圓的事件順序不是按照事件流了,這是為什么呢。其實(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é)果如下:

attachEvent

吐槽一下,IE8及以下不支持border-radius屬性,所以已經(jīng)不能算是同心圓了。

IE8下的同心圓

由輸出結(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é)果如下:


Chrome

在IE8上輸出結(jié)果如下:


IE8

還是有些區(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)擊內(nèi)層圓

點(diǎn)擊中層圓,輸出結(jié)果如下:


點(diǎn)擊中層圓

點(diǎn)擊外層圓,輸出結(jié)果如下:
點(diǎn)擊外層圓

可以得出結(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é)果如下:

在IE8上點(diǎn)擊內(nèi)層圓

點(diǎn)擊中層圓,輸出結(jié)果如下:


在IE8上點(diǎn)擊中層圓

點(diǎn)擊外層圓,輸出結(jié)果如下:


在IE8上點(diǎn)擊外層圓

可以看到,target是和W3C瀏覽器結(jié)果保持一致的,IE8及以下不支持currentTarget,
另外attachEvent的this指向window,沒(méi)有nodeName這個(gè)屬性,所以一直是undefined。

顏色參考:
http://zhongguose.com/
http://nipponcolors.com/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容