// 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'. 详细可参考feature-detection
所以我们提倡的检测方式是
复制代码 代码如下:
var isHostMethod = function (object, methodName) {
var t = typeof object[methodName];
return ((t === 'function' || t === 'object') && !!object[methodName]) || t === 'unknown';
};
这样我们上面的优化函数.再次改进成这样
复制代码 代码如下:
var addListener, docEl = document.documentElement;
if (isHostMethod(docEl, 'addEventListener')) {
/* ... */
}
else if (isHostMethod(docEl, 'attachEvent')) {
/* ... */
}
else {
/* ... */
}
丢失的this指针
this指针的处理.IE与w3c又出现了差异.在w3c下函数的指针是指向绑定该句柄的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 {
/* ... */
}
我们只需要用一个包装函数.然后在内部将handler用call重新修正指针.其实大伙应该也看出了,这里还偷偷的修正了一个问题就是.IE下event不是通过第一个函数传递,而是遗留在全局.所以我们经常会写event = event || window.event这样的代码. 这里也一并做了修正.
修正了这几个主要的问题.我们这个函数看起来似乎健壮了很多.我们可以暂停一下做下简单的测试, 测试三点
1. 各浏览器兼容 2. this指针指向兼容 3. event参数传递兼容.
测试代码如下:
[Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]
我们只需这样调用方法:
复制代码 代码如下:
addListener(o, 'click', function(event) {
this.style.backgroundColor = 'blue';
alert((event.target || event.srcElement).innerHTML);
});
可见'click' , this, event 都做到了浏览器一致性. 这样是不是我们就万事大吉了?
其实这只是万里长征的第一步.由于IE浏览器下和谐的内存泄露,使我们的事件机制要考虑的比上面复杂的多.
看下我们上面的一处修正this指针的代码
element.attachEvent('on' + eventName, function () {
handler.call(element, window.event);
});
element --> handler --> element 很容易的形成了个循环引用. 在IE下就内存泄露了.
解除循环引用
解决内存泄露的方法就是切断循环引用. 也就是将handler --> element这段引用给切断. 很容易想到的方法,也是至今还有很多类库在使用的方法.就是在window窗体unload的时候将所有handler指向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用数组保存起来.在window.unload的时候循环一次全部指向为null.从此切断引用.
这看起来是个很不错的方法.很好的解决了内存泄露问题.
避免内存泄露
在我们刚刚要松口气的时候.又一个令人咂舌的事情发生了.bfcache这个被大多主流浏览器实现的页面缓存机制.介绍上赫然写了几条会导致缓存失效的几个条款
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会杀掉页面缓存.页面缓存的作用就是.我们每次点前进后退按钮都会从缓存读取而不需每次都去请求服务器.这样一来就矛盾了...
我们既想要页面缓存.但又得切断内存泄露的循环引用.但却又不能使用unload事件...
最后只能使用终极方案.就是禁止循环引用
这个方案仔细介绍起来也很麻烦.但如果见过DE大神最早的事件函数.应该理解起来就不难了. 总结起来需要做以下工作.
1. 为每个element指定一个唯一的uniqueID.
2. 用一个独立的函数来创建监听. 但这个函数不直接引用element, 避免循环引用.
3. 创建的监听与独立的uid和eventName相结合
4. 通过attachEvent去触发包装的事件句柄.
经过上面的一系列分析.我们得到了最终的这个相对最完美的事件函数
复制代码 代码如下: