一篇文章能否解決你事件監聽的許多疑問

moZLeo發表於2019-07-10

前言: 此篇文章稍長,請您用心閱讀,完後我相信會解決你許多疑惑。

我們以一個 toy demo 開始.

// dom level
  -> div (onclick)
    -> p
      -> span
複製程式碼

問題?: 為什麼當我們點選<p>, <span> click 事件也觸發了?

Bubbling(冒泡)

冒泡是事件的一種傳遞機制,當一個事件發生時,事件會以相反的順序傳播通過目標再父級祖先,最後以Window結束。

就拿之前的例子?來說,當使用者點選<span>元素時,事件會從下至上(子元素->祖先)依次觸發click事件。並且幾乎所有的事件都會有冒泡階段。

Capture (捕獲)

我們知道在事件的傳遞機制中除了冒泡還有一種我們經常提到的就是捕獲機制。

其實這是不完全正確的!!!,依據W3C的定義, 事件的傳遞分為了3階段

  1. Capture(捕獲階段): 事件物件通過Window傳播到目標的祖先父級再到自身。

  2. Target(目標階段): 事件物件到達目標自身, 當該事件型別指定不Bubble, 則事件將在此階段終止(稍後解釋)

  3. Bubble(冒泡階段): 事件物件以相反的順序傳播通過目標的父級祖先,並以Window結束。

image

所以當一個事件發生時標準的傳遞流為 捕獲 -> 目標 -> 冒泡

當我們程式在主流的瀏覽器執行時,我們在 html 中使用 on[eventType] 或者 javascript 中使用 element. addEventListener(eventType, listener) 這些Web API像是忽略了 捕獲階段 只會執行 目標階段 和 冒泡階段。

問題?:那麼這時候就產生了一個問題? 若我們在一個巢狀的Dom上分別新增事件,我們就不能改變事件的觸發順序(子元素繫結的事件會先於父元素繫結的事件觸發),我們如何改變這個順序?

EventListener Option -【 options ={} | useCapture = boolean 】

感謝 ? Web API 的完備性,如果你看過 addEventListener API 的定義,你會發現,宣告是有第三個引數選項的,它可傳入一個 boolean 或者 一個物件。

elem.addEventListener(type, listener[, options]);
elem.addEventListener(type, listener[, useCapture]);
複製程式碼

所以當你在一個 元素 上新增對應的事件監聽時, 你可以這樣寫:

elem.addEventListener(_, _, {capture: true})
// 物件的形式還接受更多的option:[once,...]
// 或者
elem.addEventListener(_, _, true)
複製程式碼

他們是等價的,表示監聽的回撥將會在 捕獲階段 被觸發。那麼這樣我們可以解決上面的問題。 例如:

// dom level
  -> div (onclick= log('div', true))
    -> p (onclick = log('p'))
      -> span
// 此時輸出順序為 div -> p
// 相反
 -> div (onclick= log('div'))
    -> p (onclick = log('p'))
      -> span
// 此時輸出順序為 p -> div
複製程式碼

listener callback param - Event

首先對於listener來說,它不僅僅只能傳入function還能是一個物件但這個物件必須實現Event介面(包含handleEvent(fn)屬性),在此我們不對它過多解釋,具體可以去參看文件.

接下來我們具體說說 listener 被呼叫時傳入的引數 Event 或 簡寫為 e

e.target

// dom level
  -> div (onclick)
    -> p
      -> span
複製程式碼

還是使用之前的例子,我們解釋幾個誤區(這裡可能存在錯誤?,希望大佬發現後不吝糾正):

⚠️誤區 1 : 當<div>新增了 click 事件,是否代表 <p>, <span> 也新增了 click 事件

答: ❌, 事實上在 <div> 新增了 click 與它的子元素並沒有任何關係,但是可以通過 Event 物件拿到觸發事件的真正物件,這看起來就好像是 <div> 的子元素同樣新增了此事件監聽。記住回撥是發生在新增事件監聽的目標元素身上的(click 的 listener callback 是發生在 div 上的)。

⚠️誤區 2: 既然事件的傳遞分為 捕獲 -> 目標 -> 冒泡,那麼為什麼一次點選事件不會多次觸發。

答: 從之前的 addEventListener API 我們知道 事件是可以繫結到 冒泡 或者 捕獲 階段的,當沒有設定時預設是在 冒泡階段 所以只會觸發一次,那就是在對應繫結的階段。

注意: 既然新增事件是區分階段,那麼在移除此事件時也需要明確對應階段。

解釋了上述誤區,我們回過來說 e.target : 當事件觸發時,e.target (read-only)的值為最深巢狀相關元素,並不一定為新增事件所對應元素。例如上訴例子,當你點選<span>時事件傳遞冒泡走到<div>元素其 click 事件回撥被觸發,而e.target 的值是 span 元素物件。

e.currentTarget

我們知道了 e.target(read-only) 並不一定為新增事件所對應元素,那麼如何在回撥中知道是那個元素新增的此事件監聽呢? 這就是 e.currentTarget ,另外在 listener 方法內部也可以直接使用 this,它等同於(this = event.currentTarget

e.path

e.path (read-only)為一個陣列eg: [span, p, div, body, html, document, Window]它表示從 e.targetwindow 所經歷到元素層級。

e.stopPropagation()

我們在事件傳遞階段講 捕獲階段 的時候提到 可以提前終止不在進行冒泡階段。 這是怎麼做的了,其實可以在捕獲階段新增的監聽事件回撥被呼叫時候呼叫e.stopPropagation() 來阻止事件在DOM中進一步傳播。 由於歷史原因也可以呼叫 e.cancelBubble = true 來阻止事件冒泡(但不建議使用此屬性,最好使用 stopPropagation 方法)

e.[other]

Event物件上還有許多屬性,在這裡不會全部羅列,最常用的基本上就是以上幾個,其餘屬性還可以拿到很多資訊,但部分屬性可能並不是標準


以上是有關EventListener一些常識,希望大家能夠不吝賜教?

如果還有未涉及到的,歡迎提出討論。


Why write this ?

主要是由於本人近期一次【bcz】面試的遭遇有關而發:

一是面談中提到此話題由於我個人技藝不精很多細枝末節已經忘記,最終面試fail。主要問題在我,這也導致我想重新紀錄下。

二是我尊重每個公司的觀念,但不代表我認可。經歷【bcz】面試後,我認為貴公司,面試存在很多問題,這給我的感覺是一家特別重視基礎的公司(重視基礎沒有錯)。拿面試官自身來說通過交流我能感覺到面試官對於知識也不夠深入。汲取知識應該終保持敬畏之心,既然你們十分重視基礎那麼你們最好做到權威,不然在半灌水的體量下你如何來評定? 如果你讓面試者有這種感覺,我只能認為你是在為了面試而面試,這不應該是應試教育。

三是寫完我的自閉可能會緩解。

相關文章