廣州藍景分享-搞懂js事件、事件流(捕獲冒泡)、事件委托
JS事件詳解
javascript
和HTML
之間的交互是通過事件實現(xiàn)的。
事件
就是用戶或瀏覽器自身執(zhí)行的某種動作,比如點擊、加載,鼠標(biāo)移入移出等等。
DOM事件流
事件流包括三個階段。簡而言之:事件一開始從文檔的根節(jié)點流向目標(biāo)對象(捕獲階段),然后在目標(biāo)對象上被觸發(fā)(目標(biāo)階段),之后再回溯到文檔的根節(jié)點(冒泡階段)。
DOM事件流:包括三個階段:
事件捕獲階段(Capture Phase)?事件的第一個階段是捕獲階段。事件從文檔的根節(jié)點出發(fā),隨著 DOM 樹的結(jié)構(gòu)向事件的目標(biāo)節(jié)點流去。途中經(jīng)過各個層次的 DOM 節(jié)點,并在各節(jié)點上觸發(fā)捕獲事件,直到到達事件的目標(biāo)節(jié)點。捕獲階段的主要任務(wù)是建立傳播路徑,在冒泡階段,事件會通過這個路徑回溯到文檔跟節(jié)點。
目標(biāo)階段(Target Phase)?當(dāng)事件到達目標(biāo)節(jié)點的,事件就進入了目標(biāo)階段。事件在目標(biāo)節(jié)點上被觸發(fā)(執(zhí)行事件對應(yīng)的函數(shù)),然后會逆向回流,直到傳播至最外層的文檔節(jié)點。
**冒泡階段(Bubble Phase)**事件在目標(biāo)元素上觸發(fā)后,并不在這個元素上終止。它會隨著 DOM 樹一層層向上冒泡,直到到達最外層的根節(jié)點。也就是說,同一個事件會依次在目標(biāo)節(jié)點的父節(jié)點,父節(jié)點的父節(jié)點...直到最外層的節(jié)點上被觸發(fā)。
所有的事件都要經(jīng)過捕捉階段和目標(biāo)階段,但是有些事件會跳過冒泡階段。例如,讓元素獲得輸入焦點的 focus 事件以及失去輸入焦點的 blur 事件就都不會冒泡。
事件類型
UI (User Interface) 事件,當(dāng)用戶與頁面上的元素交互時觸發(fā)
load、unload、error、select、resize、scroll
焦點事件,在頁面獲得或失去焦點時觸發(fā)
blur、focusout 失去焦點
focus、focusin 獲得焦點
鼠標(biāo)事件,用戶通過鼠標(biāo)在頁面執(zhí)行操作時觸發(fā)
click、dbclick、mousedown、mouseup
mouseenter、mouserleave
mousemove
mouseout、mouseover
點擊和雙擊事件觸發(fā)的順序如下
mousedown
mouseup
click
mousedown
mouseup
dbclick
滾輪事件,當(dāng)使用鼠標(biāo)滾輪操作時觸發(fā)
mousewheel
文本事件,在文檔中輸入文本時觸發(fā)
textInput 當(dāng)用戶在可編輯區(qū)域中輸入字符時,就會觸發(fā)這個事件
鍵盤事件,當(dāng)用戶通過鍵盤在頁面上執(zhí)行操作時觸發(fā)
keydown 按下鍵盤任意鍵時觸發(fā),不松開,則一直觸發(fā)
keypress 按下鍵盤上的字符鍵時觸發(fā),不松開,則一直觸發(fā)
Keyup 用戶釋放鍵盤上的建時觸發(fā)
HTML5事件
contextmenu 事件:單價鼠標(biāo)右鍵可以調(diào)出上下文菜單
beforeunload 事件:在瀏覽器卸載頁面之前觸發(fā)
DOMContentLoad 事件:在形成完整的DOM樹之后就會觸發(fā)。
readystatechange 事件:提供與文檔加載狀態(tài)有關(guān)的信息
pageshow和pagehide 事件:頁面顯示和隱藏時觸發(fā)?MDN傳送門
hashchange 事件 : hash改變時觸發(fā)
除此之外,還有變動事件,復(fù)合事件,HTML5新加入的一些事件,不再一一列出。完整列表可在這里查看 Web Events
事件對象
event:MDN傳送門
在觸發(fā) DOM 上的某個事件時,會產(chǎn)生一個事件對象 event,這個對象中包含著所有與事件有關(guān)的信息。所有的瀏覽器都支持 event 對象,但支持方式不同。
常用屬性:
target
?事件的目標(biāo)
currentTarget
?綁定事件的元素,與 'this' 的指向相同
stopPropagation()
?取消事件的進一步捕獲或冒泡。如果bubbles為true,則可以使用這個方法
stopImmediatePropagation()
?方法阻止監(jiān)聽同一事件的其他事件監(jiān)聽器被調(diào)用。
如果多個事件監(jiān)聽器被附加到相同元素的相同事件類型上,當(dāng)此事件觸發(fā)時,它們會按其被添加的順序被調(diào)用。如果在其中一個事件監(jiān)聽器中執(zhí)行 stopImmediatePropagation() ,那么剩下的事件監(jiān)聽器都不會被調(diào)用。
preventDefault()
?取消事件的默認行為,比如點擊鏈接跳轉(zhuǎn)。如果?cancelable
?是?true
,則可以使用這個方法
type
?被觸發(fā)的事件類型
eventPhase
?調(diào)用事件處理程序的階段:1表示捕獲階段,2表示“處于目標(biāo)”,3表示冒泡階段
更多詳情參見:跨瀏覽器的事件對象
<!DOCTYPE html>
<html lang="en">
<head>
?
? ? ?<meta charset="UTF-8">
?
?<meta name="viewport" content="width=device-width, initial-scale=1.0">
? ?<title>Document</title>
?
?<style>
? ? ? ?
? ? ? .btnCon {
? ? ?
? ? ?width: 300px;
? ? ??
? ? ?height: 300px;
? ? ?
? ? ?border: 1px solid yellow;
? ?
}
? ??
? ?.btn {
? ??
? ? ? ?width: 200px;
? ? ?
? ? ?height: 200px;
? ?
? ?}
? ? ?
?.btn div {
? ? ?
? ? ?width: 100px;
? ??
? ? ?height: 100px;
? ? ?
? ? ?border: 1px solid red;
? ?
}
? ? ?
?.btn div span {
? ? ?
? ? ?background-color: #ccc;
? ?
}
?
?</style>
</head>
<body>
?
?<div class="btnCon">
? ??
? ?<button class="btn">
? ? ??
? ? ?<div>
? ? ? ? ?
? ? ?<span>按鈕</span>
? ? ?
? ? ?</div>
? ?
? ?</button>
??
?</div>
??
?<script>
? ?
?? ?let btnCon = document.querySelector('.btnCon');
??
? ? ?let btn = document.querySelector('.btnCon div');
? ??
? ?// btn.onclick = c;
? ?
?? ?btnCon.addEventListener('click', c);
? ??
? ?btn.addEventListener('click', c);
? ?
? ?// btn.addEventListener('click', c, true);
? ?
? ?function c(event) {
? ? ??
? ? ?event = event || wind
? ? ??
? ? ?console.log(event );
? ? ?
?? ? ?//事件的目標(biāo) ? ? ??
? ? ?console.log('事件的目標(biāo) =>', event.target);
? ? ?
?? ? ?//綁定事件的元素,與 'this' 的指向相同 ? ? ?
?? ? ?console.log("綁定事件的元素 =>", event.currentTarget);
? ? ? ? ? ?console.log(event.currentTarget === event.target);
? ??
? ? ? ?//被觸發(fā)的事件類型 ? ? ?
?? ? ?console.log("被觸發(fā)的事件類型 => ",event.type);
? ??
? ? ? ?//調(diào)用事件處理程序的階段:0表示這個時間沒有事件正在被處理,1表示捕獲階段,2表示“處于目標(biāo)”,3表示冒泡階段 ? ? ? ?
? ?console.log('調(diào)用事件處理程序的階段:0表示這個時間沒有事件正在被處理,1表示捕獲階段,2表示“處于目標(biāo)”,3表示冒泡階段');
? ? ? ? ? ?
console.log(event.eventPhase);
? ? ? ?
?? ?//取消事件的進一步捕獲或冒泡。如果bubbles為true,則可以使用這個方法 ? ? ? ? ? ?event.stopPropagation();
? ? ??
? ? ?//取消事件的進一步捕獲或冒泡,同時阻止任何事件處理程序被調(diào)用(DOM3級事件中新增) ? ? ??
? ? ?event.stopImmediatePropagation();
? ? ??
? ? ?//取消事件的默認行為,比如點擊鏈接跳轉(zhuǎn)。如果 cancelable 是 true,則可以使用這個方法 ? ? ??
? ? ?event.preventDefault();
? ?
?? ?}
??
?</script>
</body>
</html>
阻止事件冒泡/停止傳播(Stopping Propagation)
可以通過調(diào)用事件對象的 stopPropagation 方法,在任何階段(捕獲階段或者冒泡階段)中斷事件的傳播。此后,事件不會在后面?zhèn)鞑ミ^程中的經(jīng)過的節(jié)點上調(diào)用任何的監(jiān)聽函數(shù)。

