前言: 此篇文章稍長,請您用心閱讀,完後我相信會解決你許多疑惑。javascript
咱們以一個 toy demo 開始.html
// dom level
-> div (onclick)
-> p
-> span
複製代碼
問題🤔: 爲何當咱們點擊<p>
, <span>
click 事件也觸發了?java
冒泡是事件的一種傳遞機制,當一個事件發生時,事件會以相反的順序傳播經過目標再父級祖先,最後以Window結束。面試
就拿以前的例子🌰來講,當用戶點擊<span>
元素時,事件會從下至上(子元素->祖先)依次觸發click事件。而且幾乎全部的事件都會有冒泡階段。數組
咱們知道在事件的傳遞機制中除了冒泡還有一種咱們常常提到的就是捕獲機制。瀏覽器
其實這是不徹底正確的!!!,依據W3C的定義, 事件的傳遞分爲了3階段:bash
Capture(捕獲階段): 事件對象經過Window傳播到目標的祖先父級再到自身。dom
Target(目標階段): 事件對象到達目標自身, 當該事件類型指定不Bubble, 則事件將在此階段終止(稍後解釋)。ui
Bubble(冒泡階段): 事件對象以相反的順序傳播經過目標的父級祖先,並以Window結束。this
因此當一個事件發生時標準的傳遞流爲 捕獲 -> 目標 -> 冒泡
當咱們程序在主流的瀏覽器運行時,咱們在 html 中使用 on[eventType]
或者 javascript 中使用 element. addEventListener(eventType, listener)
這些Web API像是忽略了 捕獲階段 只會運行 目標階段 和 冒泡階段。
問題🤔:那麼這時候就產生了一個問題? 若咱們在一個嵌套的Dom上分別添加事件,咱們就不能改變事件的觸發順序(子元素綁定的事件會先於父元素綁定的事件觸發),咱們如何改變這個順序?
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來講,它不只僅只能傳入function
還能是一個對象但這個對象必須實現Event
接口(包含handleEvent(fn)
屬性),在此咱們不對它過多解釋,具體能夠去參看文檔.
接下來咱們具體說說 listener 被調用時傳入的參數 Event 或 簡寫爲 e
// 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.target
(read-only) 並不必定爲添加事件所對應元素,那麼如何在回調中知道是那個元素添加的此事件監聽呢? 這就是 e.currentTarget
,另外在 listener 方法內部也能夠直接使用 this
,它等同於(this = event.currentTarget
)
e.path
(read-only)爲一個數組eg: [span, p, div, body, html, document, Window]
它表示從 e.target
到 window
所經歷到元素層級。
咱們在事件傳遞階段講 捕獲階段 的時候提到 能夠提早終止不在進行冒泡階段。 這是怎麼作的了,其實能夠在捕獲階段添加的監聽事件回調被調用時候調用e.stopPropagation()
來阻止事件在DOM中進一步傳播。 因爲歷史緣由也能夠調用 e.cancelBubble = true
來阻止事件冒泡(但不建議使用此屬性,最好使用 stopPropagation
方法)
Event
對象上還有許多屬性,在這裏不會所有羅列,最經常使用的基本上就是以上幾個,其他屬性還能夠拿到不少信息,但部分屬性可能並非標準
以上是有關EventListener一些常識,但願你們可以不吝賜教👆
若是還有未涉及到的,歡迎提出討論。
Why write this ?
主要是因爲本人近期一次【bcz】面試的遭遇有關而發:
一是面談中提到此話題因爲我我的技藝不精不少細枝末節已經忘記,最終面試fail。主要問題在我,這也致使我想從新紀錄下。
二是我尊重每一個公司的觀念,但不表明我承認。經歷【bcz】面試後,我認爲貴公司,面試存在不少問題,這給個人感受是一家特別重視基礎的公司(重視基礎沒有錯)。拿面試官自身來講經過交流我能感受到面試官對於知識也不夠深刻。汲取知識應該終保持敬畏之心,既然大家十分重視基礎那麼大家最好作到權威,否則在半灌水的體量下你如何來評定? 若是你讓面試者有這種感受,我只能認爲你是在爲了面試而面試,這不該該是應試教育。
三是寫完個人自閉可能會緩解。