二十二、事件綁定及深入
二十二、事件綁定及深入
事件綁定分為兩種:一種是傳統事件綁定(內聯模型,腳本模型),一種是現代事件綁定(DOM2級模型)。現代事件綁定在傳統綁定上提供了更強大更方便的功能。
1.傳統事件綁定的問題
傳統事件綁定有內聯模型和腳本模型,內聯模型我們不做討論,基本很少去用。先來看一下腳本模型,腳本模型將一個函數賦值給一個事件處理函數。
var box = document.getElementById(‘box‘); //獲取元素
box.onclick = function () { //元素點擊觸發事件
alert(‘Lee‘);
};
問題一:一個事件處理函數觸發兩次事件
window.onload = function () { //第一組程序項目或第一個JS文件
alert(‘Lee‘);
};
window.onload = function () { //第二組程序項目或第二個JS文件
alert(‘Mr.Lee‘);
};
當兩組程序或兩個JS文件同時執行的時候,後面一個會把前面一個完全覆蓋掉。導致前面的window.onload完全失效了。
解決覆蓋問題,我們可以這樣去解決:
window.onload = function () { //第一個要執行的事件,會被覆蓋
alert(‘Lee‘);
};
if (typeof window.onload == ‘function‘) { //判斷之前是否有window.onload
var saved = null; //創建一個保存器
saved = window.onload; //把之前的window.onload保存起來
}
window.onload = function () { //最終一個要執行事件
if (saved) saved(); //執行之前一個事件
alert(‘Mr.Lee‘); //執行本事件的代碼
};
問題二:事件切換器
box.onclick = toBlue; //第一次執行boBlue()
function toRed() {
this.className = ‘red‘;
this.onclick = toBlue; //第三次執行toBlue(),然後來回切換
}
function toBlue() {
this.className = ‘blue‘;
this.onclick = toRed; //第二次執行toRed()
}
這個切換器在擴展的時候,會出現一些問題:
1.如果增加一個執行函數,那麽會被覆蓋
box.onclick = toAlert; //被增加的函數
box.onclick = toBlue; //toAlert被覆蓋了
2.如果解決覆蓋問題,就必須包含同時執行,但又出新問題
box.onclick = function () { //包含進去,但可讀性降低
toAlert(); //第一次不會被覆蓋,但第二次又被覆蓋
toBlue.call(this); //還必須把this傳遞到切換器裏
};
綜上的三個問題:覆蓋問題、可讀性問題、this傳遞問題。我們來創建一個自定義的事件處理函數,來解決以上三個問題。
function addEvent(obj, type, fn) { //取代傳統事件處理函數
var saved = null; //保存每次觸發的事件處理函數
if (typeof obj[‘on‘ + type] == ‘function‘) { //判斷是不是事件
saved = obj[‘on‘ + type]; //如果有,保存起來
}
obj[‘on‘ + type] = function () { //然後執行
if (saved) saved(); //執行上一個
fn.call(this); //執行函數,把this傳遞過去
};
}
addEvent(window, ‘load‘, function () { //執行到了
alert(‘Lee‘);
});
addEvent(window, ‘load‘, function () { //執行到了
alert(‘Mr.Lee‘);
});
PS:以上編寫的自定義事件處理函數,還有一個問題沒有處理,就是兩個相同函數名的函數誤註冊了兩次或多次,那麽應該把多余的屏蔽掉。那,我們就需要把事件處理函數進行遍歷,如果有同樣名稱的函數名就不添加即可。(這裏就不做了)
addEvent(window, ‘load‘, init); //註冊第一次
addEvent(window, ‘load‘, init); //註冊第二次,應該忽略
function init() {
alert(‘Lee‘);
}
用自定義事件函數註冊到切換器上查看效果:
addEvent(window, ‘load‘, function () {
var box = document.getElementById(‘box‘);
addEvent(box, ‘click‘, toBlue);
});
function toRed() {
this.className = ‘red‘;
addEvent(this, ‘click‘, toBlue);
}
function toBlue() {
this.className = ‘blue‘;
addEvent(this, ‘click‘, toRed);
}
PS:當你單擊很多很多次切換後,瀏覽器直接卡死,或者彈出一個錯誤:too much recursion(太多的遞歸)。主要的原因是,每次切換事件的時候,都保存下來,沒有把無用的移除,導致越積越多,最後卡死。
function removeEvent(obj, type) {
if (obj[‘on‘] + type) obj[‘on‘ + type] = null; //刪除事件處理函數
}
以上的刪除事件處理函數只不過是一刀切的刪除了,這樣雖然解決了卡死和太多遞歸的問題。但其他的事件處理函數也一並被刪除了,導致最後得不到自己想要的結果。如果想要只刪除指定的函數中的事件處理函數,那就需要遍歷,查找。(這裏就不做了)
2.W3C事件處理函數
“DOM2級事件”定義了兩個方法,用於添加事件和刪除事件處理程序的操作:addEventListener()和removeEventListener()。所有DOM節點中都包含這兩個方法,並且它們都接受3個參數;事件名、函數、冒泡或捕獲的布爾值(true表示捕獲,false表示冒泡)。
window.addEventListener(‘load‘, function () {
alert(‘Lee‘);
}, false);
window.addEventListener(‘load‘, function () {
alert(‘Mr.Lee‘);
}, false);
PS:W3C的現代事件綁定比我們自定義的好處就是:1.不需要自定義了;2.可以屏蔽相同的函數;3.可以設置冒泡和捕獲。
window.addEventListener(‘load‘, init, false); //第一次執行了
window.addEventListener(‘load‘, init, false); //第二次被屏蔽了
function init() {
alert(‘Lee‘);
}
事件切換器
window.addEventListener(‘load‘, function () {
var box = document.getElementById(‘box‘);
box.addEventListener(‘click‘, function () { //不會被誤刪
alert(‘Lee‘);
}, false);
box.addEventListener(‘click‘, toBlue, false); //引入切換也不會太多遞歸卡死
}, false);
function toRed() {
this.className = ‘red‘;
this.removeEventListener(‘click‘, toRed, false);
this.addEventListener(‘click‘, toBlue, false);
}
function toBlue() {
this.className = ‘blue‘;
this.removeEventListener(‘click‘, toBlue, false);
this.addEventListener(‘click‘, toRed, false);
}
設置冒泡和捕獲階段
之前我們上一章了解了事件冒泡,即從裏到外觸發。我們也可以通過event對象來阻止某一階段的冒泡。那麽W3C現代事件綁定可以設置冒泡和捕獲。
document.addEventListener(‘click‘, function () {
alert(‘document‘);
}, true); //把布爾值設置成true,則為捕獲
box.addEventListener(‘click‘, function () {
alert(‘Lee‘);
}, true); //把布爾值設置成false,則為冒泡
3.IE事件處理函數
IE實現了與DOM中類似的兩個方法:attachEvent()和detachEvent()。這兩個方法接受相同的參數:事件名稱和函數。
在使用這兩組函數的時候,先把區別說一下:1.IE不支持捕獲,只支持冒泡;2.IE添加事件不能屏蔽重復的函數;3.IE中的this指向的是window而不是DOM對象。4.在傳統事件上,IE是無法接受到event對象的,但使用了attchEvent()卻可以,但有些區別。
window.attachEvent(‘onload‘, function () {
var box = document.getElementById(‘box‘);
box.attachEvent(‘onclick‘, toBlue);
});
function toRed() {
var that = window.event.srcElement;
that.className = ‘red‘;
that.detachEvent(‘onclick‘, toRed);
that.attachEvent(‘onclick‘, toBlue);
}
function toBlue() {
var that = window.event.srcElement;
that.className = ‘blue‘;
that.detachEvent(‘onclick‘, toBlue);
that.attachEvent(‘onclick‘, toRed);
}
PS:IE不支持捕獲,無解。IE不能屏蔽,需要單獨擴展或者自定義事件處理。IE不能傳遞this,可以call過去。
window.attachEvent(‘onload‘, function () {
var box = document.getElementById(‘box‘);
box.attachEvent(‘onclick‘, function () {
alert(this === window); //this指向的window
});
});
window.attachEvent(‘onload‘, function () {
var box = document.getElementById(‘box‘);
box.attachEvent(‘onclick‘, function () {
toBlue.call(box); //把this直接call過去
});
});
function toThis() {
alert(this.tagName);
}
在傳統綁定上,IE是無法像W3C那樣通過傳參接受event對象,但如果使用了attachEvent()卻可以。
box.onclick = function (evt) {
alert(evt); //undefined
}
box.attachEvent(‘onclick‘, function (evt) {
alert(evt); //object
alert(evt.type); //click
});
box.attachEvent(‘onclick‘, function (evt) {
alert(evt.srcElement === box); //true
alert(window.event.srcElement === box); //true
});
最後,為了讓IE和W3C可以兼容這個事件切換器,我們可以寫成如下方式:
function addEvent(obj, type, fn) { //添加事件兼容
if (obj.addEventListener) {
obj.addEventListener(type, fn);
} else if (obj.attachEvent) {
obj.attachEvent(‘on‘ + type, fn);
}
}
function removeEvent(obj, type, fn) { //移除事件兼容
if (obj.removeEventListener) {
obj.removeEventListener(type, fn);
} else if (obj.detachEvent) {
obj.detachEvent(‘on‘ + type, fn);
}
}
function getTarget(evt) { //得到事件目標
if (evt.target) {
return evt.target;
} else if (window.event.srcElement) {
return window.event.srcElement;
}
}
PS:調用忽略,IE兼容的事件,如果要傳遞this,改成call即可。
PS:IE中的事件綁定函數attachEvent()和detachEvent()可能在實踐中不去使用,有幾個原因:1.IE9就將全面支持W3C中的事件綁定函數;2.IE的事件綁定函數無法傳遞this;3.IE的事件綁定函數不支持捕獲;4.同一個函數註冊綁定後,沒有屏蔽掉;5.有內存泄漏的問題。至於怎麽替代,我們將在以後的項目課程中探討。
4.事件對象的其他補充
在W3C提供了一個屬性:relatedTarget;這個屬性可以在mouseover和mouseout事件中獲取從哪裏移入和從哪裏移出的DOM對象。
box.onmouseover = function (evt) { //鼠標移入box
alert(evt.relatedTarget); //獲取移入box最近的那個元素對象
} //span
box.onmouseout = function (evt) { //鼠標移出box
alert(evt.relatedTarget); //獲取移出box最近的那個元素對象
} //span
IE提供了兩組分別用於移入移出的屬性:fromElement和toElement,分別對應mouseover和mouseout。
box.onmouseover = function (evt) { //鼠標移入box
alert(window.event.fromElement.tagName); //獲取移入box最近的那個元素對象span
}
box.onmouseout = function (evt) { //鼠標移入box
alert(window.event.toElement.tagName); //獲取移入box最近的那個元素對象span
}
PS:fromElement和toElement如果分別對應相反的鼠標事件,沒有任何意義。
剩下要做的就是跨瀏覽器兼容操作:
function getTarget(evt) {
var e = evt || window.event; //得到事件對象
if (e.srcElement) { //如果支持srcElement,表示IE
if (e.type == ‘mouseover‘) { //如果是over
return e.fromElement; //就使用from
} else if (e.type == ‘mouseout‘) { //如果是out
return e.toElement; //就使用to
}
} else if (e.relatedTarget) { //如果支持relatedTarget,表示W3C
return e.relatedTarget;
}
}
有時我們需要阻止事件的默認行為,比如:一個超鏈接的默認行為就點擊然後跳轉到指定的頁面。那麽阻止默認行為就可以屏蔽跳轉的這種操作,而實現自定義操作。
取消事件默認行為還有一種不規範的做法,就是返回false。
link.onclick = function () {
alert(‘Lee‘);
return false; //直接給個假,就不會跳轉了。
};
PS:雖然return false;可以實現這個功能,但有漏洞;第一:必須寫到最後,這樣導致中間的代碼執行後,有可能執行不到return false;第二:return false寫到最前那麽之後的自定義操作就失效了。所以,最好的方法應該是在最前面就阻止默認行為,並且後面還能執行代碼。
link.onclick = function (evt) {
evt.preventDefault(); //W3C,阻止默認行為,放哪裏都可以
alert(‘Lee‘);
};
link.onclick = function (evt) { //IE,阻止默認行為
window.event.returnValue = false;
alert(‘Lee‘);
};
跨瀏覽器兼容
function preDef(evt) {
var e = evt || window.event;
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
}
上下文菜單事件:contextmenu,當我們右擊網頁的時候,會自動出現windows自帶的菜單。那麽我們可以使用contextmenu事件來修改我們指定的菜單,但前提是把右擊的默認行為取消掉。
addEvent(window, ‘load‘, function () {
var text = document.getElementById(‘text‘);
addEvent(text, ‘contextmenu‘, function (evt) {
var e = evt || window.event;
preDef(e);
var menu = document.getElementById(‘menu‘);
menu.style.left = e.clientX + ‘px‘;
menu.style.top = e.clientY + ‘px‘;
menu.style.visibility = ‘visible‘;
addEvent(document, ‘click‘, function () {
document.getElementById(‘myMenu‘).style.visibility = ‘hidden‘;
});
});
});
PS:contextmenu事件很常用,這直接導致瀏覽器兼容性較為穩定。
卸載前事件:beforeunload,這個事件可以幫助在離開本頁的時候給出相應的提示,“離開”或者“返回”操作。
addEvent(window, ‘beforeunload‘, function (evt) {
preDef(evt);
});
鼠標滾輪(mousewheel)和DOMMouseScroll,用於獲取鼠標上下滾輪的距離。
addEvent(document, ‘mousewheel‘, function (evt) { //非火狐
alert(getWD(evt));
});
addEvent(document, ‘DOMMouseScroll‘, function (evt) { //火狐
alert(getWD(evt));
});
function getWD(evt) {
var e = evt || window.event;
if (e.wheelDelta) {
return e.wheelDelta;
} else if (e.detail) {
return -evt.detail * 30; //保持計算的統一
}
}
PS:通過瀏覽器檢測可以確定火狐只執行DOMMouseScroll。
二十二、事件綁定及深入