調(diào)用?event.stopPropagation()
不會阻止當(dāng)前節(jié)點上此事件其他的監(jiān)聽函數(shù)被調(diào)用。如果你希望阻止當(dāng)前節(jié)點上的其他回調(diào)函數(shù)被調(diào)用的話,你可以使用更激進的event.stopImmediatePropagation()
方法。
阻止瀏覽器默認行為
當(dāng)特定事件發(fā)生的時候,瀏覽器會有一些默認的行為作為反應(yīng)。最常見的事件不過于 link 被點擊。當(dāng)一個 click 事件在一個<a>
元素上被觸發(fā)時,它會向上冒泡直到 DOM 結(jié)構(gòu)的最外層 document,瀏覽器會解釋 href 屬性,并且在窗口中加載新地址的內(nèi)容。
在 web 應(yīng)用中,開發(fā)人員經(jīng)常希望能夠自行管理導(dǎo)航(navigation)信息,而不是通過刷新頁面。為了實現(xiàn)這個目的,我們需要阻止瀏覽器針對點擊事件的默認行為,而使用我們自己的處理方式。這時,我們就需要調(diào)用?event.preventDefault().
我們可以阻止瀏覽器的很多其他默認行為。比如,我們可以在 HTML5 游戲中阻止敲擊空格時的頁面滾動行為,或者阻止文本選擇0框的點擊行為。
調(diào)用 event.stopPropagation()只會阻止傳播鏈中后續(xù)的回調(diào)函數(shù)被觸發(fā)。它不會阻止瀏覽器的自身的行為。

