【WEB前端】百度前端面試經歷小研究2——事件順序與繫結

於明昊發表於2013-09-04

本來以為一篇小文章就能分析完這篇百度Web前端面試經歷,結果光是就JS的型別轉換,就寫了不小的篇幅(實際上也是因為本人能力有限,搞了半天都沒搞明白型別轉換究竟是怎麼回事兒,之前還在知乎上進行了提問:JavaScript中加號運算子的型別轉換優先順序是什麼,實際上發現自己的提問特別的愚蠢,因為在加法運算裡,本來優先順序就是字串最高,數值次之,在隱式轉換裡,除去布林值之外,大多數的物件都是通過 toString 轉換的原始值,JS在拿到原始值之後發現可用,就直接使用了,所以最後轉成字元是一件很正常的事情)。這裡再開一篇文章,簡要的說明以下作者提到的事件冒泡機制的問題。

面試官:說說事件冒泡的機制。

我:blablabla。

面試官:如果上層元素想知道到底是從哪個元素起的泡,怎麼搞?

我:Event的target屬性吧。

面試官:不是,再想想。

我:真心不會。。。(面試官也沒告訴我答案,整個面試過程中感覺這位面試官側重於指引你自己去找尋答案,不會告訴你答案的)

這裡面試官的提問面向的點是 Javascript 裡有名的事件順序(Event Order),實際上已經有很多文章對這一問題進行了深入淺出的討論,比如quirksomde的這篇博文,以及譯文。在這裡先進行事件順序的討論,也有助於在日後討論事件繫結時進行一些基礎鋪墊。

這裡先說一下作者的blabla:

不管是冒泡還是繫結機制,實際上都是為了解決一個問題:當頁面存在元素巢狀時,同時巢狀的元素又繫結了相同的事件處理函式(比如都繫結了onClick),那麼在裡層元素觸發此事件,是先響應外層元素的還是裡層的呢?比如下面這段程式碼:

<!--html-->
<div class="outside">
    <div class="inside">
    </div>
</div>

<!--script-->
<script>
    var captureClass = function(e) {
        console.log(this.className + ' is captured')
    }

    var bubbleClass = function(e) {
        console.log(this.className + ' is bubbled')
    }

    var main = function() {
        var outside = document.getElementsByClassName('outside')[0]
        ,   inside = document.getElementsByClassName('inside')[0]        
        ,   capture = true
        ,   bubble = false

        outside.addEventListener('click', bubbleClass, bubble)
        inside.addEventListener('click', bubbleClass, bubble)    
        inside.addEventListener('click', captureClass, capture)        
        outside.addEventListener('click', captureClass, capture)
    }
</script>

根據文章裡所說,在瀏覽器大戰時期,分別產生了兩種處理方法,稱為捕獲型冒泡型,簡而言之:捕獲型就是從外至內繫結,冒泡型就是從內至外繫結。至於 addEventListener() 的具體執行過程,可以參照上文所說的譯文裡,引用一段如下:

W3c明智的在這場爭鬥中選擇了一個擇中的方案。任何發生在w3c事件模型中的事件,首是進入捕獲階段,直到達到目標元素,再進入冒泡階段;

為一個web開發者,你可以選擇是在捕獲階段還是冒泡階段繫結事件處理函式,這是通過 addEventListener() 方法實現的,如果這個函式的最後一個引數是true,則在捕獲階段繫結函式,反之false,在冒泡階段繫結函式。

也就是說,如果有元素巢狀,那麼在執行 addEventListener() 時,首先進入捕獲階段,即從外至內開始檢查,如果有元素設定為在此階段繫結,則繫結之;之後再從內向外冒泡,如果有元素設定為在此階段繫結,再繫結之。按照這個解釋,我們可以猜想上述函式的執行結果應該如下所示:

//控制檯輸出,先從外向內進行捕獲,再從內向外進行冒泡,產生與程式碼順序逆序的輸出
outside is captured
inside is captured
inside is bubbled
outside is bubbled 

但是實際執行程式碼卻發現,實際的控制檯輸出是這個樣子的:

//外側元素按照先捕獲後冒泡的機制,而內測元素按照程式碼順序進行繫結
outside is captured
inside is bubbled
inside is captured
outside is bubbled

在這裡我沒有找到合理的解釋,我認為原因可能是事件在達到最內測元素時,就不再檢查元素的處理階段,而是按照程式碼的執行順序執行。正如當只有一個元素時,不管給其繫結捕獲還是冒泡,都不影響處理函式的執行順序——也就是程式碼中的執行順序。

至於上層元素想知道是哪個元素起的泡,我們可以看看MDN上關於 event.target解釋 以及 event.currentTarget解釋,我摘錄部分如下:

(target) This property of event objects is the object the event was dispatched on. It is different than event.currentTarget when the event handler is called in bubbling or capturing phase of the event.

(currentTarget) Identifies the current target for the event, as the event traverses the DOM. It always refers to the element the event handler has been attached to as opposed to event.target which identifies the element on which the event occurred.

翻譯過來就是:event 物件的 target 屬性指代觸發這個 event 的物件。其在冒泡和捕獲型事件中的指代與 event.currentTarget 均不同。而 currentTarget 是指當事件在整個DOM中起泡時,找到本次事件的繫結者;而對應 target 指代本次事件的引發者。

而MDN裡也明確說明了:

The event.target property can be used in order to implement event delegation.

也就是說 target 是用於事件代理的,也就是說在 target 是用於外部元素設定事件,在事件觸發時找到觸發該事件的內部元素的方法。所以如果面試官問的是冒泡,在上層元素繫結的時間中進行時間處理,那麼 event.target 是可以找到起泡元素的。

所以這裡我認為作者實際上說的並沒有錯,個人在這裡不是很理解面試官為何認為作者的回答有誤。


首先感謝@Lemoncolaz的回覆,面試官應該是考慮到了IE中srcElement屬性,在stackoverflow中也有這樣一個回答,其中如果首位的答主表示,考慮瀏覽器相容,應該寫作如下的格式:

var target = event.target || event.srcElement;

接下來補上事件的繫結部分,原文中面試官的提問如下:

面試官:說說事件繫結。

我:W3C是addEventListener,IE是attachEvent。

面試官:這兩種事件繫結有什麼不同。

我:。。。。(真心不知道有什麼不同,亂說一氣,難道是繫結事件執行的先後順序不同?)。

面試官:這兩種繫結還是有很大差別的。

我:。。。。是的是的,之前沒有了解過。

面試官在這裡把作者問倒了,而這一段內容是在犀牛書中有仔細講解的。當然,對於我們這種基本採用jQuery來解決IE相容的開發者來說,對於這方面知識的匱乏也算是一個常態。

在犀牛書裡,列舉了一下 attachEvent()addEventListener() 的區別如下:

  • 在瀏覽器支援上,IE5-8支援前者,IE9+和其他瀏覽器支援後者。由於原始版本IE並不支援事件的捕獲,所以前者只有兩個引數:事件型別和處理程式函式。

  • 前者採用了帶有 'on' 字首的時間處理程式屬性名,而後者不帶此字首。

  • 前者允許相同的事件處理程式函式被註冊多次。

相關文章