JavaScript事件流
0.DOM級別與DOM事件
首先在介紹DOM事件之前我們先來認識下DOM的不同級別。針對不同級別的DOM,我們的DOM事件處理方式也是不一樣的。
DOM級別一共可以分為4個級別:DOM0級,DOM1級,DOM2級和DOM3級,
而DOM事件分為3個級別:DOM0級事件處理,DOM2級事件處理和DOM3級事件處理。
如下圖所示:
其中1級DOM標準中並沒有定義事件相關的內容,所以沒有所謂的1級DOM事件模型。
1.事件
事件指可以被 JavaScript 偵測到的行為。即滑鼠點選、頁面或影像載入、滑鼠懸浮於頁面的某個熱點之上、在表單中選取輸入框、確認表單、鍵盤按鍵等操作。事件通常與函式配合使用,當事件發生時函式才會執行。
事件名稱:click/mouseover/blur("不帶on")響應某個事件的函式就是事件處理程式(事件偵聽器)。
事件處理程式函式名稱:onclick/onmouseove/onblur
例子程式碼--點選事件觸發alert函式
<button onclick="alert('hello')"></button>
更多事件類別請參考w3c中關於事件的詳細類別。
JavaScript 事件
JavaScript 事件參考手冊
2.事件流
事件流指從頁面中接收事件的順序,也可理解為事件在頁面中傳播的順序。
一點背景:
早期的IE事件傳播方向為由上至下,即從document逐級向下傳播到目標元素;
而Netscape公司的Netscape Navigator則是朝相反的方向傳播,也就是從目標元素開始向上逐級傳播最終至window。 兩家公司對於事件流出現了截然相反的定義。
後來ECMAScript在DOM2中對事件流進行了進一步規範,基本上就是上述二者的結合。
當事件發生時,最先得到通知的是window,然後是document,由上至下逐級依次而入,直到真正觸發事件的那個元素(目標元素)為止,這個過程就是捕獲。
接下來,事件會從目標元素開始起泡,由下至上逐級依次傳播,直到window物件為止,這個過程就是冒泡。
所以捕獲比冒泡先執行。
其中DOM3級事件在DOM2的基礎之上新增了更多的事件型別。
DOM2級事件規定的事件流包括三個階段:
(1)事件捕獲階段(2)處於目標階段(3)事件冒泡階段。
下面圖片來自:https://www.w3.org/TR/DOM-Lev...
我們寫一個例子:如下圖,中間白色區域的盒子分別為box1,box2...box6,包含控制按鈕設定我們的事件
<div>
<h4 id="currentBox">點選按鈕設定型別後再點選中心</h4>
<button class="btn" id="btnCapture" onclick="setCapture()">設定捕獲</button>
<button class="btn" id="btnBubble" onclick="setBubble()">設定冒泡</button>
<button class="btn" id="btnAll" onclick="setAll()">設定捕獲和冒泡</button>
<button class="btn" onclick="clearAll()">動畫完成後再清除設定</button>
</div>
<div class="box" id="box1">
<div class="box" id="box2">
<div class="box" id="box3">
<div class="box" id="box4">
<div class="box" id="box5">
<div class="box" id="box6">
點選
</div>
</div>
</div>
</div>
</div>
</div>
大概流程圖如下:
演示效果如圖:
3.事件處理程式
前面我們已經說到了,事件處理程式就是響應某個事件的函式,簡單地來說,就是函式。我們又把事件處理程式稱為事件偵聽器。事件處理程式是以"on"開頭的,比如點選事件的處理程式是"onclick",事件處理程式大概有以下5種。
- 1.HTML事件處理程式
- 2.DOM0級事件處理程式
- 3.DOM2級事件處理程式
- 4.IE事件處理程式
- 5.跨瀏覽器的事件處理程式
3.1 HTML事件處理程式
像我們的第一個例子,就是HTML事件處理程式,它是寫在html裡面的,是全域性作用域:
例子程式碼--點選事件觸發alert函式
<button onclick="alert('hello')"></button>
當我們需要使用一個複雜的函式時,將js程式碼寫在這裡面,顯然很不合適,所以有了下面這種寫法:
例子程式碼--點選事件觸發doSomething()函式,這個函式寫在單獨的js檔案或<script>之中。
<button onclick="doSomething()"></button>
這樣會出現一個時差問題,當使用者在HTML元素出現一開始就進行點選,有可能js還沒有載入好,這時候就會報錯。但我們可以將函式封裝在try-catch塊中來處理:
<button onclick="try{doSomething();}catch(err){}"></button>
同時,一個函式的改變,同時可能會涉及html和js的修改,這樣是很不方便的,綜上,我們有了DOM0級事件處理程式。
3.2 DOM0級事件處理程式
之所以有DOM0級事件處理程式,和我們之前提到的IE以及Netscape對應事件傳播方向不同處理而產生的事件處理程式。
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(){
alert("hello");
}
</script>
可以看到button.onclick這種形式,這裡事件處理程式作為了btn物件的方法,是區域性作用域。
所以我們可以用
btn.onclick = null;來刪除指定的事件處理程式。
如果我們嘗試給事件新增兩個事件,如:
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(){
alert("hello");
}
btn.onclick=function(){
alert("hello again");
}
</script>
輸出,hello again,很明顯,第一個事件函式被第二個事件函式給覆蓋掉了,所以,DOM0級事件處理程式不能新增多個,也不能控制事件流到底是捕獲還是冒泡。
3.3 DOM2級事件處理程式(不支援IE)
進一步規範之後,有了DOM2級事件處理程式,其中定義了兩個方法:
addEventListener() ---新增事件偵聽器
removeEventListener() ---刪除事件偵聽器
具體用法看
1.https://developer.mozilla.org...
2.https://developer.mozilla.org...
函式均有3個引數,
第一個引數是要處理的事件名(不帶on字首的才是事件名)
第二個引數是作為事件處理程式的函式
第三個引數是一個boolean值,預設false表示使用冒泡機制,true表示捕獲機制。
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
btn.addEventListener('click',hello,false);
btn.addEventListener('click',helloagain,false);
function hello(){
alert("hello");
}
function helloagain(){
alert("hello again");
}
</script>
這時候兩個事件處理程式都能夠成功觸發,說明可以繫結多個事件處理程式,但是注意,如果定義了一摸一樣時監聽方法,是會發生覆蓋的,即同樣的事件和事件流機制下相同方法只會觸發一次,比如:
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
btn.addEventListener('click',hello,false);
btn.addEventListener('click',hello,false);
function hello(){
alert("hello");
}
</script>
removeEventListener()的方法幾乎和新增時用法一摸一樣:
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
btn.addEventListener('click',hello,false);
btn.removeEventListener('click',hello,false);
function hello(){
alert("hello");
}
</script>
這樣的話,事件處理程式只會執行一次。
但是要注意,如果同一個監聽事件分別為“事件捕獲”和“事件冒泡”註冊了一次,一共兩次,這兩次事件需要分別移除。兩者不會互相干擾。
這時候的this指向該元素的引用。
這裡事件觸發的順序是新增的順序。
3.4 IE事件處理程式
對於 Internet Explorer 來說,在IE 9之前,你必須使用 attachEvent 而不是使用標準方法 addEventListener。
IE事件處理程式中有類似與DOM2級事件處理程式的兩個方法:
1.attachEvent()
2.detachEvent()
它們都接收兩個引數:
1.事件處理程式名稱。如onclick、onmouseover,注意:這裡不是事件,而是事件處理程式的名稱,所以有on。
2.事件處理程式函式。
之所以沒有和DOM2級事件處理程式中類似的第三個引數,是因為IE8及更早版本只支援冒泡事件流。
removeEventListener()的方法幾乎和新增時用法一摸一樣:
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
btn.attachEvent('onclick',hello);
btn.detachEvent('onclick',hello);
function hello(){
alert("hello");
}
</script>
這裡事件觸發的順序不是新增的順序而是新增順序的相反順序。
使用 attachEvent 方法有個缺點,this 的值會變成 window 物件的引用而不是觸發事件的元素。
3.5 跨瀏覽器的事件處理程式
為了相容IE瀏覽器和標準的瀏覽器,我們需要編寫通用的方法來處理:
var EventUtil = {
addHandler: function (element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
removeHandler: function (element, type, handler) {
if (element.removeEventListener()) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
}
};
這一部分需要建立兩個方法:
addHandler() --這個方法職責是視情況來使用DOM0級、DOM2級、IE事件處理程式來新增事件。
removeHandler()--這個方法就是移除使用addHandler新增的事件。
這兩個方法接收相同的三個引數:
1.要操作的元素--透過dom方法獲取
2.事件名稱--注意:沒有on,如"click"、"mouseover"
3.事件處理程式函式--對應的函式
使用:
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
EventUtil.addHandler(btn,'click',hello);
EventUtil.removeHandler(btn,'click',hello);
function hello(){
alert("hello");
}
</script>
4.事件物件
事件物件是用來記錄一些事件發生時的相關資訊的物件,但事件物件只有事件發生時才會產生,並且只能是事件處理函式內部訪問,在所有事件處理函式執行結束後,事件物件就被銷燬!
屬性和方法如圖,詳細請檢視以下連結:
1.HTML DOM Event 物件:http://www.w3school.com.cn/js...
2.詳細介紹請檢視:http://www.jb51.net/article/9...
4.1 屬性
下面是一個例子:
<button id="btn">點選</button>
<script>
var btn=document.getElementById("btn");
btn.ddEventListener('click', doCurrent, true);
// 判斷事件的屬性
function doCurrent(event) {
//獲取當前事件觸發的div
var target = event.currentTarget;
//透過判斷事件的event.eventPhase屬性返回事件傳播的當前階段
//1:捕獲階段、2:正常事件派發和3:起泡階段。
//得到當前階段和id值並輸出
var msg = (event.eventPhase == 1 ? '捕獲階段:' : '冒泡階段:')+ target.attributes["id"].value;;
console.log(msg);
}
</script>
在這個例子裡,我們用到了currentTarget、eventPhase 屬性。
4.2 方法
Event物件主要有以下兩個方法,用於處理事件的傳播(冒泡、捕獲)和事件的取消。
stopPropagation()——冒泡機制下,阻止事件的進一步往上冒泡
var btn1=document.getElementById("btn1");
var content=document.getElementById("content");
btn1.addEventListener("click",function(event){
alert("btn1");
event.stopPropagation();
},false);
content.addEventListener("click",function(){
alert("content");
},false);
//這裡會輸出btn1,阻止了向content的冒泡
preventDefault()——用於取消事件的預設操作,比如連結的跳轉或者表單的提交,主要是用來阻止標籤的預設行為
<a id="go" href="https://www.baidu.com/">禁止跳轉</a>
var go = document.getElementById('go');
function goFn(event) {
event.preventDefault();
// 不會跳轉
}
go.addEventListener('click', goFn, false);
4.3 相容性
當然,事件物件也存在一定的相容性問題,在IE8及以前本版之中,透過設定屬性註冊事件處理程式時,呼叫的時候並未傳遞事件物件,需要透過全域性物件window.event來獲取。解決方法如下:
function getEvent(event) {
event = event || window.event;
}
在IE瀏覽器上面是event事件是沒有preventDefault()這個屬性的,所以在IE上,我們需要設定的屬性是returnValue
window.event.returnValue=false
stopPropagation()也是,所以需要設定cancelBubble,cancelBubble是IE事件物件的一個屬性,設定這個屬性為true能阻止事件進一步傳播。
event.cancelBubble=true
5.事件委託
事件委託就是利用事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。
例子說明,我們為ul新增新的li,其中對li標籤元素繫結了click事件,但是發現,後增加的元素沒有辦法觸發我們的click事件。
<button id="btnAdd">新增</button>
<ul id="ulList">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var btnAdd = document.getElementById('btnAdd');
var ulList = document.getElementById('ulList');
var list = document.getElementsByTagName('li');
var num = 3;
btnAdd.onclick = function () {
num++;
var li = document.createElement('li');
li.innerHTML = num;
ulList.appendChild(li)
}
for (i = 0; i < list.length; i++) {
list[i].onclick = function(){
alert(this.innerHTML);
}
}
</script>
這是因為如果事件涉及到更新HTML節點或者新增HTML節點時,新新增的節點無法繫結事件,更新的節點也是無法繫結事件,表現的行為是無法觸發事件。
其中一種解決方法是,新增子節點的時候,再次為其新增監聽事件
<button id="btnAdd">新增</button>
<ul id="ulList">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var btnAdd = document.getElementById('btnAdd');
var ulList = document.getElementById('ulList');
var list = document.getElementsByTagName('li');
var num = 3;
function doclick() {
for (i = 0; i < list.length; i++) {
list[i].onclick = function () {
alert(this.innerHTML);
}
}
}
doclick();
btnAdd.onclick = function () {
num++;
var li = document.createElement('li');
li.innerHTML = num;
ulList.appendChild(li);
doclick();
}
</script>
這也是問題所在:
1.首先我們多次操作DOM獲取元素,這樣勢必會降低瀏覽器處理效能
2.事件不具有繼承性,如果我們動態在頁面中新增了一個元素,那麼還需要重新走一遍上述程式為其新增監聽事件
那麼有沒有更好的方法呢?根據事件的冒泡原理,我們還可以實現另外一個很重要的功能:事件委託。
我們只監聽最外層的元素,然後在事件函式中根據事件來源進行不同的事件處理。這樣,我們新增事件監聽時只需要操作一個元素,極大的降低了DOM訪問,並且不用再給新增的元素新增監聽事件了,因為元素的事件會冒泡到最外層,被我們截獲。
<button id="btnAdd">新增</button>
<ul id="ulList">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var btnAdd = document.getElementById('btnAdd');
var ulList = document.getElementById('ulList');
var num = 3;
ulList.onclick = function(event){
var event = event || window.event;
var target = event.target || event.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
alert(target.innerHTML);
}
}
btnAdd.onclick = function () {
num++;
var li = document.createElement('li');
li.innerHTML = num;
ulList.appendChild(li);
doclick();
}
</script>
這裡用父級ul做事件處理,當li被點選時,由於冒泡原理,事件就會冒泡到ul上,因為ul上有點選事件,所以事件就會觸發,當然,這裡當點選ul的時候,也是會觸發的,所以要判斷點選的物件到底是不是li標籤元素。
Event物件提供了一個屬性叫target,可以返回事件的目標節點,我們成為事件源,也就是說,target就可以表示為當前的事件操作的dom,但是不是真正操作dom,當然,這個是有相容性的,標準瀏覽器用ev.target,IE瀏覽器用event.srcElement,此時只是獲取了當前節點的位置,並不知道是什麼節點名稱,這裡我們用nodeName來獲取具體是什麼標籤名,這個返回的是一個大寫的,我們需要轉成小寫再做比較(習慣問題)。
這樣,我們就實現了我們的事件委託,當然,不是所有的事件都是可以委託的。
適合用事件委託的事件:click,mousedown,mouseup,keydown,keyup,keypress。
當用事件委託的時候,根本就不需要去遍歷元素的子節點,只需要給父級元素新增事件就好了,新增加的節點也可以觸發事件效果。
參考:
1.http://www.cnblogs.com/souven...
2.https://www.cnblogs.com/st-le...
3.https://segmentfault.com/a/11...
4.http://www.jb51.net/article/9...
5.http://www.w3school.com.cn/js...
6.http://www.jb51.net/article/8...
7.http://www.jb51.net/article/9...