一区二区久久-一区二区三区www-一区二区三区久久-一区二区三区久久精品-麻豆国产一区二区在线观看-麻豆国产视频

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

使用類(lèi)庫(kù)可以比較容易的解決兼容性問(wèn)題.但這背后的機(jī)理又是如何呢? 下面我們就一點(diǎn)點(diǎn)鋪開(kāi)來(lái)講.

首先,DOM Level2為事件處理定義了兩個(gè)函數(shù)addEventListener和removeEventListener, 這兩個(gè)函數(shù)都來(lái)自于EventTarget接口. 
復(fù)制代碼 代碼如下:
element.addEventListener(eventName, listener, useCapture);
element.removeEventListener(eventName, listener, useCapture);

EventTarget接口通常實(shí)現(xiàn)自Node或Window接口.也就是所謂的DOM元素.
那么比如window也就可以通過(guò)addEventListener來(lái)添加監(jiān)聽(tīng).
復(fù)制代碼 代碼如下:
function loadHandler() {
console.log('the page is loaded!');
}
window.addEventListener('load', loadHandler, false);

移除監(jiān)聽(tīng)通過(guò)removeEventListener同樣很容易做到, 只要注意移除的句柄和添加的句柄引用自一個(gè)函數(shù)就可以了.
window.removeEventListener('load', loadHandler, false);

如果我們活在完美世界.那么估計(jì)事件函數(shù)就此結(jié)束了.
但情況并非如此.由于IE獨(dú)樹(shù)一幟.通過(guò)MSDHTML DOM定義了attachEvent和detachEvent兩個(gè)函數(shù)取代了addEventListener和removeEventListener.
恰恰函數(shù)間又存在著很多的差異性,使整個(gè)事件機(jī)制變得異常復(fù)雜.
所以我們要做的事情其實(shí)就轉(zhuǎn)移成了.處理IE瀏覽器和w3c標(biāo)準(zhǔn)之間對(duì)于事件處理的差異性.

在IE下添加監(jiān)聽(tīng)和移除監(jiān)聽(tīng)可以這樣寫(xiě)
復(fù)制代碼 代碼如下:
function loadHandler() {
alert('the page is loaded!');
}
window.attachEvent('onload', loadHandler); // 添加監(jiān)聽(tīng)
window.detachEvent('onload', loadHandler); // 移除監(jiān)聽(tīng)

從表象看來(lái),我們可以看出IE與w3c的兩處差異:
1. 事件前面多了個(gè)"on"前綴.
2. 去除了useCapture第三個(gè)參數(shù).
其實(shí)真正的差異遠(yuǎn)遠(yuǎn)不止這些.等我們后面會(huì)繼續(xù)分析.那么對(duì)于現(xiàn)在這兩處差異我們很容易就可以抽象出一個(gè)公用的函數(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. 第一個(gè)分支最好先測(cè)定w3c標(biāo)準(zhǔn). 因?yàn)镮E也漸漸向標(biāo)準(zhǔn)靠近. 第二個(gè)分支監(jiān)測(cè)IE.
2. 第三個(gè)分支是留給既不支持(add/remove)EventListener也不支持(attach/detach)Event的瀏覽器.

性能優(yōu)化
對(duì)于上面的函數(shù)我們是運(yùn)用"運(yùn)行時(shí)"監(jiān)測(cè)的.也就是每次綁定事件都需要進(jìn)行分支監(jiān)測(cè).我們可以將其改為"運(yùn)行前"就確定兼容函數(shù).而不需要每次監(jiān)測(cè).
這樣我們就需要用一個(gè)DOM元素提前進(jìn)行探測(cè). 這里我們選用了document.documentElement. 為什么不用document.body呢? 因?yàn)閐ocument.documentElement在document沒(méi)有ready的時(shí)候就已經(jīng)存在. 而document.body沒(méi)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;
};
}

這樣就避免了每次綁定都需要判斷.
值得一提的是.上面的代碼其實(shí)也是有兩處硬傷. 除了代碼量增多外, 還有一點(diǎn)就是使用了硬性編碼推測(cè).上面代碼我們基本的意思就是斷定.如果document.documentElement具備了add/remove方法.那么element就一定具備(雖然大多數(shù)情況如此).但這顯然是不夠安全.
不安全的檢測(cè)
下面兩個(gè)例子說(shuō)明.在某些情況下這種檢測(cè)不是足夠安全的.
復(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
所以我們提倡的檢測(cè)方式是
復(fù)制代碼 代碼如下:
var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};

這樣我們上面的優(yōu)化函數(shù).再次改進(jìn)成這樣
復(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
});

