javascript打造跨瀏覽器事件處理機制[Blue-Dream出品]
首先,DOM Level2為事件處理定義了兩個函數(shù)addEventListener和removeEventListener, 這兩個函數(shù)都來自于EventTarget接口.
element.addEventListener(eventName, listener, useCapture);
element.removeEventListener(eventName, listener, useCapture);
EventTarget接口通常實現(xiàn)自Node或Window接口.也就是所謂的DOM元素.
那么比如window也就可以通過addEventListener來添加監(jiān)聽.
function loadHandler() {
console.log('the page is loaded!');
}
window.addEventListener('load', loadHandler, false);
移除監(jiān)聽通過removeEventListener同樣很容易做到, 只要注意移除的句柄和添加的句柄引用自一個函數(shù)就可以了.
window.removeEventListener('load', loadHandler, false);
如果我們活在完美世界.那么估計事件函數(shù)就此結(jié)束了.
但情況并非如此.由于IE獨樹一幟.通過MSDHTML DOM定義了attachEvent和detachEvent兩個函數(shù)取代了addEventListener和removeEventListener.
恰恰函數(shù)間又存在著很多的差異性,使整個事件機制變得異常復(fù)雜.
所以我們要做的事情其實就轉(zhuǎn)移成了.處理IE瀏覽器和w3c標(biāo)準(zhǔn)之間對于事件處理的差異性.
在IE下添加監(jiān)聽和移除監(jiān)聽可以這樣寫
function loadHandler() {
alert('the page is loaded!');
}
window.attachEvent('onload', loadHandler); // 添加監(jiān)聽
window.detachEvent('onload', loadHandler); // 移除監(jiān)聽
從表象看來,我們可以看出IE與w3c的兩處差異:
1. 事件前面多了個"on"前綴.
2. 去除了useCapture第三個參數(shù).
其實真正的差異遠(yuǎn)遠(yuǎn)不止這些.等我們后面會繼續(xù)分析.那么對于現(xiàn)在這兩處差異我們很容易就可以抽象出一個公用的函數(shù)
function addListener(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
}
else if (element.attachEvent) {
element.attachEvent('on' + eventName, handler);
}
else {
element['on' + eventName] = handler;
}
}
function removeListener(element, eventName, handler) {
if (element.addEventListener) {
element.removeEventListener(eventName, handler, false);
}
else if (element.detachEvent) {
element.detachEvent('on' + eventName, handler);
}
else {
element['on' + eventName] = null;
}
}
上面函數(shù)有兩處需要注意一下就是:
1. 第一個分支最好先測定w3c標(biāo)準(zhǔn). 因為IE也漸漸向標(biāo)準(zhǔn)靠近. 第二個分支監(jiān)測IE.
2. 第三個分支是留給既不支持(add/remove)EventListener也不支持(attach/detach)Event的瀏覽器.
性能優(yōu)化
對于上面的函數(shù)我們是運用"運行時"監(jiān)測的.也就是每次綁定事件都需要進行分支監(jiān)測.我們可以將其改為"運行前"就確定兼容函數(shù).而不需要每次監(jiān)測.
這樣我們就需要用一個DOM元素提前進行探測. 這里我們選用了document.documentElement. 為什么不用document.body呢? 因為document.documentElement在document沒有ready的時候就已經(jīng)存在. 而document.body沒ready前是不存在的.
這樣函數(shù)就優(yōu)化成
var addListener, removeListener,
/* test element */
docEl = document.documentElement;
// addListener
if (docEl.addEventListener) {
/* if `addEventListener` exists on test element, define function to use `addEventListener` */
addListener = function (element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
}
else if (docEl.attachEvent) {
/* if `attachEvent` exists on test element, define function to use `attachEvent` */
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, handler);
};
}
else {
/* if neither methods exists on test element, define function to fallback strategy */
addListener = function (element, eventName, handler) {
element['on' + eventName] = handler;
};
}
// removeListener
if (docEl.removeEventListener) {
removeListener = function (element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (docEl.detachEvent) {
removeListener = function (element, eventName, handler) {
element.detachEvent('on' + eventName, handler);
};
}
else {
removeListener = function (element, eventName, handler) {
element['on' + eventName] = null;
};
}
這樣就避免了每次綁定都需要判斷.
值得一提的是.上面的代碼其實也是有兩處硬傷. 除了代碼量增多外, 還有一點就是使用了硬性編碼推測.上面代碼我們基本的意思就是斷定.如果document.documentElement具備了add/remove方法.那么element就一定具備(雖然大多數(shù)情況如此).但這顯然是不夠安全.
不安全的檢測
下面兩個例子說明.在某些情況下這種檢測不是足夠安全的.
// In Internet Explorer
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
if (xhr.open) { } // Error
var element = document.createElement('p');
if (element.offsetParent) { } // Error
如: 在IE7下 typeof xhr.open === 'unknown'. 詳細(xì)可參考feature-detection
所以我們提倡的檢測方式是
var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};
這樣我們上面的優(yōu)化函數(shù).再次改進成這樣
var addListener, docEl = document.documentElement;
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
/* ... */
}
else {
/* ... */
}
丟失的this指針
this指針的處理.IE與w3c又出現(xiàn)了差異.在w3c下函數(shù)的指針是指向綁定該句柄的DOM元素. 而IE下卻總是指向window.
// IE
document.body.attachEvent('onclick', function () {
alert(this === window); // true
alert(this === document.body); // false
});
// W3C
document.body.addEventListener('onclick', function () {
alert(this === window); // false
alert(this === document.body); // true
});
這個問題修正起來也不算麻煩
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
addListener = function (element, eventName, handler) {
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
};
}
else {
/* ... */
}
我們只需要用一個包裝函數(shù).然后在內(nèi)部將handler用call重新修正指針.其實大伙應(yīng)該也看出了,這里還偷偷的修正了一個問題就是.IE下event不是通過第一個函數(shù)傳遞,而是遺留在全局.所以我們經(jīng)常會寫event = event || window.event這樣的代碼. 這里也一并做了修正.
修正了這幾個主要的問題.我們這個函數(shù)看起來似乎健壯了很多.我們可以暫停一下做下簡單的測試, 測試三點
1. 各瀏覽器兼容 2. this指針指向兼容 3. event參數(shù)傳遞兼容.
測試代碼如下:
[Ctrl+A 全選 注:引入外部Js需再刷新一下頁面才能執(zhí)行]
我們只需這樣調(diào)用方法:
addListener(o, 'click', function(event) {
this.style.backgroundColor = 'blue';
alert((event.target || event.srcElement).innerHTML);
});
可見'click' , this, event 都做到了瀏覽器一致性. 這樣是不是我們就萬事大吉了?
其實這只是萬里長征的第一步.由于IE瀏覽器下和諧的內(nèi)存泄露,使我們的事件機制要考慮的比上面復(fù)雜的多.
看下我們上面的一處修正this指針的代碼
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
element --> handler --> element 很容易的形成了個循環(huán)引用. 在IE下就內(nèi)存泄露了.
解除循環(huán)引用
解決內(nèi)存泄露的方法就是切斷循環(huán)引用. 也就是將handler --> element這段引用給切斷. 很容易想到的方法,也是至今還有很多類庫在使用的方法.就是在window窗體unload的時候?qū)⑺衕andler指向null .
基本代碼如下
代碼
function wrapHandler(element, handler) {
return function (e) {
return handler.call(element, e || window.event);
};
}
function createListener(element, eventName, handler) {
return {
element: element,
eventName: eventName,
handler: wrapHandler(element, handler)
};
}
function cleanupListeners() {
for (var i = listenersToCleanup.length; i--; ) {
var listener = listenersToCleanup[i];
litener.element.detachEvent(listener.eventName, listener.handler);
listenersToCleanup[i] = null;
}
window.detachEvent('onunload', cleanupListeners);
}
var listenersToCleanup = [ ];
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
addListener = function (element, eventName, handler) {
var listener = createListener(element, eventName, handler);
element.attachEvent('on' + eventName, listener.handler);
listenersToCleanup.push(listener);
};
window.attachEvent('onunload', cleanupListeners);
}
else {
/* ... */
}
也就是將listener用數(shù)組保存起來.在window.unload的時候循環(huán)一次全部指向為null.從此切斷引用.
這看起來是個很不錯的方法.很好的解決了內(nèi)存泄露問題.
避免內(nèi)存泄露
在我們剛剛要松口氣的時候.又一個令人咂舌的事情發(fā)生了.bfcache這個被大多主流瀏覽器實現(xiàn)的頁面緩存機制.介紹上赫然寫了幾條會導(dǎo)致緩存失效的幾個條款
the page uses an unload or beforeunload handler
the page sets "cache-control: no-store"
the page sets "cache-control: no-cache" and the site is HTTPS.
the page is not completely loaded when the user navigates away from it
the top-level page contains frames that are not cacheable
the page is in a frame and the user loads a new page within that frame (in this case, when the user navigates away from the page, the content that was last loaded into the frames is what is cached)
第一條就是說我們偉大的unload會殺掉頁面緩存.頁面緩存的作用就是.我們每次點前進后退按鈕都會從緩存讀取而不需每次都去請求服務(wù)器.這樣一來就矛盾了...
我們既想要頁面緩存.但又得切斷內(nèi)存泄露的循環(huán)引用.但卻又不能使用unload事件...
最后只能使用終極方案.就是禁止循環(huán)引用
這個方案仔細(xì)介紹起來也很麻煩.但如果見過DE大神最早的事件函數(shù).應(yīng)該理解起來就不難了. 總結(jié)起來需要做以下工作.
1. 為每個element指定一個唯一的uniqueID.
2. 用一個獨立的函數(shù)來創(chuàng)建監(jiān)聽. 但這個函數(shù)不直接引用element, 避免循環(huán)引用.
3. 創(chuàng)建的監(jiān)聽與獨立的uid和eventName相結(jié)合
4. 通過attachEvent去觸發(fā)包裝的事件句柄.
經(jīng)過上面的一系列分析.我們得到了最終的這個相對最完美的事件函數(shù)
(function(global) {
// 判斷是否具有宿主屬性
function areHostMethods(object) {
var methodNames = Array.prototype.slice.call(arguments, 1),
t, i, len = methodNames.length;
for (i = 0; i < len; i++) {
t = typeof object[methodNames[i]];
if (!(/^(?:function|object|unknown)$/).test(t)) return false;
}
return true;
}
// 獲取唯一ID
var getUniqueId = (function() {
if (typeof document.documentElement.uniqueID !== 'undefined') {
return function(element) {
return element.uniqueID;
};
}
var uid = 0;
return function(element) {
return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
};
})();
// 獲取/設(shè)置元素標(biāo)志
var getElement, setElement;
(function() {
var elements = {};
getElement = function(uid) {
return elements[uid];
};
setElement = function(uid, element) {
elements[uid] = element;
};
})();
// 獨立創(chuàng)建監(jiān)聽
function createListener(uid, handler) {
return {
handler: handler,
wrappedHandler: createWrappedHandler(uid, handler)
};
}
// 事件句柄包裝函數(shù)
function createWrappedHandler(uid, handler) {
return function(e) {
handler.call(getElement(uid), e || window.event);
};
}
// 分發(fā)事件
function createDispatcher(uid, eventName) {
return function(e) {
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
handlersForEvent[i].call(this, e || window.event);
}
}
}
}
// 主函數(shù)體
var addListener, removeListener,
shouldUseAddListenerRemoveListener = (
areHostMethods(document.documentElement, 'addEventListener', 'removeEventListener') &&
areHostMethods(window, 'addEventListener', 'removeEventListener')),
shouldUseAttachEventDetachEvent = (
areHostMethods(document.documentElement, 'attachEvent', 'detachEvent') &&
areHostMethods(window, 'attachEvent', 'detachEvent')),
// IE branch
listeners = {},
// DOM L0 branch
handlers = {};
if (shouldUseAddListenerRemoveListener) {
addListener = function(element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
removeListener = function(element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (shouldUseAttachEventDetachEvent) {
addListener = function(element, eventName, handler) {
var uid = getUniqueId(element);
setElement(uid, element);
if (!listeners[uid]) {
listeners[uid] = {};
}
if (!listeners[uid][eventName]) {
listeners[uid][eventName] = [];
}
var listener = createListener(uid, handler);
listeners[uid][eventName].push(listener);
element.attachEvent('on' + eventName, listener.wrappedHandler);
};
removeListener = function(element, eventName, handler) {
var uid = getUniqueId(element), listener;
if (listeners[uid] && listeners[uid][eventName]) {
for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) {
listener = listeners[uid][eventName][i];
if (listener && listener.handler === handler) {
element.detachEvent('on' + eventName, listener.wrappedHandler);
listeners[uid][eventName][i] = null;
}
}
}
};
}
else {
addListener = function(element, eventName, handler) {
var uid = getUniqueId(element);
if (!handlers[uid]) {
handlers[uid] = {};
}
if (!handlers[uid][eventName]) {
handlers[uid][eventName] = [];
var existingHandler = element['on' + eventName];
if (existingHandler) {
handlers[uid][eventName].push(existingHandler);
}
element['on' + eventName] = createDispatcher(uid, eventName);
}
handlers[uid][eventName].push(handler);
};
removeListener = function(element, eventName, handler) {
var uid = getUniqueId(element);
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
if (handlersForEvent[i] === handler){
handlersForEvent.splice(i, 1);
}
}
}
};
}
global.addListener = addListener;
global.removeListener = removeListener;
})(this);
至此.我們的整個事件函數(shù)算是發(fā)展到了比較完美的地步.但總歸還是有我們沒照顧到的地方.只能驚嘆IE和w3c對于事件的處理相差太大了.
遺漏的細(xì)節(jié)
盡管我們洋洋灑灑的上百行代碼修正了一個兼容的事件機制.但仍然有需要完善的地方.
1. 由于MSHTML DOM不支持事件機制不支持捕獲階段.所以第三個參數(shù)就讓他缺失去吧.
2. 事件句柄觸發(fā)順序.大多數(shù)瀏覽器都是FIFO(先進先出).而IE偏偏就要來個LIFO(后進先出).其實DOM3草案已經(jīng)說明了specifies the order as FIFO.
其他細(xì)節(jié)不一一道來.
整個文章為了記錄自己的思路.所以顯得比較啰嗦.但那個相對完美的事件函數(shù)還是有稍許參考價值, 希望會對大家有稍許幫助.
如果大家有好的意見和提議,望指教.謝謝.
代碼打包下載
相關(guān)文章
JavaScript快速排序(quickSort)算法的實現(xiàn)方法總結(jié)
快速排序的思想式 分治法,選一個基準(zhǔn)點,然后根據(jù)大小進行分配,分配然完畢之后,對已經(jīng)分配的進行遞歸操作,最終形成快速排序,所以遞歸也是快速排序思想的一個重要組成部分,本文主要給大家介紹了JavaScript實現(xiàn)快速排序的寫法,需要的朋友可以參考下2023-11-11BootStrap學(xué)習(xí)筆記之nav導(dǎo)航欄和面包屑導(dǎo)航
這篇文章主要介紹了BootStrap學(xué)習(xí)筆記之nav導(dǎo)航欄和面包屑導(dǎo)航的相關(guān)資料,需要的朋友可以參考下2017-01-01JS實現(xiàn)帶關(guān)閉功能的阿里媽媽網(wǎng)站頂部滑出banner工具條代碼
這篇文章主要介紹了JS實現(xiàn)帶關(guān)閉功能的阿里媽媽網(wǎng)站頂部滑出banner工具條代碼,可實現(xiàn)頂部banner窗口的浮動顯示及關(guān)閉隱藏功能,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09ES6中async函數(shù)與await表達(dá)式的基本用法舉例
async和await是我們進行Promise時的一個語法糖,async/await為了讓我們書寫代碼時更加流暢,增強了代碼的可讀性,下面這篇文章主要給大家介紹了關(guān)于ES6中async函數(shù)與await表達(dá)式的基本用法,需要的朋友可以參考下2022-07-07微信小程序?qū)崿F(xiàn)動態(tài)改變view標(biāo)簽寬度和高度的方法【附demo源碼下載】
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)動態(tài)改變view標(biāo)簽寬度和高度的方法,涉及微信小程序事件響應(yīng)及使用setData針對data數(shù)據(jù)動態(tài)操作相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-12-12JS設(shè)置網(wǎng)頁圖片vspace和hspace屬性的方法
這篇文章主要介紹了JS設(shè)置網(wǎng)頁圖片vspace和hspace屬性的方法,具體分析了vspace和hspace屬性的功能及javascript修改技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-04-04使用JavaScript實現(xiàn)node.js中的path.join方法
Node.JS中的 path.join 非常方便,能直接按相對或絕對合并路徑,有時侯前端也需要這種方法,如何實現(xiàn)呢?感興趣的朋友跟隨腳本之家小編一起看看吧2018-08-08