理解DOM中的事件流的概念
什麼是事件流
流:用術語說流是對輸入輸出裝置的抽象。以程式的角度說,流是具有方向的資料。
事件流:從頁面中接收事件的順序。也就是說當一個事件產生時,這個事件的傳播過程,就是事件流。
事件:使用者或者瀏覽器自身執行的某個動作,比如load,click,mousemove等
事件處理程式:相應處理某個事件的函式叫做事件處理函式(也叫做事件偵聽器)
比如說React中的單向資料流,Node中的流,又或是今天本文所講的DOM事件流。都是流的一種生動體現。
理解DOM中的事件流
當瀏覽器發展到第四代時(IE4和Netscape Communicator 4),瀏覽器團隊遇到一個很有意思的問題:頁面的哪一部分會擁有特定的事件?想象下在一張紙上有一組同心圓,如果你把手指放在圓心上,那麼你的手指指向的不是一個圓,而是一組圓。兩家公司的開發團隊在看待瀏覽器事件方面還是一致的。如果你單擊了某個按鈕,那麼同時你也單擊了按鈕的容器元素,甚至整個頁面。
事件流描述的是從頁面中接受事件的順序。但有意思的是,IE和Netscape開發團隊居然提出了兩個截然相反的事件流概念。IE的事件流是事件冒泡流,而Netscape的事件流是事件捕獲流。
IE提出的事件冒泡
事件冒泡即事件開始時,由最具體的元素接收(也就是事件發生所在的節點),然後逐級傳播到較為不具體的節點。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<button id="click">點選</button>
<script>
(function (){
var btn = document.getElementById("click");
btn.onclick = function(){
console.log("1. button");
}
document.body.onclick = function(){
console.log("2. document.body");
}
document.onclick = function (){
console.log("3. document");
}
window.onclick = function(){
console.log("4. window");
}
})()
</script>
</body>
</html>
在程式碼所示的頁面中,如果點選了button,那麼這個點選事件會得到如下的結果:
也就是說,click事件首先在button元素上發生,然後逐級向上傳播。這就是事件冒泡。
netscape提出的事件捕獲
事件捕獲的概念,與事件冒泡正好相反。它認為當某個事件發生時,父元素應該更早接收到事件,具體元素則最後接收到事件。比如說剛才的demo,如果是事件捕獲的話,事件發生順序會是剛好與上面相反的。即window,document,document.body,button。
雖然事件捕獲是Netscape唯一支援的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也都支援這種事件流模型。但由於老版本的瀏覽器不支援,因此很少有人使用事件捕獲。
所以放心的使用事件冒泡,有特殊需要再使用事件捕獲即可。
DOM事件流
DOM事件流可以分為下面3個階段:
1. 事件捕獲階段
2. 處於目標階段
3. 事件冒泡階段
事件捕獲階段
也就是說,當事件發生時,首先發生的是事件捕獲,為父元素截獲事件提供了機會。
例如,我把上面的Demo中,window點選事件更改為使用事件捕獲模式。
addEventListener最後一個引數,為true則代表使用事件捕獲模式,false則表示使用事件冒泡模式。
<script>
(function(){
var btn = document.getElementById("click");
btn.addEventListener("click",function(){
console.log("1. button");
},true)
//省略document.body和document
.....
window.addEventListener("click",function(){
console.log("4. window");
},true)
})()
</script>
結果如下:
可以看到,點選事件先被父元素截獲了,且該函式只在事件捕獲階段起作用。
在DOM事件流中,事件的目標在捕獲階段不會接受到事件。這意味著在捕獲階段,事件從document到body後就定停止了。下一個階段是處於目標階段,於是事件在button上發生,並在事件處理中被看成冒泡階段的一部分。然後,冒泡階段發生,事件又傳播回document。
但是:我們的各大瀏覽器總是不喜歡按照規範來,IE9,Safari,chrome,firefox及其更高的版本中都會在捕獲階段出發事件物件上的事件,最後導致有兩個機會在目標物件上操作事件。
處於目標與事件冒泡階段
事件到了具體元素時,在具體元素上發生,並且被看成冒泡階段的一部分。
隨後,冒泡階段發生,事件開始冒泡。
阻止事件冒泡
件冒泡過程,是可以被阻止的。防止事件冒泡而帶來不必要的錯誤和困擾。
這個方法就是:stopPropagation()
(function(){
var btn = document.getElementById("click");
btn.addEventListener("click",function(event){
console.log("1. button");
event.stopPropagation();
console.log('Stop Propagation!');
},false)
//省略document.body和document
.....
window.addEventListener("click",function(){
console.log("4. window");
},false)
})()
最後結果是:1.button,Stop Propagation!。通過stopPropagation();阻止了事件的冒泡。
事件處理程式類別
剛剛我們已經講了事件處理程式就是相應處理某個事假的函式。它可以分為幾個類別:
html事件處理程式
某個元素支援的某個事件可以用與事件處理程式同名的html特性來指定,該特性的值是能夠執行的javascript程式碼,這也是我們最初學js,最開始的方法。
<script>
function show(){
alert('我被點選了');
}
/*
點選後也會彈出 '我被點選了'
*/
</script>
<input type="button" value="點選" onclick="show()" />
優點:簡單明瞭,省去獲取元素等一系列前提操作
缺點:html程式碼與js程式碼高度耦合,不符合分離原則
DOM0級別事件處理函式
DOM0級別事件處理函式,使用 element.on[eventname]=fn的方式給元素新增事件
<input type="button" value="點選" id="click" />
<script>
var oBtn=document.getElementById('click');
//該方式被認為是元素的方法,即事件處理程式在元素的作用域中進行,this即該元素本身
oBtn.onclick=function(){
alert(this.id);//click
}
//注意:刪除該事件處理程式可以用如下方法
oBtn.onclick=null;//即點選後不再有任何反應
</script>
DOM2級事件處理程式
DOM2級添加了addEventListener(新增事件處理程式)和removeEventListener(移除事件處理程式),也就是我們剛剛講的上面的DOM2例子。
新增事件處理函式addEventListener
引數1 指定事件名稱...click mouseover mouseout
引數2 事件處理程式(匿名函式或者有名函式)
引數3 true(捕獲階段發生) or false(冒泡階段發生)
<input type="button" value="點選" id="click" />
<script>
var oBtn=document.getElementById('click');
oBtn.addEventListener('click',function(){
alert(this.id)//click this指的是該元素作用域內
},false)
//注意該種方式可以給一個函式新增多個事件處理函式,執行順序與新增順序相同
oBtn.addEventListener('click',function(){
alert('Hello World')//click
},false)
</script>
移除事件處理函式removeEventListener
如果事件處理函式是有名函式,則可以通過名字來移除,匿名函式無法移除。
<input type="button" value="點選" id="click" />
<script>
var oBtn=document.getElementById('click');
function showId(){
alert(this.id);
};
function HelloWorld(){
alert('HellowWorld');
}
oBtn.addEventListener("click",showId,false);
oBtn.addEventListener("click",HelloWorld,false);
oBtn.removeEventListener('click',showId,false)
</script>
最後只能彈出HellowWorld
IE事件處理程式attachEvent,detachEvent
ie實現了與dom類似的兩個方法,attachEvent(新增),detachEvent(刪除)
oBtn.attachEvent('onclick',showId);//這時候會報錯,因為這裡的是在window的作用域內
//修改如下
oBtn.detachEvent('onclick',showId) ;//點選沒有任何反應