這個(gè)問(wèn)題修正起來(lái)也不算麻煩
復(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 {
/* ... */
}

我們只需要用一個(gè)包裝函數(shù).然后在內(nèi)部將handler用call重新修正指針.其實(shí)大伙應(yīng)該也看出了,這里還偷偷的修正了一個(gè)問(wèn)題就是.IE下event不是通過(guò)第一個(gè)函數(shù)傳遞,而是遺留在全局.所以我們經(jīng)常會(huì)寫(xiě)event = event || window.event這樣的代碼. 這里也一并做了修正.
修正了這幾個(gè)主要的問(wèn)題.我們這個(gè)函數(shù)看起來(lái)似乎健壯了很多.我們可以暫停一下做下簡(jiǎn)單的測(cè)試, 測(cè)試三點(diǎn)
1. 各瀏覽器兼容 2. this指針指向兼容 3. event參數(shù)傳遞兼容.

測(cè)試代碼如下:

[Ctrl+A 全選 注:如需引入外部Js需刷新才能執(zhí)行]
我們只需這樣調(diào)用方法:
復(fù)制代碼 代碼如下:
addListener(o, 'click', function(event) {
this.style.backgroundColor = 'blue';
alert((event.target || event.srcElement).innerHTML);
});

可見(jiàn)'click' , this, event 都做到了瀏覽器一致性. 這樣是不是我們就萬(wàn)事大吉了?
其實(shí)這只是萬(wàn)里長(zhǎng)征的第一步.由于IE瀏覽器下和諧的內(nèi)存泄露,使我們的事件機(jī)制要考慮的比上面復(fù)雜的多.
看下我們上面的一處修正this指針的代碼
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
element --> handler --> element 很容易的形成了個(gè)循環(huán)引用. 在IE下就內(nèi)存泄露了.
解除循環(huán)引用
解決內(nèi)存泄露的方法就是切斷循環(huán)引用. 也就是將handler --> element這段引用給切斷. 很容易想到的方法,也是至今還有很多類(lèi)庫(kù)在使用的方法.就是在window窗體unload的時(shí)候?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ù)組保存起來(lái).在window.unload的時(shí)候循環(huán)一次全部指向?yàn)閚ull.從此切斷引用.
這看起來(lái)是個(gè)很不錯(cuò)的方法.很好的解決了內(nèi)存泄露問(wèn)題.
避免內(nèi)存泄露
在我們剛剛要松口氣的時(shí)候.又一個(gè)令人咂舌的事情發(fā)生了.bfcache這個(gè)被大多主流瀏覽器實(shí)現(xiàn)的頁(yè)面緩存機(jī)制.介紹上赫然寫(xiě)了幾條會(huì)導(dǎo)致緩存失效的幾個(gè)條款
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)
第一條就是說(shuō)我們偉大的unload會(huì)殺掉頁(yè)面緩存.頁(yè)面緩存的作用就是.我們每次點(diǎn)前進(jìn)后退按鈕都會(huì)從緩存讀取而不需每次都去請(qǐng)求服務(wù)器.這樣一來(lái)就矛盾了...
我們既想要頁(yè)面緩存.但又得切斷內(nèi)存泄露的循環(huán)引用.但卻又不能使用unload事件...
最后只能使用終極方案.就是禁止循環(huán)引用
這個(gè)方案仔細(xì)介紹起來(lái)也很麻煩.但如果見(jiàn)過(guò)DE大神最早的事件函數(shù).應(yīng)該理解起來(lái)就不難了. 總結(jié)起來(lái)需要做以下工作.
1. 為每個(gè)element指定一個(gè)唯一的uniqueID.
2. 用一個(gè)獨(dú)立的函數(shù)來(lái)創(chuàng)建監(jiān)聽(tīng). 但這個(gè)函數(shù)不直接引用element, 避免循環(huán)引用.
3. 創(chuàng)建的監(jiān)聽(tīng)與獨(dú)立的uid和eventName相結(jié)合
4. 通過(guò)attachEvent去觸發(fā)包裝的事件句柄.
經(jīng)過(guò)上面的一系列分析.我們得到了最終的這個(gè)相對(duì)最完美的事件函數(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;
};
})();
// 獨(dú)立創(chuàng)建監(jiān)聽(tīng)
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);

至此.我們的整個(gè)事件函數(shù)算是發(fā)展到了比較完美的地步.但總歸還是有我們沒(méi)照顧到的地方.只能驚嘆IE和w3c對(duì)于事件的處理相差太大了.
遺漏的細(xì)節(jié)
盡管我們洋洋灑灑的上百行代碼修正了一個(gè)兼容的事件機(jī)制.但仍然有需要完善的地方.
1. 由于MSHTML DOM不支持事件機(jī)制不支持捕獲階段.所以第三個(gè)參數(shù)就讓他缺失去吧.
2. 事件句柄觸發(fā)順序.大多數(shù)瀏覽器都是FIFO(先進(jìn)先出).而IE偏偏就要來(lái)個(gè)LIFO(后進(jìn)先出).其實(shí)DOM3草案已經(jīng)說(shuō)明了specifies the order as FIFO.
其他細(xì)節(jié)不一一道來(lái).

整個(gè)文章為了記錄自己的思路.所以顯得比較