事件處理程序
HTML 事件處理程序

當(dāng)然在 HTML 中定義的事件處理程序也可以調(diào)用其它地方定義的腳本:

通過 HTML 指定的事件處理程序都需要HTML的參與,即結(jié)構(gòu)和行為相耦合,不易維護。
DOM0 級事件處理程序

這里是將一個函數(shù)賦值給一個事件處理程序的屬性,以這種方式添加的事件處理程序會在事件流的冒泡階段被處理。要刪除事件將 btn.onclick 設(shè)置為?null?即可。
DOM2 級事件處理程序
DOM2 級事件定義了addEventListener()?和?removeEventListener()
兩個方法,用于添加和刪除事件處理程序的操作。
所有 DOM 節(jié)點都包含這兩個方法,它們接受3個參數(shù):要處理的事件名、作為事件處理程序的函數(shù)和一個布爾值。最后的布爾值參數(shù)是?true 表示在捕獲階段調(diào)用事件處理程序
,如果是?false(默認) 表示在冒泡階段調(diào)用事件處理程序
。

通過上面例子可以看出,同一個dom元素,可以通過addEventListener()
添加多個事件
上面代碼兩個事件處理程序會按照它們的添加順序觸發(fā),先輸出 btn 再輸出 Hello word!,
通過?addEventListener()
添加的事件處理程序只能使用?removeEventListener()
來移除,移除時傳入的參數(shù)與添加時使用的參數(shù)相同,即匿名函數(shù)無法被移除。

