事件流
JavaScript中,事件流指的是DOM事件流。
概念
事件的傳播過程即DOM事件流。
事件對象在 DOM 中的傳播過程,被稱為“事件流”。
舉個例子:開電腦這個事,首先你是不是得先找到你的電腦,然后找到你的開機鍵,最后用手按下開機鍵。完成開電腦這個事件。這整個流程叫做事件流。
DOM事件流
DOM事件,也是有一個流程的。從事件觸發開始到事件響應是有三個階段。
- 事件捕獲階段
- 處於目標階段
- 事件冒泡階段
上面例子中,開電腦這個事件的過程就像JavaScript中的事件流,找開機鍵這個過程就是 事件捕獲 的過程,你找到開機鍵后,然后用手按開機鍵,這個選擇用手去按的過程就是 處於目標階段 按下開機按鈕,電腦開始開機這也就是 事件的冒泡。 順序為先捕獲再冒泡。
了解了事件源,讓我們看看它的三個過程吧!
1.事件捕獲
注:由於事件捕獲不被舊版本瀏覽器(IE8 及以下)支持,因此實際中通常在冒泡階段觸發事件處理程序。
事件捕獲處於事件流的第一步,
DOM事件觸發時(被觸發DOM事件的這個元素被叫作事件源),瀏覽器會從根節點開始 由外到內 進行事件傳播。即事件從文檔的根節點流向目標對象節點。途中經過各個層次的DOM節點,最終到目標節點,完成事件捕獲。
2.目標階段
當事件到達目標節點的,事件就進入了目標階段。事件在目標節點上被觸發。
就是事件傳播到觸發事件的最底層元素上。
3.事件冒泡
事件冒泡與事件捕獲順序相反。事件捕獲的順序是從外到內,事件冒泡是從內到外。
當事件傳播到了目標階段后,處於目標階段的元素就會將接收到的時間向上傳播,就是順着事件捕獲的路徑,反着傳播一次,逐級的向上傳播到該元素的祖先元素。直到window對象。
看一個例子,點擊 box3 會將 box2 與 box1 的點擊事件觸發。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JavaScript 事件冒泡</title>
</head>
<style type="text/css">
#box1 { background: blueviolet;}
#box2 {background: aquamarine;}
#box3 {background: tomato;}
div { padding: 40px; margin: auto;}
</style>
<body>
<div id="box1">
<div id="box2">
<div id="box3"></div>
</div>
</div>
<script>
window.onload = function () {
const box1 = document.getElementById('box1')
const box2 = document.getElementById('box2')
const box3 = document.getElementById('box3')
box1.onclick = sayBox1;
box2.onclick = sayBox2;
box3.onclick = sayBox3;
function sayBox3() {
console.log('你點了最里面的box');
}
function sayBox2() {
console.log('你點了最中間的box');
}
function sayBox1() {
console.log('你點了最外面的box');
}
}
</script>
</body>
</html>
這個時候 click 捕獲的傳播順序為:
window -> document -> <html> -> <body> -> <div #box1> -> <div #box2> -> <div #box3>
這個時候 click 冒泡的傳播順序為:
<div #box3> -> <div #box2> -> <div #box1> -> <body> -> <html> -> document -> window
現代瀏覽器都是從 window 對象開始捕獲事件的,冒泡最后一站也是 window 對象。而 IE8 及以下瀏覽器,只會冒泡到 document 對象。
事件冒泡:是由元素的 HTML 結構決定,而不是由元素在頁面中的位置決定,所以即便使用定位或浮動使元素脫離父元素的范圍,單擊元素時,其依然存在冒泡現象。
現在我們知道了事件流的三個階段后,那我們可以利用這個特性做什么呢?
事件委托
設想這樣一個場景,當你有一堆的<li>
標簽在一個<ul>
標簽下,需要給所有的<li>
標簽綁定onclick
事件,這個問題我們可以用循環解決,但還有沒有更簡便的方式呢?
我們可以給這些<li>
共同的父元素<ul>
添加onclick
事件,那么里面的任何一個<li>
標簽觸發onclick
事件時,都會通過冒泡機制,將onclick
事件傳播到<ul>
上,進行處理。這個行為叫做事件委托,<li>
利用事件冒泡將事件委托到<ul>
上。
也可以利用事件捕獲進行事件委托。用法是一樣的,只是順序反了。
<ul id="myUl">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
...
</ul>
可能還是有點不好理解,簡單來說,就是利用事件冒泡,將某個元素上的事件委托給他的父級。
舉個生活中的例子,雙十一快遞到了,需要快遞小哥送快遞一般是挨家挨戶送貨上門,這樣效率慢,小哥想了個辦法,把一個小區的快遞都放在小區里面的快遞驛站,進行送快遞的事件委托,小區的收件人能通過取件碼去快遞驛站領取到自己的快遞。
在這里,快遞小哥送快遞就是一個事件,收件人就是響應事件的元素,驛站就相當於代理元素,收件人憑着收獲碼去驛站里面領快遞就是事件執行中,代理元素判斷當前響應的事件匹配該觸發的具體事件。
可是這樣做有什么好處呢?
事件委托的優點
事件委托有兩個好處
-
減少內存消耗
-
動態綁定事件
-
減少內存消耗,優化頁面性能
在JavaScript中,每個事件處理程序都是對象,是對象就會占用頁面內存,內存中的對象越多,頁面的性能當然越差,而且DOM的操作是會導致瀏覽器對頁面進行重排和重繪(這個不清楚的話,小伙伴可以了解頁面的渲染過程),過多的DOM操作會影響頁面的性能。性能優化主要思想之一就是為了最小化的重排和重繪也就是減少DOM操作。
在上面給<li>
標簽綁定onclick
事件的例子中,使用事件委托就可以不用給每一個<li>
綁定一個函數,只需要給<ul>
綁定一次就可以了,當li的數量很多時,無疑能減少大量的內存消耗,節約效率。
- 動態綁定事件
如果子元素不確定或者動態生成,可以通過監聽父元素來取代監聽子元素。
還是上面在<li>
標簽綁定onclick
事件的例子中, 很多時候我們的這些<li>
標簽的數量並不是固定的,會根據用戶的操作對一些<li>
標簽進行增刪操作。在每次增加或刪除標簽都要重新對新增或刪除元素綁定或解綁對應事件。
可以使用事件委托就可以不用給每一個<li>
都要操作一遍,只需要給<ul>
綁定一次就可以了,因為事件是綁定在<ul>
上的,<li>
元素是影響不到<ul>
的 ,執行到<li>
元素是在真正響應執行事件函數的過程中去匹配的,所以使用事件委托在動態綁定事件的情況下是可以減少很多重復工作的。
我們知道了事件委托的優點,那么該如何使用呢?
事件委托的使用
事件委托的使用需要用的addEventListener()
方法,事件監聽。
方法將指定的監聽器注冊到調用該函數的對象上,當該對象觸發指定的事件時,指定的回調函數就會被執行。
- 用法
element.addEventListener(eventType, function, useCapture);
- 參數描述
參數 | 必/選填 | 描述 |
---|---|---|
eventType | 必填 | 指定事件的類型。 |
function | 必填 | 指定事件觸發后的回調函數。 |
useCapture | 選填 | 指定事件是在捕獲階段執行還是在冒泡階段執行。 |
第三個參數 useCapture
是個布爾類型,默認值為false
- true - 表示事件在捕獲階段執行執行
- false- 表示事件在冒泡階段執行執行
看下面例子
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JavaScript 事件委托</title>
</head>
<body>
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item 4</li>
</ul>
<script>
const myUl = document.getElementsByTagName("ul")[0];
myUl.addEventListener("click", myUlFn);
function myUlFn(e) {
if (e.target.tagName.toLowerCase() === 'li') { // 判斷是否為所需要點擊的元素
console.log(`您點擊了${e.target.innerText}`);
}
}
</script>
</body>
</html>
⚠️ 這是一般的事件委托方法,但是這種寫法有問題,就是當_<li>_
中還有子元素時,點擊這個子元素就不會進行觸發事件。這個問題是一個坑。
事件冒泡有時候確實很有用,但是有時候也討人煩,當你不需要它的時候能不能取消掉呢?
禁止事件冒泡與捕獲
⚠️ 並不是所有事件都會冒泡,比如focus,blur,change,submit,reset,select等。
禁止冒泡和捕獲可以用到方法stopPropagation()
。
stopPropagation()起到阻止捕獲和冒泡階段中當前事件的進一步傳播。
這是阻止事件的冒泡方法,進行冒泡,但是默認事件任然會執行,當你調用了這個方法后。
如果點擊一個a
標簽,這個a
標簽會進行跳轉。
使用起來也很簡單,沒有返回值也沒有參數。
event.stopPropagation();
請看下面例子,這個例子實在上文事件冒泡例子基礎上稍加修改得到的
<div id="box1">
<div id="box2">
<div id="box3"></div>
</div>
</div>
<script>
const box1 = document.getElementById('box1')
const box2 = document.getElementById('box2')
const box3 = document.getElementById('box3')
box1.onclick = sayBox1;
box2.onclick = sayBox2;
box3.onclick = sayBox3;
function sayBox3() {
console.log('你點了最里面的box');
}
function sayBox2(e) {
console.log('你點了最中間的box');
e.stopPropagation(); //禁止事件捕獲和冒泡
}
function sayBox1() {
console.log('你點了最外面的box');
}
</script>
當事件冒泡到box2
時調用了在函數sayBox2
,調用了e.stopPropagation();
就停止冒泡了。
參考文獻
MDN中文版 https://developer.mozilla.org/zh-CN/
知乎 https://zhuanlan.zhihu.com/p/26536815