JavaScript、瀏覽器、事件之間的關係
JavaScript程式採用了非同步事件驅動程式設計(Event-driven programming)模型,維基百科對它的解釋是:
事件驅動程式設計(Event-driven programming)是一種電腦程式設計模型。這種模型的程式執行流程是由使用者的動作(如滑鼠的按鍵,鍵盤的按鍵動作)或者是由其他程式的訊息來決定的。相對於批處理程式設計(batch programming)而言,程式執行的流程是由程式設計師來決定。批量的程式設計在初級程式設計教學課程上是一種方式。然而,事件驅動程式設計這種設計模型是在互動程式(Interactive program)的情況下孕育而生的
簡言之,在web前端程式設計裡面JavaScript通過瀏覽器提供的事件模型API和使用者互動,接收使用者的輸入。
由於使用者的行為是不確定的。這種場景是傳統的同步程式設計模型沒法解決的,因為你不可能等使用者操作完了才執行後面的程式碼。所以在javascript中使用了非同步事件,也就是說:js中的事件都是非同步執行的。
事件驅動程式模型基本的實現原理基本上都是使用 事件迴圈(Event Loop),這部分內容涉及瀏覽器事件模型、回撥原理。
JavaScript DOM、BOM模型中,同樣非同步的還有setTimeout,XMLHTTPRequest這類API並不是JavaScript語言本身就有的。
事件繫結的方法
事件繫結有3種方法:
行內繫結
直接在DOM元素上通過設定on + eventType
繫結事件處理程式。例如:
<a href="#none" onclick="alert(`clicked`)">點選我</a>
這種方法有兩個缺點:
- 事件處理程式和HTML結構混雜在一起,不符合MVX的規範。為了讓內容、表現和行為分開,我們應該避免這種寫法。
- 這樣寫的程式碼判斷具有全域性作用域,可能會產生命名衝突,導致不可預見的嚴重的後果。
在DOM元素上直接重寫事件回撥函式
使用DOM Element上面的on + eventType屬性 API
var el = getElementById(`button`); //button是一個<button>元素
el.onclick = function(){ alert(`button clicked.`) };
el.onclick = function(){ alert(`Button Clicked.`) };
//實際之彈出`Button Clicked.`,函式發生了覆蓋
這種方法也有一個缺點:後繫結的函式會覆蓋之前的函式。比如我們註冊一個window.onload事件,可能會覆蓋某個庫中已有的事件函式。當然,這個可以有解決方法:
function addEvent(element, EventName, fun) { //EventName = `on` + eventType
var oldFun = element[EventName];
if (typeof oldFun !== `function`) {
element[EventName] = fun;
} else {
element[EventName] = function() {
oldFun();
fun();
};
}
}
addEvent(window, "onload", function() { alert(`onload 1`) });
addEvent(window, "onload", function() { alert(`onload 2`) });
當然,一般情況下使用DOM Ready就可以了,因為JavaScript在DOM載入完就可以執行了
標準繫結方法
標準的繫結方法有兩種,addEventListener和attachEvent前者是標準瀏覽器支援的API,後者是IE8以下瀏覽器支援的API:
//例如給一個button註冊click事件
var el = getElementById(`button`); //button是一個<button>元素
if(el.addEventLister){
el.addEventListener("click", function(e){
alert("button clicked.");
},false);
}
if(el.attachEvent){
el.attachEvent("onclick", function(e){
alert("button clicked.");
});
}
需要注意的是:
- addEventLister的第一個引數事件型別是不加on字首的,而attachEvent中需要加on字首。
- addEventLister中的事件回撥函式中的this指向事件元素target本身,而attachEvent中的事件回撥函式的this指向的是window。
- addEventLister有第三個引數,true表示事件工作在捕獲階段,false為冒泡階段(預設值:false)。而attachEvent只能工作在冒泡階段。
在chrome中執行如下程式碼:
<a href="javascript:alert(1)" onclick="alert(2)" id="link">click me</a>
<script>
var link = document.getElementById(`link`);
link.onclick = function() { alert(3); }; //覆蓋了行內的onclick定義
link.addEventListener(`click`, function() { alert(4); },false);
link.addEventListener(`click`, function() { alert(5); },false);
</script>
點選後彈出順序是: 3 -> 4 -> 5 -> 1
這裡第4行程式碼覆蓋了行內的onclick定義,如果註釋了這一行,輸入順序為: 2 -> 4 -> 5 -> 1,而addEventListener之間不會發生覆蓋。
解除事件繫結
對於上述的前二個方法,解除事件繫結只需要將對應的事件函式設為null,就可以了:
var el = document.getElementById(`button`);
el.onclick = null;
對於上述第三種方法使用removeListen()方法即可,在IE8中,對應使用detachEvent()。注意,他們和上面的註冊方法一一對應,不能混用。
//這是一段錯誤程式碼,不能實現事件移除
//建立一個事件
var el = document.getElementById(`button`); //button是一個<button>元素
if(el.addEventLister){
el.addEventListener("click", function(e){
alert("button clicked.");
},false);
}
if(el.attachEvent){
el.attachEvent("onclick", function(e){
alert("button clicked.");
});
}
//試圖移除這個事件
if(el.removeEventLister){
el.addEventListener("click", function(e){
alert("button clicked.");
},false);
}
if(el.detachEvent){
el.datachEvent("onclick", function(e){
alert("button clicked.");
});
}
//移除失敗
以上的錯誤在於事件函式這樣定義時,雖然看著完全一樣,但在記憶體中地址不一樣。這樣一來,電腦不會認為解除的和繫結的是同一個函式,自然也就不會正確解除。應該這樣寫:
//建立一個事件
var el = document.getElementById(`button`); //button是一個<button>元素
var handler = function(e){alert("button clicked.");};
if(el.addEventLister){
el.addEventListener("click", handler,false);
}
if(el.attachEvent){
el.attachEvent("onclick", handler);
}
//試圖移除這個事件
if(el.removeEventLister){
el.addEventListener("click", handler, false);
}
if(el.detachEvent){
el.datachEvent("onclick", handler);
}
//移除成功
事件的捕獲與冒泡
之前說addEventListener函式的第三個參數列示捕獲和冒泡,這個是一個重點!
我自己描述一下他們的定義就是:
冒泡:在一個元素上觸發的某一事件,會在這個元素的父輩元素上會依次由內向外觸發該事件,直到window元素。
捕獲:在一個元素上觸發的某一事件,這個元素的每一層的所有子元素上觸發該事件,並逐層向內,直到所有元素不再有子元素。
如下圖(注:圖片來自百度搜尋)
事件間回到函式引數是一個事件物件,它裡面包括許多事件屬性和方法,比如,我們可以用以下方式阻止冒泡和預設事件:
//該例子只寫了handler函式
function handler(event) {
event = event || window.event;
//阻止冒泡
if (event.stopPropagation) {
event.stopPropagation(); //標準方法
} else {
event.cancelBubble = true; // IE8
}
//組織預設事件
if (event.perventDefault) {
event.perventDefault(); //標準方法
} else {
event.returnValue = false; // IE8
}
}
其次,普通註冊事件只能阻止預設事件,不能阻止冒泡
element = document.getElemenById("submit");
element.onclick = function(e){
/*...*/
return false; //通過返回false,阻止冒泡
}
事件物件
事件函式中有一個引數是事件物件,它包含了事件發生的所有資訊,比如鍵盤時間會包括點選了什麼按鍵,包括什麼組合鍵等等,而滑鼠事件會包括一系列螢幕中的各種座標和點選型別,甚至拖拽等等。當然,它裡面也會包括很多DOM資訊,比如點選了什麼元素,拖拽進入了什麼元素,事件的當前狀態等等。
這裡關於事件相容性有必要強調一下:
document.addEventListener(`click`, function(event) {
event = event || window.event; //該物件是註冊在window上的
console.log(event); //可以輸出事件物件看一看, 屬性很多很多
var target = event.target || event.srcElement; //前者是標準事件目標,後者是IE的事件目標
},false);
關於滑鼠事件座標的問題,可以看另一篇部落格:元素和滑鼠事件的距離屬性
事件觸發
除了使用者操作以外,我們也可以寫程式碼主動觸發一個事件,以ele元素的click事件為例:
ele.click(); //觸發ele元素上的單擊事件
事件代理
有時候我們需要給不存在的的一段DOM元素繫結事件,比如使用者動態新增的元素,或者一段 Ajax 請求完成後渲染的DOM節點。一般繫結事件的邏輯會在渲染前執行,但繫結的時候找不到元素所以並不能成功。
為了解決這個問題,我們通常使用事件代理/委託(Event Delegation)。而且通常來說使用 事件代理的效能會比單獨繫結事件高很多,我們來看個例子。
- 傳統註冊事件方法,當內容很多時效率低,不支援動態新增元素
<ul id="list">
<li>item-1</li>
<li>item-2</li>
<li>item-3</li>
<li>item-4</li>
<li>item-5</li>
</ul>
<script>
var lists = document.getElementsByTagName(`li`);
for(var i = 0; i < lists.length; ++i){
lists[i].onclick = (function(i){
return function(){
console.log("item-" + (i + 1));
};
})(i);
}
//新增節點
var list = document.getElementById(`list`);
var newNode = document.createElement(`li`);
newNode.innerHTML = "item-6";
list.appendChild(newNode);
</script>
- 事件委託註冊方法,不論內容有多少都只註冊1次,支援動態新增元素:
<ul id="list">
<li>item-1</li>
<li>item-2</li>
<li>item-3</li>
<li>item-4</li>
<li>item-5</li>
</ul>
<script>
var list = document.getElementById(`list`);
var handler = function(e){
e = e || window.event;
var target = e.target || e.srcElement;
if(target.nodeName && target.nodeName === "LI"){
console.log(target.innerHTML);
}
};
if(list.addEventListener){
list.addEventListener("click", handler);
} else {
list.attachEvent("onclick", handler);
}
//新增節點
var list = document.getElementById(`list`);
var newNode = document.createElement(`li`);
newNode.innerHTML = "item-6";
list.appendChild(newNode);
</script>
事件封裝
很明顯,處理瀏覽器相容太麻煩了,所以這裡把js中的事件註冊相關函式封裝一下,作為整理。
//均採用冒泡事件模型
var myEventUtil={
//新增事件函式
addEvent: function(ele, event, func){
var target = event.target || event.srcElement;
if(ele.addEventListener){
ele.addEventListener(event, func, false);
} else if(ele.attachEvent) {
ele.attachEvent(`on` + event, func); //func中this是window
} else {
ele[`on` + event] = func; //會發生覆蓋
}
},
//刪除事件函式
delEvent:function(ele, event, func) {
if(ele.removeEventListener){
ele.removeEventListener(event, func, false);
} else if(ele.detachEvent) {
ele.detachEvent(`on` + event, func);
} else {
ele[`on` + event] = null;
}
},
//獲取觸發事件的源DOM元素
getSrcElement: function(event){
return event.target || event.srcElement;
},
//獲取事件型別
getType: function(event){
return event.type;
},
//獲取事件
getEvent:function(event){
return event || window.event;
},
//阻止事件冒泡
stopPropagation: function(event) {
if(event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBuble = false;
}
},
//禁用預設行為
preventDefault: function(event){
if(event.preventDefault){
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
jQuery中的事件
需要注意的是: JQuery中的事件都工作在冒泡階段,且只能工作在冒泡階段
註冊、解除事件
- 方法一:
//不會發生覆蓋,但不利於解除,不能動態操作事件
<button id="button">here</button>
$("#button").click(function(){ //註冊一個click事件,當然可以用其他事件名的函式註冊其他事件
console.log("clicked");
});
- 方法二:
//不會發生覆蓋,利於解除,不能動態操作事件
<button id="button">here</button>
//註冊一個事件
$("#button").bind("click", function() { //註冊一個click事件,當然可以用其他事件名的函式註冊其他事件
console.log("clicked");
});
//當然還可以這樣寫,給事件指定名稱空間
$(document).bind(`click.handler1`, function() { console.log(1);})
$(document).bind(`click.handler2`, function() { console.log(2);})
//解除一個事件
$("#button").unbind(".handler1"); //解除元素上所以handler1名稱空間中的事件
$("#button").unbind(`click.handler2`); // 解除元素上的click.handler2事件
$("#button").unbind(`click`); // 解除元素上所有點選事件
$("#button").unbind() // 解除元素上所有事件
//bind()方法還介受3個引數形式,這裡就不贅述了,感興趣可以自己看看相關資料。
- 方法三:
//不會發生覆蓋,但不利於解除,能動態操作事件,依賴於事件冒泡
//註冊事件
$(document).delegate(".item", "click", function(){console.log(this.innerHTML);}); //第一個是選擇器, 第二個是事件型別, 第三個是事件函式
//移除事件
$(document).undelegate(".item", "click", handler); //移除元素上指定事件
$(document).undelegate(".item", "click"); //移除元素上所有click事件
$(document).undelegate(".item"); //移除元素上所有事件
- 方法四:
//不會發生覆蓋,但不利於解除,能動態操作事件,不依賴於事件冒泡
//註冊事件
#(".item").live("click", function(){console.log(this.innerHTML);}) //第一引數是事件型別, 第二引數是事件函式
//移除事件
$(".item").die("click", handler); //移除元素上指定click事件
$(".item").die("click"); //移除元素上所有click事件
- 兩個簡化方法:
//hover方法
$("#button").hover(function(){
//滑鼠移入時的動作,不冒泡
}, function(){
//滑鼠移出時的動作,不冒泡
});
//toggle方法
$("#button").toggle(function(){
//第一次點選時的動作
}, function(){
//第二次點選時的動作
}, .../*可以放多個函式,依次迴圈響應*/);
事件觸發
//不能觸發addEventListener和attachEvent
//主動觸發一個事件
$("#button").trigger("click"); //觸發所有click事件
$("#button").trigger("click.handler1"); //觸發所有click.handler1事件
$("#button").trigger(".handler1"); //觸發所有handler1名稱空間的事件
$("#button").trigger("click!"); //觸發所有沒有名稱空間的click事件
$("#button").trigger(event); //在該元素上觸發和事件event一樣的事件
$("#button").trigger({type:"click", sync: true}); //觸發click事件,同步