IE 事件處理程序
IE通常都是特立獨行的,它添加和刪除事件處理程序的方法分別是:attachEvent()?和detachEvent()
同樣接受事件處理程序名稱與事件處理程序函數(shù)兩個參數(shù),但跟**addEventListener()**的區(qū)別是:
事件名稱需要加“on”,比如“onclick”;
沒了第三個布爾值,IE8及更早版本只支持事件冒泡;
仍可添加多個處理程序,但觸發(fā)順序是反著來的。
還有一點需要注意,DOM0 和 DOM2 級的方法,其作用域都是在其所依附的元素當(dāng)中,attachEvent()則是全局
,即如果像之前一樣使用this.id,訪問到的就不是 button 元素,而是 window,就得不到正確的結(jié)果。
封裝兼容瀏覽器事件處理



事件委托/代理事件監(jiān)聽
事件委托利用了事件冒泡
,只指定一個事件處理程序,就可以管理某一類型的所有事件。例如,click 事件會一直冒泡到 document 層次(根元素),也就是說,我們可以為整個頁面指定一個 onclick 事件處理程序,而不必為每個可點擊的元素分別添加事件處理程序。
事件委托的優(yōu)點
可以大量節(jié)省內(nèi)存占用,減少事件注冊
可以實現(xiàn)當(dāng)新增子對象時無需再次對其綁定(動態(tài)綁定事件)
使用事件委托注意事項
使用“事件委托”時,并不是說把事件委托給的元素越靠近頂層就越好。
事件冒泡的過程也需要耗時,越靠近頂層,事件的”事件傳播鏈”越長,也就越耗時。
如果DOM嵌套結(jié)構(gòu)很深,事件冒泡通過大量祖先元素會導(dǎo)致性能損失。

上面例子中的EVentUtil對象,是我們之前封裝的,可以往上看看
通過上面例子可以看到, 我們在ul 標(biāo)簽 id="myLinks" 綁定了事件,
然后我們可以通過點擊不同的li,li通過事件冒泡,來觸發(fā)到ul綁定的事件,
這樣在事件對應(yīng)的函數(shù)內(nèi)寫上判斷,
我們就可以通過點擊不同的li,就可以執(zhí)行相應(yīng)的代碼了
就不用給每個li綁定事件,而且以后新增li也綁定事件,統(tǒng)一在ul綁定了事件,這也就是動態(tài)綁定事件
移除事件處理程序
內(nèi)存中留有那些過時不用的“空事件處理程序
”,也是造成 web 應(yīng)用程序內(nèi)存與性能問題的主要原因。
在兩種情況下,可能會造成上述問題:
第一種情況就是從文檔中移除帶有事件處理程序的元素時。這可能是通過純粹的DOM操作,例如使用removeChild()
和replaceChild()
方法,但更多地是發(fā)生在使用?innerHTML
?替換頁面中某一部分的時候。如果帶有事件處理程序的元素被?innerHTML
?刪除了,那么原來添加到元素中的事件處理程序極有可能被當(dāng)作垃圾回收。來看下面的例子:

這里,有一個按鈕被包含在<div>
元素中,為避免雙擊,單擊這個按鈕時就將按鈕移除并替換成一條消息;這是網(wǎng)站設(shè)計中非常流行的一種做法。但問題在于,當(dāng)按鈕被從頁面中移除時,它還帶著一個事件處理程序呢,在<div>
元素中設(shè)置 innerHTML 可以把按鈕移走,但事件處理各種仍然與按鈕保持著引用聯(lián)系。有的瀏覽器(尤其是IE
)在這種情況下不會作出恰當(dāng)?shù)奶幚恚鼈兒苡锌赡軙υ睾褪录幚沓绦虻囊枚急4嬖趦?nèi)存中。如果你想知道某個元即將被移除,那么最好手工移除事件處理程序。如下面的例子所示:

