性能优化 对于上面的函数我们是运用"运行时"监测的.也就是每次绑定事件都需要进行分支监测.我们可以将其改为"运行前"就确定兼容函数.而不需要每次监测. 这样我们就需要用一个DOM元素提前进行探测. 这里我们选用了document.documentElement. 为什么不用document.body呢? 因为document.documentElement在document没有ready的时候就已经存在. 而document.body没ready前是不存在的. 这样函数就优化成 复制代码 代码如下: 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就一定具备(虽然大多数情况如此).但这显然是不够安全. 不安全的检测 下面两个例子说明.在某些情况下这种检测不是足够安全的. 复制代码 代码如下: // 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'; };
可见'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去触发包装的事件句柄. 经过上面的一系列分析.我们得到了最终的这个相对最完美的事件函数 复制代码 代码如下: (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++); }; })(); // 获取/设置元素标志 var getElement, setElement; (function() { var elements = {}; getElement = function(uid) { return elements[uid]; }; setElement = function(uid, element) { elements[uid] = element; }; })(); // 独立创建监听 function createListener(uid, handler) { return { handler: handler, wrappedHandler: createWrappedHandler(uid, handler) }; } // 事件句柄包装函数 function createWrappedHandler(uid, handler) { return function(e) { handler.call(getElement(uid), e || window.event); }; } // 分发事件 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); } } } } // 主函数体 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);
至此.我们的整个事件函数算是发展到了比较完美的地步.但总归还是有我们没照顾到的地方.只能惊叹IE和w3c对于事件的处理相差太大了. 遗漏的细节 尽管我们洋洋洒洒的上百行代码修正了一个兼容的事件机制.但仍然有需要完善的地方. 1. 由于MSHTML DOM不支持事件机制不支持捕获阶段.所以第三个参数就让他缺失去吧. 2. 事件句柄触发顺序.大多数浏览器都是FIFO(先进先出).而IE偏偏就要来个LIFO(后进先出).其实DOM3草案已经说明了specifies the order as FIFO. 其他细节不一一道来.