欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

javascript打造跨瀏覽器事件處理機制[Blue-Dream出品]

 更新時間:2010年07月13日 00:43:55   作者:  
由于瀏覽器兼容的復(fù)雜性.打造一個較優(yōu)的跨瀏覽器事件處理函數(shù).不是件容易的事情.各大類庫也都通過了種種方案去抽象一個龐大的事件機制.
使用類庫可以比較容易的解決兼容性問題.但這背后的機理又是如何呢? 下面我們就一點點鋪開來講.

首先,DOM Level2為事件處理定義了兩個函數(shù)addEventListenerremoveEventListener, 這兩個函數(shù)都來自于EventTarget接口. 

復(fù)制代碼 代碼如下:

element.addEventListener(eventName, listener, useCapture);
element.removeEventListener(eventName, listener, useCapture);

EventTarget接口通常實現(xiàn)自Node或Window接口.也就是所謂的DOM元素.
那么比如window也就可以通過addEventListener來添加監(jiān)聽.
復(fù)制代碼 代碼如下:

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)聽可以這樣寫
復(fù)制代碼 代碼如下:

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ù)
復(fù)制代碼 代碼如下:

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)化成
復(fù)制代碼 代碼如下:

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ù)情況如此).但這顯然是不夠安全.
不安全的檢測
下面兩個例子說明.在某些情況下這種檢測不是足夠安全的.
復(fù)制代碼 代碼如下:

// 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
所以我們提倡的檢測方式是
復(fù)制代碼 代碼如下:

var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};

這樣我們上面的優(yōu)化函數(shù).再次改進成這樣
復(fù)制代碼 代碼如下:

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.
復(fù)制代碼 代碼如下:

// 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
});

這個問題修正起來也不算麻煩
復(fù)制代碼 代碼如下:

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)用方法:
復(fù)制代碼 代碼如下:

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 .
基本代碼如下
代碼
復(fù)制代碼 代碼如下:

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ù)
復(fù)制代碼 代碼如下:

(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)文章

最新評論