在此,我們設(shè)置<div>
的innerHTML屬性之前,先移除了按鈕的事件處理程序。這樣就確保了內(nèi)存可以被再次利用,而從DOM中移除按鈕也做到了干凈利索。
注意,在事件處理程序中刪除按鈕也能阻止事件冒泡。目標(biāo)元素在文檔中是事件冒泡的前提, 有元素節(jié)點才能冒泡上去。
**導(dǎo)致“空事件處理程序”的另一情況,就是卸載頁面中的時候。**毫不奇怪,IE在這種情況下依然是問題最多的瀏覽器,盡管其他瀏覽器或多或少也有類似的問題。如果在頁面被卸載之前沒有清理干凈事件處理程序。那它們就會滯留在內(nèi)存中。每次加載完頁面再卸載頁面時(可能是在兩個頁面間來加切換,也可以是單擊了“刷新”按鈕),內(nèi)存中滯留的對象數(shù)目就會增加,因為事件處理程序占用的內(nèi)存并沒有被釋放。
一般來說,最好的做法是在頁面卸載之前 ,先通過?onunload
?事件處理程序移除所有事件處理程序。在此,事件委托技術(shù)再次表現(xiàn)出它的優(yōu)勢——需要跟蹤的事件程序越少,移除它們就越容易,對這種類似的操作,我們可把它想象成:只要是通過?onload
?事件處理程序添加的東西,最后都要通過?onunload
?事件處理程序?qū)⑺鼈円瞥?/p>
一些常用事情的操作
load 事件:
load 事件可以在任何資源(包括被依賴的資源)被加載完成時被觸發(fā),這些資源可以是圖片,css,腳本,視頻,音頻等文件,也可以是 document 或者 window。

Image 元素 load:

注意:新圖像元素不一定要添加到文檔后才開始下載,只要設(shè)置了 src 屬性就會開始下載。
script 元素 load:

與圖像不同,只有設(shè)置了 script 元素的 src 屬性并將元素添加到文檔后,才會開始下載 js 文件
onbeforeunload 事件(HTML5事件):?監(jiān)聽關(guān)閉頁面事件
window.onbeforeunload
?讓開發(fā)人員可以在想用戶離開一個頁面的時候進行確認。這個在有些應(yīng)用中非常有用,比如用戶不小心關(guān)閉瀏覽器的 tab,我們可以要求用戶保存他的修改和數(shù)據(jù),否則將會丟失他這次的操作。

需要注意的是,對頁面添加?onbeforeunload
?處理會導(dǎo)致瀏覽器不對頁面進行緩存,這樣會影響頁面的訪問響應(yīng)時間。 同時,onbeforeunload
?的處理函數(shù)必須是同步的(synchronous)。
resize 事件:?監(jiān)聽頁面大小變化事件
在一些復(fù)雜的響應(yīng)式布局中,對 window 對象監(jiān)聽 resize 事件是非常常用的一個技巧。僅僅通過 css 來達到想要的布局效果比較困難。很多時候,我們需要使用 JavaScript 來計算并設(shè)置一個元素的大小。
error 事件:
當(dāng)我們的應(yīng)用在加載資源的時候發(fā)生了錯誤,我們很多時候需要去做點什么,尤其當(dāng)用戶處于一個不穩(wěn)定的網(wǎng)絡(luò)情況下。Financial Times 中,我們使用 error 事件來監(jiān)測文章中的某些圖片加載失敗,從而立刻隱藏它。由于“DOM Leven 3 Event”規(guī)定重新定義了 error 事件不再冒泡,我們可以使用如下的兩種方式來處理這個事件。

不幸的是,addEventListener 并不能處理所有的情況。而確保圖片加載錯誤回調(diào)函數(shù)被執(zhí)行的唯一方式是使用讓人詬病內(nèi)聯(lián)事件處理函數(shù)(inline event handlers)。
<img src="http://example.com/image.jpg" onerror="this.style.display='none';" />
原因是你不能確定綁定 error 事件處理函數(shù)的代碼會在 error 事件發(fā)生之前被執(zhí)行。而使用內(nèi)聯(lián)處理函數(shù)意味著在標(biāo)簽被解析并且請求圖片的時候,error監(jiān)聽器也將并綁定。
當(dāng)JavaScript運行時錯誤(包括語法錯誤)發(fā)生時,window會觸發(fā)一個ErrorEvent接口的error事件,并執(zhí)行window.onerror()。
加載一個全局的error事件處理函數(shù)可用于自動收集錯誤報告。
window.onerror = function(message, source, lineno, colno, error) { ... }
函數(shù)參數(shù):
message:錯誤信息(字符串)??捎糜贖TML onerror=""處理程序中的event。
source:發(fā)生錯誤的腳本URL(字符串)
lineno:發(fā)生錯誤的行號(數(shù)字)
colno:發(fā)生錯誤的列號(數(shù)字)
error:Error對象(對象)
若該函數(shù)返回true,則阻止執(zhí)行默認事件處理函數(shù)。
window.addEventListener('error', function(event) { ... })
window.error詳情;